Drawing DirectX11 text using SlimDX

August 18, 2011

I am teaching myself how to program the GPU.  My end goal is to make a planet renderer and possibly a game based on the planet, however I’m having fun just learning graphics programming.

I have become dissatisfied with XNA and it’s lack of directX 11 support, so I started using SlimDX.

Oddly Microsoft doesn’t provide a straight forward mechanism to draw text onto a directX 11 surface.  The naive solution is to draw text onto a bitmap, convert it into a texture2D, then draw to the screen.  I tried this solution and the performance was horrible (45-50 fps).  After some searching around I found out Microsoft recommends using directX10 and a sharedsurface.

After searching the web I only found a few working C++ examples.  The SlimDX C# posts were mostly about how people couldn’t get drawText to work with directX 11.  These failed C# attempts along with the successful C++ examples gave me enough to create this unofficial tutorial.  The shared surface solution nets me over 1000 fps.

It really isn’t a tutorial, just the source code of how to draw text and use an effect with SlimDX and DirectX 11.

?View Code CSHARP
/*
 * Dx11TriangleText
 * Requires SlimDX (slimdx.org)
 * Intended as an unofficial Tutorial 4
 *
 * Shows how to use an Effect.
 * Shows how to drawText with DirectX 11 as discussed at
 * http://msdn.microsoft.com/en-us/library/ee913554(v=vs.85).aspx
 *
 * Coded by Aaron Auseth
 *
 * Freeware: The author, of this software accepts no responsibility for damages resulting
 * from the use of this product and makes no warranty or representation, either
 * express or implied, including but not limited to, any implied warranty of
 * merchantability or fitness for a particular purpose. This software is provided
 * "AS IS", and you, its user, assume all risks when using it.
 *
 * All I ask is that I be given credit if you use as a tutorial or for educational purposes.
 *
*/
 
using System.Windows.Forms;
using SlimDX;
using SlimDX.D3DCompiler;
using SlimDX.Direct3D11;
using SlimDX.DXGI;
using SlimDX.Windows;
using Device = SlimDX.Direct3D11.Device;
using Resource = SlimDX.Direct3D11.Resource;
using System.Runtime.InteropServices;
 
namespace Dx11TriangleText
{
    static class Program
    {
        // Vertex Structure
        // LayoutKind.Sequential is required to ensure the public variables
        // are written to the datastream in the correct order.
        [StructLayout(LayoutKind.Sequential)]
        struct VertexPositionColor
        {
            public Vector4 Position;
            public Color4 Color;
            public static readonly InputElement[] inputElements = new[] {
                new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0),
                new InputElement("COLOR",0,Format.R32G32B32A32_Float,16,0)
            };
            public static readonly int SizeInBytes = Marshal.SizeOf(typeof(VertexPositionColor));
            public VertexPositionColor(Vector4 position, Color4 color)
            {
                Position = position;
                Color = color;
            }
            public VertexPositionColor(Vector3 position, Color4 color)
            {
                Position = new Vector4(position, 1);
                Color = color;
            }
        }
 
        [StructLayout(LayoutKind.Sequential)]
        struct VertexPositionTexture
        {
            public Vector4 Position;
            public Vector2 TexCoord;
            public static readonly InputElement[] inputElements = new[] {
                new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0),
                new InputElement("TEXCOORD",0,Format.R32G32_Float, 16 ,0)
            };
            public static readonly int SizeInBytes = Marshal.SizeOf(typeof(VertexPositionTexture));
            public VertexPositionTexture(Vector4 position, Vector2 texCoord)
            {
                Position = position;
                TexCoord = texCoord;
            }
            public VertexPositionTexture(Vector3 position, Vector2 texCoord)
            {
                Position = new Vector4(position, 1);
                TexCoord = texCoord;
            }
        }
 
        static void Main()
        {
            SlimDX.Direct3D11.Device device11;
            SwapChain swapChain;
 
            // DirectX DXGI 1.1 factory
            Factory1 factory1 = new Factory1();
 
            // The 1st graphics adapter
            Adapter1 adapter1 = factory1.GetAdapter1(0);
 
            var form = new RenderForm("Tutorial 4: Dx11 Triangle + Text");
 
            var description = new SwapChainDescription()
            {
                BufferCount = 2,
                Usage = Usage.RenderTargetOutput,
                OutputHandle = form.Handle,
                IsWindowed = true,
                ModeDescription = new ModeDescription(0, 0, new Rational(60, 1), Format.R8G8B8A8_UNorm),
                SampleDescription = new SampleDescription(1, 0),
                Flags = SwapChainFlags.AllowModeSwitch,
                SwapEffect = SwapEffect.Discard
            };
 
            SlimDX.Direct3D11.Device.CreateWithSwapChain(adapter1, DeviceCreationFlags.Debug, description, out device11, out swapChain);
 
            // create a view of our render target, which is the backbuffer of the swap chain we just created
            RenderTargetView renderTarget;
            using (var resource = Resource.FromSwapChain(swapChain, 0))
                renderTarget = new RenderTargetView(device11, resource);
 
            // setting a viewport is required if you want to actually see anything
            var context = device11.ImmediateContext;
            var viewport = new Viewport(0.0f, 0.0f, form.ClientSize.Width, form.ClientSize.Height);
            context.OutputMerger.SetTargets(renderTarget);
            context.Rasterizer.SetViewports(viewport);
 
            // A DirectX 10.1 device is required because DirectWrite/Direct2D are unable
            // to access DirectX11.  BgraSupport is required for DXGI interaction between
            // DirectX10/Direct2D/DirectWrite.
            SlimDX.Direct3D10_1.Device1 device10_1 = new SlimDX.Direct3D10_1.Device1(
                adapter1,
                SlimDX.Direct3D10.DriverType.Hardware,
                SlimDX.Direct3D10.DeviceCreationFlags.BgraSupport | SlimDX.Direct3D10.DeviceCreationFlags.Debug,
                SlimDX.Direct3D10_1.FeatureLevel.Level_10_0
            );
 
            // Create the DirectX11 texture2D.  This texture will be shared with the DirectX10
            // device.  The DirectX10 device will be used to render text onto this texture.  DirectX11
            // will then draw this texture (blended) onto the screen.
            // The KeyedMutex flag is required in order to share this resource.
            SlimDX.Direct3D11.Texture2D textureD3D11 = new Texture2D(device11, new Texture2DDescription
            {
                Width = form.Width,
                Height = form.Height,
                MipLevels = 1,
                ArraySize = 1,
                Format = Format.B8G8R8A8_UNorm,
                SampleDescription = new SampleDescription(1, 0),
                Usage = ResourceUsage.Default,
                BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
                CpuAccessFlags = CpuAccessFlags.None,
                OptionFlags = ResourceOptionFlags.KeyedMutex
            });
 
            // A DirectX10 Texture2D sharing the DirectX11 Texture2D
            SlimDX.DXGI.Resource sharedResource = new SlimDX.DXGI.Resource(textureD3D11);
            SlimDX.Direct3D10.Texture2D textureD3D10 = device10_1.OpenSharedResource(sharedResource.SharedHandle);
 
            // The KeyedMutex is used just prior to writing to textureD3D11 or textureD3D10.
            // This is how DirectX knows which DirectX (10 or 11) is supposed to be writing
            // to the shared texture.  The keyedMutex is just defined here, they will be used
            // a bit later.
            KeyedMutex mutexD3D10 = new KeyedMutex(textureD3D10);
            KeyedMutex mutexD3D11 = new KeyedMutex(textureD3D11);
 
            // Direct2D Factory
            SlimDX.Direct2D.Factory d2Factory = new SlimDX.Direct2D.Factory(
                SlimDX.Direct2D.FactoryType.SingleThreaded,
                SlimDX.Direct2D.DebugLevel.Information
            );
 
            // Direct Write factory
            SlimDX.DirectWrite.Factory dwFactory = new SlimDX.DirectWrite.Factory(
                SlimDX.DirectWrite.FactoryType.Isolated
            );
 
            // The textFormat we will use to draw text with
            SlimDX.DirectWrite.TextFormat textFormat = new SlimDX.DirectWrite.TextFormat(
                dwFactory,
                "Arial",
                SlimDX.DirectWrite.FontWeight.Normal,
                SlimDX.DirectWrite.FontStyle.Normal,
                SlimDX.DirectWrite.FontStretch.Normal,
                24,
                "en-US"
            );
            textFormat.TextAlignment = SlimDX.DirectWrite.TextAlignment.Center;
            textFormat.ParagraphAlignment = SlimDX.DirectWrite.ParagraphAlignment.Center;
 
            // Query for a IDXGISurface.
            // DirectWrite and DirectX10 can interoperate thru DXGI.
            Surface surface = textureD3D10.AsSurface();
            SlimDX.Direct2D.RenderTargetProperties rtp = new SlimDX.Direct2D.RenderTargetProperties();
            rtp.MinimumFeatureLevel = SlimDX.Direct2D.FeatureLevel.Direct3D10;
            rtp.Type = SlimDX.Direct2D.RenderTargetType.Hardware;
            rtp.Usage = SlimDX.Direct2D.RenderTargetUsage.None;
            rtp.PixelFormat = new SlimDX.Direct2D.PixelFormat(Format.Unknown, SlimDX.Direct2D.AlphaMode.Premultiplied);
            SlimDX.Direct2D.RenderTarget dwRenderTarget = SlimDX.Direct2D.RenderTarget.FromDXGI(d2Factory, surface, rtp);
 
            // Brush used to DrawText
            SlimDX.Direct2D.SolidColorBrush brushSolidWhite = new SlimDX.Direct2D.SolidColorBrush(
                dwRenderTarget,
                new Color4(1, 1, 1, 1)
            );
 
            // Think of the shared textureD3D10 as an overlay.
            // The overlay needs to show the text but let the underlying triangle (or whatever)
            // show thru, which is accomplished by blending.
            BlendStateDescription bsd = new BlendStateDescription();
            bsd.RenderTargets[0].BlendEnable = true;
            bsd.RenderTargets[0].SourceBlend = BlendOption.SourceAlpha;
            bsd.RenderTargets[0].DestinationBlend = BlendOption.InverseSourceAlpha;
            bsd.RenderTargets[0].BlendOperation = BlendOperation.Add;
            bsd.RenderTargets[0].SourceBlendAlpha = BlendOption.One;
            bsd.RenderTargets[0].DestinationBlendAlpha = BlendOption.Zero;
            bsd.RenderTargets[0].BlendOperationAlpha = BlendOperation.Add;
            bsd.RenderTargets[0].RenderTargetWriteMask = ColorWriteMaskFlags.All;
            BlendState BlendState_Transparent = BlendState.FromDescription(device11, bsd);
 
            // Load Effect. This includes both the vertex and pixel shaders.
            // Also can include more than one technique.
            ShaderBytecode shaderByteCode = ShaderBytecode.CompileFromFile(
                "effectDx11.fx",
                "fx_5_0",
                ShaderFlags.EnableStrictness,
                EffectFlags.None);
 
            Effect effect = new Effect(device11, shaderByteCode);
 
            // create triangle vertex data, making sure to rewind the stream afterward
            var verticesTriangle = new DataStream(VertexPositionColor.SizeInBytes * 3, true, true);
            verticesTriangle.Write(
                new VertexPositionColor(
                    new Vector3(0.0f, 0.5f, 0.5f),
                    new Color4(1.0f, 0.0f, 0.0f, 1.0f)
                )
            );
            verticesTriangle.Write(
                new VertexPositionColor(
                    new Vector3(0.5f, -0.5f, 0.5f),
                    new Color4(0.0f, 1.0f, 0.0f, 1.0f)
                )
            );
            verticesTriangle.Write(
                new VertexPositionColor(
                    new Vector3(-0.5f, -0.5f, 0.5f),
                    new Color4(0.0f, 0.0f, 1.0f, 1.0f)
                )
            );
 
            verticesTriangle.Position = 0;
 
            // create the triangle vertex layout and buffer
            InputLayout layoutColor = new InputLayout(device11, effect.GetTechniqueByName("Color").GetPassByIndex(0).Description.Signature, VertexPositionColor.inputElements);
            Buffer vertexBufferColor = new Buffer(device11, verticesTriangle, (int)verticesTriangle.Length, ResourceUsage.Default, BindFlags.VertexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0);
            verticesTriangle.Close();
 
            // create text vertex data, making sure to rewind the stream afterward
            // Top Left of screen is -1, +1
            // Bottom Right of screen is +1, -1
            var verticesText = new DataStream(VertexPositionTexture.SizeInBytes * 4, true, true);
            verticesText.Write(
                new VertexPositionTexture(
                        new Vector3(-1, 1, 0),
                        new Vector2(0, 0f)
                )
            );
            verticesText.Write(
                new VertexPositionTexture(
                    new Vector3(1, 1, 0),
                    new Vector2(1, 0)
                )
            );
            verticesText.Write(
                new VertexPositionTexture(
                    new Vector3(-1, -1, 0),
                    new Vector2(0, 1)
                )
            );
            verticesText.Write(
                new VertexPositionTexture(
                    new Vector3(1, -1, 0),
                    new Vector2(1, 1)
                )
            );
 
            verticesText.Position = 0;
 
            // create the text vertex layout and buffer
            InputLayout layoutText = new InputLayout(device11, effect.GetTechniqueByName("Text").GetPassByIndex(0).Description.Signature, VertexPositionTexture.inputElements);
            Buffer vertexBufferText = new Buffer(device11, verticesText, (int)verticesText.Length, ResourceUsage.Default, BindFlags.VertexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0);
            verticesText.Close();
 
            // prevent DXGI handling of alt+enter, which doesn't work properly with Winforms
            factory1.SetWindowAssociation(form.Handle, WindowAssociationFlags.IgnoreAltEnter);
 
            // handle alt+enter ourselves
            form.KeyDown += (o, e) =>
            {
                if (e.Alt && e.KeyCode == Keys.Enter)
                    swapChain.IsFullScreen = !swapChain.IsFullScreen;
            };
 
            // handle form size changes
            form.UserResized += (o, e) =>
            {
                renderTarget.Dispose();
 
                swapChain.ResizeBuffers(2, 0, 0, Format.R8G8B8A8_UNorm, SwapChainFlags.AllowModeSwitch);
                using (var resource = Resource.FromSwapChain(swapChain, 0))
                    renderTarget = new RenderTargetView(device11, resource);
 
                context.OutputMerger.SetTargets(renderTarget);
            };
 
            MessagePump.Run(form, () =>
            {
                // clear the render target to black
                context.ClearRenderTargetView(renderTarget, new Color4(0, 0, 0));
 
                // Draw the triangle
                // configure the Input Assembler portion of the pipeline with the vertex data
                context.InputAssembler.InputLayout = layoutColor;
                context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
                context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertexBufferColor, VertexPositionColor.SizeInBytes, 0));
                context.OutputMerger.BlendState = null;
                EffectTechnique currentTechnique = effect.GetTechniqueByName("Color");
                for (int pass = 0; pass < currentTechnique.Description.PassCount; ++pass)
                {
                    EffectPass Pass = currentTechnique.GetPassByIndex(pass);
                    System.Diagnostics.Debug.Assert(Pass.IsValid, "Invalid EffectPass");
                    Pass.Apply(context);
                    context.Draw(3, 0);
                };
 
                // Draw Text on the shared Texture2D
                // Need to Acquire the shared texture for use with DirectX10
                mutexD3D10.Acquire(0, 100);
                dwRenderTarget.BeginDraw();
                dwRenderTarget.Clear(new Color4(0, 0, 0, 0));
                string text = adapter1.Description1.Description;
                dwRenderTarget.DrawText(text, textFormat, new System.Drawing.Rectangle(0, 0, form.Width, form.Height), brushSolidWhite);
                dwRenderTarget.EndDraw();
                mutexD3D10.Release(0);
 
                // Draw the shared texture2D onto the screen
                // Need to Aquire the shared texture for use with DirectX11
                mutexD3D11.Acquire(0, 100);
                ShaderResourceView srv = new ShaderResourceView(device11, textureD3D11);
                effect.GetVariableByName("g_textOverlay").AsResource().SetResource(srv);
                context.InputAssembler.InputLayout = layoutText;
                context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleStrip;
                context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertexBufferText, VertexPositionTexture.SizeInBytes, 0));
                context.OutputMerger.BlendState = BlendState_Transparent;
                currentTechnique = effect.GetTechniqueByName("Text");
                for (int pass = 0; pass < currentTechnique.Description.PassCount; ++pass)
                {
                    EffectPass Pass = currentTechnique.GetPassByIndex(pass);
                    System.Diagnostics.Debug.Assert(Pass.IsValid, "Invalid EffectPass");
                    Pass.Apply(context);
                    context.Draw(4, 0);
                }
                srv.Dispose();
                mutexD3D11.Release(0);
 
                swapChain.Present(0, PresentFlags.None);
            });
 
            // clean up all resources
            // anything we missed will show up in the debug output
 
            vertexBufferColor.Dispose();
            vertexBufferText.Dispose();
            layoutColor.Dispose();
            layoutText.Dispose();
            effect.Dispose();
            shaderByteCode.Dispose();
            renderTarget.Dispose();
            swapChain.Dispose();
            device11.Dispose();
            device10_1.Dispose();
            mutexD3D10.Dispose();
            mutexD3D11.Dispose();
            textureD3D10.Dispose();
            textureD3D11.Dispose();
            factory1.Dispose();
            adapter1.Dispose();
            sharedResource.Dispose();
            d2Factory.Dispose();
            dwFactory.Dispose();
            textFormat.Dispose();
            surface.Dispose();
            dwRenderTarget.Dispose();
            brushSolidWhite.Dispose();
            BlendState_Transparent.Dispose();
 
        }
    }
}
?View Code CSHARP
Texture2D g_textOverlay;
 
SamplerState g_samLinear
{
	Filter = MIN_MAG_MIP_LINEAR;
	AddressU = CLAMP;
	AddressV = CLAMP;
};
 
// ------------------------------------------------------
// A very simple shader
// ------------------------------------------------------
 
float4 SimpleVS(float4 position : POSITION) : SV_POSITION
{
	return position;
}
 
float4 SimplePS(float4 position : SV_POSITION) : SV_Target
{
	return float4(1.0f, 1.0f, 0.0f, 1.0f);
}
 
// ------------------------------------------------------
// A shader that accepts Position and Color
// ------------------------------------------------------
 
struct ColorVS_IN
{
	float4 pos : POSITION;
	float4 col : COLOR;
};
 
struct ColorPS_IN
{
	float4 pos : SV_POSITION;
	float4 col : COLOR;
};
 
ColorPS_IN ColorVS( ColorVS_IN input )
{
	ColorPS_IN output = (ColorPS_IN)0;
	output.pos = input.pos;
	output.col = input.col;
	return output;
}
 
float4 ColorPS( ColorPS_IN input ) : SV_Target
{
	return input.col;
}
 
// ------------------------------------------------------
// A shader that accepts Position and Texture
// Used as a text overlay
// ------------------------------------------------------
 
struct TextVS_IN
{
	float4 pos : POSITION;
	float2 tex : TEXCOORD0;
};
 
struct TextPS_IN
{
	float4 pos : SV_POSITION;
	float2 tex : TEXCOORD0;
};
 
TextPS_IN TextVS( TextVS_IN input )
{
	TextPS_IN output = (TextPS_IN)0;
	output.pos = input.pos;
	output.tex = input.tex;
	return output;
}
 
float4 TextPS( TextPS_IN input ) : SV_Target
{
	float4 color =  g_textOverlay.Sample(g_samLinear, input.tex);
	return color;
}
 
// ------------------------------------------------------
// Techniques
// ------------------------------------------------------
 
technique11 Simple
{
	pass P0
	{
		SetGeometryShader( 0 );
		SetVertexShader( CompileShader( vs_4_0, SimpleVS() ) );
		SetPixelShader( CompileShader( ps_4_0, SimplePS() ) );
	}
}
 
technique11 Color
{
	pass P0
	{
		SetGeometryShader( 0 );
		SetVertexShader( CompileShader( vs_4_0, ColorVS() ) );
		SetPixelShader( CompileShader( ps_4_0, ColorPS() ) );
	}
}
 
technique11 Text
{
	pass P0
	{
		SetGeometryShader( 0 );
		SetVertexShader( CompileShader( vs_4_0, TextVS() ) );
		SetPixelShader( CompileShader( ps_4_0, TextPS() ) );
	}
}

6 Responses to “Drawing DirectX11 text using SlimDX”

  1. Absolutely brilliant. I created a whole almost a whole engine on SlimDX. With only the Text giving me troubles.

    I love DirectX11 on C++. and loved XNA, but no support for DX11. So I shifted to SlimDX. (Also should check out SharpDX).

    This is what i was looking for. Thanks a lot mate.

    Any way to subscribe to ur blog?

    of if u can add me to ur update list.

    email: red666devil@hotmail.com

  2. I looked at SharpDX, however at the time it seemed to be missing some things, such as effect support.

    I’m new to blogging, but I found google’s feedburner service. You can now subscribe via email using the widget on the righthand toolbar.

  3. [...] Among others, SlimDX uses the above described path to connect the DirectX11 and .Net worlds. This works in the context of general .Net software. SlimDX is therefore not an easy available alternative for XNA in Silverlight. One might be on the look-out for an alternative since the XNA subset in Silverlight 5 is restricted, and XNA is based on DirectX9, which is increasingly seen as becoming outdated by now. See e.g. this blog post by Aaron. [...]

  4. [...] I am trying to do is port this code into my framework: http://www.aaronblog.us/?p=36 … which is all about drawing text in SlimDX with [...]

  5. I just tried the example, and it works for me when I change FromSwapChain to

    using (var resource = Resource.FromSwapChain(swapChain, 0))

    If I use Resource then I get same error, most likely because resource is an abstract class. I used .NET reflector to inspect Resource and find it’s derived classes.

  6. Ernst Naezer also has a SharpDX Shared surface example at https://github.com/enix/SharpDXSharedResources

Leave a Reply

You must be logged in to post a comment.