Quick Overview: How to work with MSAA (DirectX too!)

Hi guys,

very quick rundown of how to make MSAA work for your 3d game.

First of all - what is msaa? It’s multisample anti aliasing and it helps to combat the “jaggies” at the edges of 3d models.
I won’t explain why oversampling is needed, but for reference:


Make it work with the backbuffer (no rendertarget used)

This is the default option and it works right from the get-go.
(If you click on the images they will show up)

inside your game.cs (so the main class from which everything is running, it’s also the first class you have next to program.cs)

        public Game()
        {
            //Initialize graphics and content
            _graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            [....]
            //HiDef enables usable shaders, safe to enable for basically all pc users
           _graphics.GraphicsProfile = GraphicsProfile.HiDef;

          _graphics.PreferMultiSampling = true;
          _graphics.ApplyChanges();
         [...]
         }

and if you want more control over how it performs:

protected override void Initialize()
        {
           [...]

            // The number determines how good our antialiasing works. 
            // Possible values are 2,4,8,16,32, but not all work on all computers.
            // 4 is safe, and 8 is too in almost all cases
            // Higher numbers mean lower framerates
            GraphicsDevice.PresentationParameters.MultiSampleCount = 8;
            _graphics.ApplyChanges();
           [...]
        }

The second part is not needed, the default one will work already.

This should work on either DirectX or OpenGL platforms.

Make it work with a rendertarget

A rendertarget is basically just a texture we draw to. The advantage of drawing to a texture first instead of the backbuffer means we can reuse it for other stuff and modify it extensively.

Unfortunately, when using DirectX, Monogame does not support MSAA currently per default.

However, if you modify the monogame source a little bit you can make it work for your game!

The simplest way to do so is to modify the RenderTarget2D.DirectX.cs a little bit (add a few lines, nothing gets lost)

for convenience: here is the complete class
namespace Microsoft.Xna.Framework.Graphics
{
public partial class RenderTarget2D
{
internal RenderTargetView[] _renderTargetViews;
internal DepthStencilView _depthStencilView;

        private void PlatformConstruct(GraphicsDevice graphicsDevice, int width, int height, bool mipMap,
            SurfaceFormat preferredFormat, DepthFormat preferredDepthFormat, int preferredMultiSampleCount, RenderTargetUsage usage, bool shared)
        {
            GenerateIfRequired();
        }

        private void GenerateIfRequired()
        {
            if (_renderTargetViews != null)
                return;

            // Create a view interface on the rendertarget to use on bind.
            if (ArraySize > 1)
            {
                _renderTargetViews = new RenderTargetView[ArraySize];
                for (var i = 0; i < ArraySize; i++)
                {
                    var renderTargetViewDescription = new RenderTargetViewDescription();
                    if (GetTextureSampleDescription().Count > 1)
                    {
                        renderTargetViewDescription.Dimension = RenderTargetViewDimension.Texture2DMultisampledArray;
                        renderTargetViewDescription.Texture2DMSArray.ArraySize = 1;
                        renderTargetViewDescription.Texture2DMSArray.FirstArraySlice = i;
                    }
                    else
                    {
                        renderTargetViewDescription.Dimension = RenderTargetViewDimension.Texture2DArray;
                        renderTargetViewDescription.Texture2DArray.ArraySize = 1;
                        renderTargetViewDescription.Texture2DArray.FirstArraySlice = i;
                        renderTargetViewDescription.Texture2DArray.MipSlice = 0;
                    }
                    _renderTargetViews[i] = new RenderTargetView(
                        GraphicsDevice._d3dDevice, GetTexture(),
                        renderTargetViewDescription);
                }
            }
            else
            {
                if (MultiSampleCount > 1)
                {
                    //_multiSampledTexture = GetMultiSampledTexture();
                    _renderTargetViews = new [] {new RenderTargetView(GraphicsDevice._d3dDevice, GetMultiSampledTexture()) };
                }
                else
                {
                    _renderTargetViews = new[] { new RenderTargetView(GraphicsDevice._d3dDevice, GetTexture())};
                }
            }

            // If we don't need a depth buffer then we're done.
            if (DepthStencilFormat == DepthFormat.None)
                return;

            // The depth stencil view's multisampling configuration must strictly
            // match the texture's multisampling configuration.  Ignore whatever parameters
            // were provided and use the texture's configuration so that things are
            // guarenteed to work.
            var multisampleDesc = GetTextureSampleDescription();

            // Create a descriptor for the depth/stencil buffer.
            // Allocate a 2-D surface as the depth/stencil buffer.
            // Create a DepthStencil view on this surface to use on bind.
            using (var depthBuffer = new SharpDX.Direct3D11.Texture2D(GraphicsDevice._d3dDevice, new Texture2DDescription
            {
                Format = SharpDXHelper.ToFormat(DepthStencilFormat),
                ArraySize = 1,
                MipLevels = 1,
                Width = width,
                Height = height,
                SampleDescription = multisampleDesc,
                BindFlags = BindFlags.DepthStencil,
            }))
            {
                // Create the view for binding to the device.
                _depthStencilView = new DepthStencilView(GraphicsDevice._d3dDevice, depthBuffer,
                    new DepthStencilViewDescription()
                    {
                        Format = SharpDXHelper.ToFormat(DepthStencilFormat),
                        Dimension = GetTextureSampleDescription().Count > 1 ? DepthStencilViewDimension.Texture2DMultisampled : DepthStencilViewDimension.Texture2D
                    });
            }
        }

        private SharpDX.Direct3D11.Resource GetMultiSampledTexture()
         { 
             var desc = new SharpDX.Direct3D11.Texture2DDescription(); 
             desc.Width = width; 
             desc.Height = height; 
             desc.MipLevels = _levelCount; 
             desc.ArraySize = 1; 
             desc.Format = SharpDXHelper.ToFormat(_format); 
             desc.BindFlags = SharpDX.Direct3D11.BindFlags.RenderTarget; 
             desc.CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.None; 
             desc.SampleDescription.Count = MultiSampleCount;
             desc.SampleDescription.Quality = (int)StandardMultisampleQualityLevels.StandardMultisamplePattern; 
             desc.Usage = SharpDX.Direct3D11.ResourceUsage.Default; 
             desc.OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.None; 
             if (_mipmap) 
                 desc.OptionFlags |= SharpDX.Direct3D11.ResourceOptionFlags.GenerateMipMaps; 
 
 
             if (_shared) 
                 desc.OptionFlags |= SharpDX.Direct3D11.ResourceOptionFlags.Shared;

             _sampleDescription = desc.SampleDescription;

             _texture = new SharpDX.Direct3D11.Texture2D(GraphicsDevice._d3dDevice, desc);

             return _texture;
         }


    private void PlatformGraphicsDeviceResetting()
        {
            if (_renderTargetViews != null)
            {
                for (var i = 0; i < _renderTargetViews.Length; i++)
                    _renderTargetViews[i].Dispose();
                _renderTargetViews = null;
            }
            SharpDX.Utilities.Dispose(ref _depthStencilView);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_renderTargetViews != null)
                {
                    for (var i = 0; i < _renderTargetViews.Length; i++)
                        _renderTargetViews[i].Dispose();
                    _renderTargetViews = null;
                }
                SharpDX.Utilities.Dispose(ref _depthStencilView);
            }

            base.Dispose(disposing);
        }

        RenderTargetView IRenderTarget.GetRenderTargetView(int arraySlice)
        {
            GenerateIfRequired();
            return _renderTargetViews[arraySlice];
        }

        DepthStencilView IRenderTarget.GetDepthStencilView()
        {
            GenerateIfRequired();
            return _depthStencilView;
        }

        public void ResolveSubresource(RenderTarget2D destination)
        {
            GraphicsDevice._d3dContext.ResolveSubresource(this._texture, 0, destination._texture, 0,
                SharpDXHelper.ToFormat(_format));
        }
    }
}

The main things added are:

  1. private SharpDX.Direct3D11.Resource GetMultiSampledTexture()
    which is an alternative to our default GetTexture()
  2. public void ResolveSubresource(RenderTarget2D destination)
    which copies our multisampled texture to a not multisampled texture so we can use it
  3. an if function to check which getTexture we use

alright. That’s it for the monogame source.
Most of this stuff is copied from here: MSAA? Does it work? How to make it work?

Now we can use a rendertarget with a preferred MultisampleCount > 0, as always 2,4,8 are your candidates.

We also need a resolve texture which we can use as a resource after the rendering of our geometry.

Here is an example from my application.

_renderTarget = new RenderTarget2D(graphicsDevice: _graphicsDevice, 
                width: GameSettings.g_ScreenWidth / scale, 
                height: GameSettings.g_ScreenHeight / scale,
                mipMap: false,
                preferredFormat: SurfaceFormat.Color,
                preferredDepthFormat: DepthFormat.Depth24,
                preferredMultiSampleCount: 8,
                usage: RenderTargetUsage.DiscardContents
                );

            _resolvedRenderTarget = new RenderTarget2D(graphicsDevice: _graphicsDevice,
                width: GameSettings.g_ScreenWidth / scale,
                height: GameSettings.g_ScreenHeight / scale,
                mipMap: false,
                preferredFormat: SurfaceFormat.Color,
                preferredDepthFormat: DepthFormat.None,
                preferredMultiSampleCount: 0,
                usage: RenderTargetUsage.DiscardContents
                );

When we finish the rendering (we drew all meshes) we need to copy the MSAA’d texture to the standard texture.
We can do it like this

if (_renderTarget.MultiSampleCount > 1)
            {
                _renderTarget.ResolveSubresource(_resolvedRenderTarget);

                DrawMapToScreenToFullScreen(_resolvedRenderTarget);
            }
            else
            {
                DrawMapToScreenToFullScreen(_renderTarget);
            }

In this case DrawMapToFullScreen just draws a texture to the backbuffer.

Done!

FAQ

  • why are you not contributing to the monogame source?
    Several things. First of all, this does only work with non-mipmapped non-atlassed rendertargets. That’s most likely all the RT’s you need, but not good enough for a release to the public.
    Then you also have the issue that the developer has to resolve the texture “by hand”. Which is good in cases where people know what they are doing but ideally this is solved behind the scenes and changing the multisamplecount on a rendertarget just works. That’s not trivial though.

internal SharpDX.Direct3D11.ShaderResourceView GetShaderResourceView() in Texture.DirectX.cs has to be handled, too.

One could have an additional referenced texture inside the RenderTarget2D as well and that could be used as a target for the MSAA resolve, but would be dead weight for non-msaa rendertargets.

It would also be useful to find out whether or not one can use a simple Texture2D instead of RT2D as a resolve texture. Most likely, yes. But that might be an issue for someone in the future, too.

  • Why desc.SampleDescription.Quality = (int)StandardMultisampleQualityLevels.StandardMultisamplePattern ?
    You can also use zero here, or find out what MSAA patterns / techniques your vendor supports (I might write some stuff about that soon). There is also potential to let the developer decide the quality or to set it to maximum by default, but the exact quality level number depends on sample count. Setting it to -1 (StandardMultisamplePattern) or 0 is safe.
5 Likes

Great guide! Stuff like this should go in the docs!

Great writeup, well done.

Just a heads up that this is not the right way to handle setting the MS count. It will work in MG right now, but it doesn’t work in XNA, so this can change in the future. If you want to control the MS count directly the recommended way (and what works in XNA) is to respond to the PreparingDeviceSettings event and setting the MultisampleCount value in the PresentationParameters of the event args.

how MSAA work in deferred rendering pipeline

How would that look like in code (am in mobile Right now, can’t reproduce)? Im sure that would make it easier for people to integrate or correctly.

I never knew there was such an event in the first place

I’m on mobile too, I’ll post a code example later.
GraphicsDeviceManager has an event PreparingDeviceSettings that is raised whenever a GraphicsDevice is created or when ApplyChanges is called on the GDM. The event argument contains a GraphicsDeviceInformation instance which has a PresentationParameters property. Basically the event allows you to modify the PresentationParameters before they’re passed to the GraphicsDevice. So you can use that to set the MS count.

I’m in favor of replacing the PreferMultisampling property in GDM with a MultisampleCount property though.

Sample code:

GraphicsDeviceManager graphics;        
public Game1()
{
    graphics = new GraphicsDeviceManager(this);
    graphics.PreparingDeviceSettings += SetMultiSampling;
}

private void SetMultiSampling(object sender, PreparingDeviceSettingsEventArgs e)
{
    var pp = e.GraphicsDeviceInformation.PresentationParameters;
    pp.MultiSampleCount = 4;
}

i had try to implement msaa to my gbuffer (diffuse RenderTarget) but seem dont work. can anyone help?

//Device setup
graphics.GraphicsProfile = Microsoft.Xna.Framework.Graphics.GraphicsProfile.HiDef;
graphics.PreferMultiSampling = true;
graphics.GraphicsDevice.PresentationParameters.MultiSampleCount = 8;
graphics.ApplyChanges();

//RT setup
DiffuseBuffer = new RenderTarget2D(_device, _device.Viewport.Width, _device.Viewport.Height, false, SurfaceFormat.Color, DepthFormat.Depth24,8, RenderTargetUsage.DiscardContents);

//bind gbuffer
_gbufferBinding[0] = new RenderTargetBinding(DiffuseBuffer);

//set gbuffer
_device.SetRenderTargets(_gbufferBinding);

Do you resolve your target?

did u mean _renderTarget.ResolveSubresource ?? i tot with v3.6 is already done it for us?

I must have missed that, as far s I know monogame does not support msaa’d rendertargets by default

so what u mean is i still have to modify from the source?

That’s why made this thread

Someone actually tried to actually commit to the source, but the pull request is still pending (see here https://github.com/MonoGame/MonoGame/pull/5538)

hi @kosmonautgames , sorry for ressurrecting an old topic.

Should MSAA antialiase textures too or does it just antialiasses the edges of triangles?

I know the question sounds weird, but I’m writing a kind of voxel game. Until now I was rendering all the quads as two triangles each and the MSAA made it look very well. However now I’m optimizing the voxel objects, replacing groups of faces by larger quads with texture (point sampled), and the edges of the objects keep looking good, but the texture looks jaggy.

thanks!

I found the answer in the wikipedia (weird place to find monogame answers :slight_smile: ) “Multisampling calculates textures only once per pixel”, so I’ll have to resort to another kind of AA.