Bizarre line artifact when drawing outlined rectangle with multi-sampling

So, I noticed in my project some bizarre line artifacts that would go away if multi-sampling is disabled. Here’s sample code that shows the artifact:

public class Game1 : Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    Texture2D blankTexture;
    RectangleF box;

    RenderTarget2D renderTarget;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        IsFixedTimeStep = false;
        graphics.SynchronizeWithVerticalRetrace = true;
        graphics.PreferMultiSampling = true;
    }

    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        blankTexture = new Texture2D(GraphicsDevice, 1, 1);

        Color[] colors = new Color[1];
        colors[0] = new Color(255, 255, 255);

        blankTexture.SetData(colors);

        box = new RectangleF(124, 49, 100, 100);

        renderTarget = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight,
            false, SurfaceFormat.Color, DepthFormat.None, 8, RenderTargetUsage.PreserveContents);

        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        KeyboardState keys = Keyboard.GetState();
        if (keys.IsKeyDown(Keys.Escape))
            Exit();

        float moveSpeed = 50f;
        float moveAmount = (float)(moveSpeed * gameTime.ElapsedGameTime.TotalSeconds);

        if (keys.IsKeyDown(Keys.Up))
            MoveBox(0, -moveAmount);
        if (keys.IsKeyDown(Keys.Down))
            MoveBox(0, moveAmount);
        if (keys.IsKeyDown(Keys.Left))
            MoveBox(-moveAmount, 0);
        if (keys.IsKeyDown(Keys.Right))
            MoveBox(moveAmount, 0);

        base.Update(gameTime);

        void MoveBox(float x, float y)
        {
            box.X += x;
            box.Y += y;
        }
    }

    protected override void Draw(GameTime gameTime)
    {
        bool useRenderTarget = true;

        if (useRenderTarget)
            GraphicsDevice.SetRenderTarget(renderTarget);
        GraphicsDevice.Clear(Color.White);

        spriteBatch.Begin();
        RectangleF rectangle = box;
        DrawRectangle(spriteBatch, blankTexture, rectangle, Color.Black);
        rectangle.Inflate(-2, -2);
        DrawRectangle(spriteBatch, blankTexture, rectangle, Color.White);
        spriteBatch.End();

        if (useRenderTarget)
        {
            GraphicsDevice.SetRenderTarget(null);
            GraphicsDevice.Clear(Color.CornflowerBlue);
            spriteBatch.Begin();
            spriteBatch.Draw(renderTarget, renderTarget.Bounds, new Color(255, 255, 255));
            spriteBatch.End();
        }

        base.Draw(gameTime);
    }

    public void DrawRectangle(SpriteBatch batch, Texture2D texture, RectangleF rectangle, Color color)
    {
        if (rectangle.IsEmpty)
            return;

        Vector2 scale = rectangle.Size / texture.Bounds.Size.ToVector2();
        batch.Draw(texture, rectangle.Location, null, color, 0f, Vector2.Zero, scale, SpriteEffects.None, 1f);
    }
}

// RectangleF struct, with elements unnecessary to the example removed
public struct RectangleF
{
    public float X;
    public float Y;
    public float Width;
    public float Height;

    public Vector2 Location
    {
        get => new(X, Y);
        set
        {
            X = value.X;
            Y = value.Y;
        }
    }
    public Vector2 Size
    {
        get => new(Width, Height);
        set
        {
            Width = value.X;
            Height = value.Y;
        }
    }
    public bool IsEmpty => Width == 0 || Height == 0;

    public RectangleF(float x, float y, float width, float height)
    {
        X = x;
        Y = y;
        Width = width;
        Height = height;
    }

    public void Inflate(float xAmount, float yAmount)
    {
        X -= xAmount;
        Y -= yAmount;
        Width += xAmount * 2f;
        Height += yAmount * 2f;
    }
}

Here’s the output with 8 passed as the PreferredMultiSampleCount to the RenderTarget2D constructor:

See that little grey line in the upper-right corner of the box? That shouldn’t be there, and it goes away if you set the PreferredMultiSampleCount to 1:

It also changes and goes away if you move the box around with the arrow keys. In the sample code, it also goes away if you set PreferredMultiSampleCount to 4, but in my actual project the artifacts remain even at a PreferredMultiSampleCount of 4.

The artifact still shows up if you skip the rendertarget by setting useRenderTarget to false, as long as graphics.PreferMultiSampling is set to true.

Anyone have any insight as to what might be causing this bizarre behavior?

Oh, and if you set the RenderTargetUsage to DiscardContents, the line instead appears and disappears, flickering, if that helps.

Edit: So, I tested it again and now it seems it does go away in my actual project at PreferredMultiSampleCount of 4, when before it didn’t. Weird. Also, the bug doesn’t show up at all in my OpenGL build, so it seems to be DirectX only.

I just added an issue on Github because I think this might actually be a bug and not intended behavior.

1 Like