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:
- Dispose resources bound to the render target
- Dipose the render target itself
- Dispose the depth buffer
- Resize the buffers of the swap chain
- Create a new render target buffer from the swap chain
- Create a new render target
- Create a new depth/stencil buffer
- Create a new viewport and assign it to the rasterizer
- Set the render target buffer and the depth/stencil buffer in the output merger
- 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:- The RenderControl: the control I am rendering to
- DeviceManager: responsible for managing the device and its buffers
- 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.