• Advertisement
Sign in to follow this  

The best way to render text in D3D12

This topic is 716 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I used to render text in D3D10/11 with multiple draw() calls under immediate context, which seems inefficient but logically easy, it works like this:

 

*update char position in constant buffer

*draw the quad (accurately 2 triangle lists) according to the char position

*update position in CB for the next char

*draw the quad again

......

 

Obviously it's easy to implement but bad in performance. With the advent of command list introduced by D3D12, such implementation would no longer work, as we can't update the CB in the prepare stage of commandlist when it hasn't render anything yet.

 

I read a few samples from MS, one is in MiniEngine in D3D12Sample another is in XBoxOne SDK implemented with D3D11. Both samples use the way of dynamically updating VB/IB for quad rendering before a single draw() call for a line of string characters.

 

It does make sense the performance would improve with a single draw(), however I care a little about the frequent updated VB/IB each time in CPU side. Consequently I implemented my own way to use single draw() call but without VB/IB, I use the non-buffer dynamic IA input instead. Thus, I just need to handle the quad position/UV in the vertex shader and update the list of string offsets in a CB array.

 

I personally feel it would be a better choice in both performance and coding aspects.

 

Any one has other ideas?

Share this post


Link to post
Share on other sites
Advertisement

This is a bitmap font (e.g. textured quad) correct? Are characters fixed-size or variable width?

Share this post


Link to post
Share on other sites

Obviously it's easy to implement but bad in performance. With the advent of command list introduced by D3D12, such implementation would no longer work, as we can't update the CB in the prepare stage of commandlist when it hasn't render anything yet.

It is possible -- D3D11 implements dynamic cbuffers inside the graphics driver (via WRITE_DISCARD), but in D3D12 you have to implement dynamic-cbuffers in your engine code -- which actually lets you do it more efficiently than in D3D11. It might be useful to implement these so you can use them in other rendering systems too :wink:

Doing that might make you realize how much work you're asking the driver to do in D3D11 -- it's managing a constant stream of oprphaned cbuffer packets, and is streaming them through to the GPU for you -- much like a dynamic vertex buffer will.

I read a few samples from MS, one is in MiniEngine in D3D12Sample another is in XBoxOne SDK implemented with D3D11. Both samples use the way of dynamically updating VB/IB for quad rendering before a single draw() call for a line of string characters.
 
It does make sense the performance would improve with a single draw(), however I care a little about the frequent updated VB/IB each time in CPU side.

Your current cbuffer-per-character is transferring far more dynamic data than a dynamic VB will do.

Edited by Hodgman

Share this post


Link to post
Share on other sites

This is a bitmap font (e.g. textured quad) correct? Are characters fixed-size or variable width?

Yes, exactly. However since we render the characters from the fixed-size texture to the destination buffer, the latter's size can be adjusted ourselves.

Share this post


Link to post
Share on other sites

 

Obviously it's easy to implement but bad in performance. With the advent of command list introduced by D3D12, such implementation would no longer work, as we can't update the CB in the prepare stage of commandlist when it hasn't render anything yet.

It is possible -- D3D11 implements dynamic cbuffers inside the graphics driver (via WRITE_DISCARD), but in D3D12 you have to implement dynamic-cbuffers in your engine code -- which actually lets you do it more efficiently than in D3D11. It might be useful to implement these so you can use them in other rendering systems too :wink:

Doing that might make you realize how much work you're asking the driver to do in D3D11 -- it's managing a constant stream of oprphaned cbuffer packets, and is streaming them through to the GPU for you -- much like a dynamic vertex buffer will.

I read a few samples from MS, one is in MiniEngine in D3D12Sample another is in XBoxOne SDK implemented with D3D11. Both samples use the way of dynamically updating VB/IB for quad rendering before a single draw() call for a line of string characters.
 
It does make sense the performance would improve with a single draw(), however I care a little about the frequent updated VB/IB each time in CPU side.

Your current cbuffer-per-character is transferring far more dynamic data than a dynamic VB will do.

 

No doubt, agree on your first comment.

 

You probably misunderstand my current method:

I update CB each time when I output a line of text, rather than one char.

I need to update the following info in the CB: the offset of each char, lets say if the base char is 'a', and I want to display the string "bcdef", I just need to give the following array in the CB:

{ 1, 2, 3, 4, 5 }

 

And in the draw call I specified the number of vertex as 5, simply because I need to draw 5 chars here

Then in the vertex shader, I handle the non-buffer IA input id, namely 0,1,2,3,4, and output 5 quads accordingly.

 

So you see, without using VB, I put all the process in shader while maintaining an integer array in the CB.

Share this post


Link to post
Share on other sites

In my immediate mode GUI I achieve some pretty nice font rendering, dynamic geometry for the whole UI is created each frame:

 

chessboard.png

 

I create quad lists for the text and update a vertex and index buffer each frame. I originally looked at using distance field fonts but i found it hard to achieve the same quality of rendering. The bitmap font sheet was rendered with the Angel Code bitmap font tool. I've added in some GUI elements that use the different font effects such as italic and drop shadow.

Edited by Dave

Share this post


Link to post
Share on other sites
There are several ways to render text to the screen. The one I have adopted for directx 12 is the same one used in the mini engine. It is very complicated though to someone just starting out with directx 12. What is 100 times easier and will achieve almost the same level of quality is directx 2d. You can find a complete example of it being used for a 3D hud in the https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/Simple3DGameDX

Specifically look at this file
https://github.com/Microsoft/Windows-universal-samples/blob/master/Samples/Simple3DGameDX/cpp/GameInfoOverlay.cpp

Share this post


Link to post
Share on other sites

There are several ways to render text to the screen. The one I have adopted for directx 12 is the same one used in the mini engine. It is very complicated though to someone just starting out with directx 12. What is 100 times easier and will achieve almost the same level of quality is directx 2d. You can find a complete example of it being used for a 3D hud in the https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/Simple3DGameDX

Specifically look at this file
https://github.com/Microsoft/Windows-universal-samples/blob/master/Samples/Simple3DGameDX/cpp/GameInfoOverlay.cpp

 

Thanks for your info!

 

I've got some time to encapsulate my font implementation into a class and, although my method appears distinctive from the majority, I still prefer it.

 

The reason I'm happy with it is:

*It process all the job in shader rather than updating VB/IB in CPU side. It updates an uint array in CB instead each time when about to draw a line of text. As a result it transmit less info (just one uint value representing the offset of an char in fontmap, not 4 vertex positions)

*It effectively utilizes the non-buffer IA input which was introduced since D3D10, so like such dynamical and simple vertex data, we can simply use vertex shader to generate.

*CPU works less and less transmission between CPU & GPU

 

 

 

 

Here is the VS/PS code I implemented:

 

 

#define NUM_CHAR_FONT_MAP 95 //total number of characters in the font map texture
#define MAX_NUM_DRAWABLE_CHAR 40 //the maximum number of text we can draw once

cbuffer ConstantBuffer : register(b0)
{
    //because of the restriction of array padding in HLSL which is described in GetIntAtIndex()
    //We use uint4 array to store all the single uint elements
    //Therefore, 5 elements mean actual 20 scalar uint elements
    uint4 DisplayText[MAX_NUM_DRAWABLE_CHAR/4]; //the array contains the offset of each char(from ' ')

    float2 ScreenPos;
};


Texture2D       FontMap : register(t0);
SamplerState    ColorSmp     : register(s0);


//Calculate the texture coordinates of the left and right points of the character in the font map
float2 GetLeftAndRight(unsigned int index) //input arguement: the index of the character in the font map
{
    float2 v;
    v.x = 1.0f / NUM_CHAR_FONT_MAP * (float)index; //left position
    v.y = v.x + 1.0f / NUM_CHAR_FONT_MAP; //right position

    return v;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Get the integer element from an uint4 array with the logic index of the single integer
//The reason we need to implement this is, in HLSL shader all the array is packed with 4 element size in the address space
//which means if we define a uint[2], the second element jumped to the next 4 size of uint, as a result it's just suitable
//for use to use uint4 array instead if not wasting memory space. However we need to encode and decode the data from the uint4 vector
//array to a logical scalar array.
uint GetIntAtIndex(uint4 array[MAX_NUM_DRAWABLE_CHAR / 4], //uint4 array that contains the whole data
                   uint index) //the perceived logical index for the scalar uint element
{
    //Firstly determine which uint4 element contains the scalar uint element we want to get
    uint index_of_vector;
    index_of_vector = index / 4; //calculate the index

    //then calculate the position of our desired scalar element inside the corresponding uint4 vector element
    uint element_index;
    element_index = index % 4; //get the index (from 0 to 3, as there are 4 elements)

    //convert that uint4 vector element into a simple float4 value so that we can easily use array index to retrieve the
    //wanted element, rather than using .x/.y/.z/.w
    float4 Datax4 = array[index_of_vector];

    return Datax4[element_index]; //return the acquired scalar uint element
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
void VSMain(uint vertexId : SV_VertexID, //automatically generated ID from IA stage (without the need of vertex buffer)

    out float4 out_pos : SV_POSITION, //output position to the pipline
    out float2 out_tex : TEXCOORD0 )
{
    float2 tex;

    //each char is a quad consisting of 2 triangles (6 vertics)
    //The vertexId shows which quad this vertex belongs to (by deviding 6)
    unsigned int char_index = vertexId / 6; //shows the index of the character we want to draw in the string

    float left, right; // the left and right position(in UV coordinates) of the character in font map
    float2 v;
    unsigned int index;

    //get the the position info of char we want to draw this time in bitmap font
    //to calculate this positions, we need to get the corresponding element stored in array DisplayText
    //However since it's an uint4 vector array, we need to call GetIntAtIndex() to get the scalar uint element
    //with the perceived scalar uint index.
    uint char_offset_in_fontmap = GetIntAtIndex(DisplayText, char_index);
    v = GetLeftAndRight(char_offset_in_fontmap); //get the char position info in font map
    left = v.x;
    right = v.y;

    float2 pos = float2(ScreenPos.x, ScreenPos.y); //The position on the target buffer

    float2 size; //the size of each character in the target buffer
    size.x = 0.05; //width
    size.y = size.x * 2.5f; //height is 2.5 time of width

    pos.x += char_index * size.x; //get the position of this char in target buffer
    
    //draw the 2 triangles which form a quad
    switch (vertexId % 6)
    {
    //the first triangle in the quad
    case 0:
        out_pos = float4(pos.x, pos.y, 0, 1.0f); //bottom left
        tex = float2(left, 1.0f);
        break;
    case 1:
        out_pos = float4(pos.x, pos.y + size.y, 0, 1.0f); //top left
        tex = float2(left, 0.0f);
        break;
    case 2:
        out_pos = float4(pos.x + size.x, pos.y, 0, 1.0f); //right bottom
        tex = float2(right, 1.0f);
        break;

    //the second triangle in the quad
    case 3:
        out_pos = float4(pos.x + size.x, pos.y, 0, 1.0f); //right bottom
        tex = float2(right, 1.0f);
        break;
    case 4:
        out_pos = float4(pos.x, pos.y + size.y, 0, 1.0f); //top left
        tex = float2(left, 0.0f);
        break;
    case 5:
        out_pos = float4(pos.x + size.x, pos.y + size.y, 0, 1.0f); //right top
        tex = float2(right, 0.0f);
        break;
    }

    out_tex = tex; //just pass the texture UV coordinates of the character in font map
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
void PSMain(float4 pos: SV_POSITION, //output position to the pipline
    float2 tex : TEXCOORD0,

    out float4 FinalColor : SV_Target)
{
    FinalColor = FontMap.Sample(ColorSmp, tex);
    
    if(FinalColor.r < 0.04f && FinalColor.g < 0.04f && FinalColor.b < 0.04f)
        discard; //just not draw pixel this pass
}

Edited by Yu Liu

Share this post


Link to post
Share on other sites

The only problem I encountered using this way is, as described in the comments above, we can't simple use array of scalar variables in HLSL to map consecutive bytes from CPU. However a workaround is to use vector4 type array to pack them and then decode it into byte stream. That's an interesting discovery through this task.

 

By the way the reason I didn't want to use Direct2D is, simply because the latter is encapsulated with D3D. It's just because we feel it's fun to implement the low-level code using the "close-to-metal" D3D12 API, I don't think we need any other existing lib then.

 

Though PS4's API is said to be even closer to metal compared to D3D12.

Edited by Yu Liu

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement