Saturday, September 20, 2025

Text Rendering in HLSL

 You can view the entire text.hlsl file you can include here.

Intro

For a long time, I've wanted a way to directly output text to a render target from HLSL. It would be similar to printf debugging, but output directly to the screen instead of to a log file somewhere.

There have already been some other efforts to add printf style support to shaders, but that usually involves outputting strings to a buffer and then reading that buffer back on the CPU.

While a common approach to rendering text is to use a texture of a font and then drawing textured quads for each glyph, I wanted to use a different approach. I wanted to avoid binding a texture entirely and use a vector font.

I had previous written debug shape drawing functions in HLSL to draw things like lines, circles, and boxes. For a vector font, I would only need lines, so I was prepared from that side of thing.

A perfect, simple, vector line-based font is the Hershey set.

Process Hershey Fonts via C#

I downloaded the full Hershey set of Roman characters and immediately began writing a simple C# script in LINQPad to process the file.

An important thing to note is that the original Hershey file is hard-wrapped at 72 characters. This small thing caused me some issues until I saw it mentioned here.

In order to verify that my C# parser was working correctly, I wrote code to write out each glyph to a Bitmap.



With all of the glyphs being properly processed and written, I then moved on to mapping the characters I wanted into the standard ASCII set. Luckily, there are already .hmp files which do exactly this for each of the different font styles in the Hershey set. I was interested in only the Roman Simplex mapping. With very little effort, I was soon outputting the appropriate glyphs.

Generate HLSL Code

With the glyphs defined, I wrote more C# code to output HLSL code to draw the glyphs. I started with a simple, brute-force solution where I output a large switch statement, with a case for each ASCII character. Each case would have a series of DrawLine() calls that would represent the glyph.

The function signature is this:

float2 DrawCharacter(RWTexture2D<float4> Output, uint CharacterCode, float2 Position, float4 Color)

For the character H, it would generate this:

case 72: // "H" (Hershey #508)
DrawLineDDA(Output, float2(Position.x + -7, Position.y + -12), float2(Position.x + -7, Position.y + 9), Color);
DrawLineDDA(Output, float2(Position.x + 7, Position.y + -12), float2(Position.x + 7, Position.y + 9), Color);
DrawLineDDA(Output, float2(Position.x + -7 * Scale, Position.y + -2), float2(Position.x + 7, Position.y + -2), Color);
Position.x += 32;
break;

This generated over 1200 lines in a single HLSL function, but it worked ... mostly. 



It would take about 2-3 seconds to compile the HLSL, which is long, but not ridiculous. The main problem was, it would take three minutes to generate the PSO the first time when calling SetPipelineState in D3D12 on a PC with an i9-14900K and an RTX 4090. That was completely unacceptable!

I did attempt swapping the attribute on the switch statement to be [branch], [call], etc, but it did nothing to change the compilation or PSO creation time. It was clear I needed a different approach.

Generate HLSL Array

I figured that since all I needed were the x and y positions for the vertices for the lines, then I could store each line in an array, and use the exact same DrawCharacter() function with no massive switch statement.

float2 DrawCharacter(RWTexture2D<float4> Output, uint CharacterCode, float2 Position, float4 Color)
{
int ArrayIndex = CharacterCode - 32;
int LineCount = RomanSimplexFont[ArrayIndex][0];
int Width = RomanSimplexFont[ArrayIndex][1];

for (int i = 2; i < LineCount; i++)
{
float2 Start = float2(Position.x + RomanSimplexFont[ArrayIndex][i], Position.y + RomanSimplexFont[ArrayIndex][i+1]);
float2 End = float2(Position.x + RomanSimplexFont[ArrayIndex][i+2], Position.y + RomanSimplexFont[ArrayIndex][i+3]);
DrawLineDDA(Output, Start, End, Color);
}

return Position + float2(Width, 0);
}

int Array

I changed my C# to output a simple two-dimensional int array. Unfortunately, it obviously needed all of the glyphs to have the same size, which was the largest symbol, which is the @ symbol with 48 lines. 48 lines with 2 vertices per line and 2 integers per vertex equaled 192 integers for 95 ASCII characters.

int RomanSimplexFont[95][192]

For the character H, it would generate this:

    3, 32, // ASCII 72 "H" (Hershey #508)
    -7, -12, -7, 9,
    7, -12, 7, 9,
    -7, -2, 7, -2,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,

Note all of the zeros that are needs to pad it out to fill the 192 integers.

So, I generated the entire array, went to compile the shader ... and it failed. My constant buffer was larger than the limit of 65,536 bytes or 16,384 ints. I was using 95 * 192 = 18,240 ints. 

However!

The compiler wasn't reporting that my array was the expected 72,960 bytes. Instead, it was reporting that it was 297,972 bytes, which was nearly 4 times larger than expected! I was cursed (yet again) by the HLSL packing rules for constant buffers, which have bitten me so many times in the past.

int4 Array

So, I updated the code generator to create an array of int4 which resulted in the expected 74,480 bytes.

int4 RomanSimplexFont[95][49]

However, this is still obviously over the 65,536 limit, so I needed to do something to get the Hershey data under the 64K.

int14_t4 Array

I attempted to use the int16_t4 data type (enabled via the DXC -enable-16bit-types flag), which I assumed would cut my array size in half. 

int16_t4 RomanSimplexFont[95][49]

But surprisingly, it stayed the exact same size. This is due to DXC/HLSL treating any constant buffer array as 32-bit, even if you explicitly use a 16-bit value. It will automatically pad the values. Note that if you create an array inside a function, it will be the correct size.

Bit-Packed int4 Array

Since all of the coordinates are less than abs(128), that means I can use an 8-bit signed number to represent each one. So, I could pack all 4 coordinates of a line into a single 32-bit int. That means I could store 4 lines in each int4!

I updated my C# code yet again to perform all of the proper bit-packing and generate the HLSL array. It only needed to 13 int4s per glyph due to the @ symbol having 48 lines. That is 12 int4s + 1 extra to store the line count, width, left padding, and right padding.

static const int4 RomanSimplexFont[96][13]

For the character H, it would generate this:

    // ASCII 72 "H" (Hershey #508)
    int4(3, 22, -11, 11),
    int4(-101385975, 133433097, -100792322, 0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),
    int4(0,0,0,0),

This worked and I was able to render text with a 2-3 second shader compilation and a negligible PSO compilation!



ItoA

Outputting static text alone isn't as useful. I would obviously want to output numeric values that were constantly changing, such as frame-rates or pixel values. Unfortunately, HLSL has no string processing or string functions at all. I found a C custom implementation of itoa and I ported it over to HLSL easily.

void itoa(int value, inout uint buffer[256])

FtoA

Similarly, I did the same for ftoa.

void ftoa(float value, inout uint buffer[256], int afterpoint = 2)

TEXT() Preprocessor Macro

As you can see, and as I have stated, there is no native string support in HLSL. You must use an array of uints. This because a big problem when you want to define a string literal.

const uint helloStr[] =  {'H','e','l','l','o',',',' ','W','o','r','l','d','!',};

Gross!

So, I wrote my own custom TEXT() preprocessor macro. Before I compile the HLSL text, I use regular expressions in (hacky, ugly) C++ to find TEXT() and automatically expand the string to a uint array.

const char* shaderCodeText = "ORIGINAL HLSL SOURCE HERE";
std::string originalShaderCodeString(shaderCodeText);
std::regex regexTextPattern("TEXT\\(.*\"\\)");
auto words_begin = std::sregex_iterator(originalShaderCodeString.begin(), originalShaderCodeString.end(), regexTextPattern);
auto words_end = std::sregex_iterator();
std::sregex_iterator i = words_begin;
while (i != words_end)
{
std::smatch match = *i;
// remove TEXT(" from the start and ") from the end
std::string actualString = match.str().erase(0, 6);
actualString = actualString.erase(actualString.size() - 2);
std::string output = "{";
int charIndex = 0;
for (char c : actualString)
{
output += "'";
output += c;
output += "',";
charIndex++;
}
while (charIndex < 255)
{
output += "0,";
charIndex++;
}
output += "0}";
size_t pos = 0;
while ((pos = originalShaderCodeString.find(match.str(), pos)) != std::string::npos)
{
originalShaderCodeString.replace(pos, match.length(), output);
pos += output.length(); // Move past the newly inserted substring
}
i = std::sregex_iterator(originalShaderCodeString.begin(), originalShaderCodeString.end(), regexTextPattern);
}
shaderCodeText = originalShaderCodeString.c_str();

So the above line becomes this:

const uint helloStr[] = TEXT("Hello, World!");

Much better!

There are still major limitations, mainly due to C limitations, not HLSL.

  • You cannot re-initialize an array with a literal.
    • helloStr = TEXT("Changed string!"); \\ fails to compile
  • You cannot pass an array literal in as a function argument.
    • DrawText(TEXT("My string literal arg!")); \\ fails to compile

strcat and strcpy

To provide some more C-esque support for strings, I did add my own custom implementations of strcat and strcpy.

void strcat(in uint stringA[256], in uint stringB[256], inout uint outString[256])

void strcpy(inout uint destination[256], in uint source[256])



Here is the HLSL source code for the above screenshot:

uint printStr[] = TEXT("The quick brown fox jumped over the lazy dog!");
DrawText(gOutput, printStr, float2(0, 530), 0.75f);

uint tempStr[] = TEXT("itoa + cursor = ");
strcpy(printStr, tempStr);
float2 cursor = DrawText(gOutput, printStr, float2(0, 580), 2.0f, float4(1, 0, 0, 1));

itoa(1234567890, tempStr);
DrawText(gOutput, tempStr, cursor, 2.0f, float4(1, 0, 0, 1));

ftoa(3.14159f, printStr, 3);
const uint tempStr2[] = TEXT("ftoa + strcat = ");
uint concatStr[256];
strcat(tempStr2, printStr, concatStr);
DrawText(gOutput, concatStr, float2(0, 650), 3.0f, float4(1, 0, 1, 1));

Future Work

I now have a text renderer, fully implemented in HLSL, which can be used by any shader to output to any RWTexture, including the back-buffer. There are still many things I would like to add and improve.

  • I would like to optimize the number lines needed to draw the @ symbol in order to reduce the array size
    • It needs 48 lines and the next most expensive character & needs 33, so we could potentially save 3 int4s per character, which would be a reduction of 4,560 bytes.
  • I already have in scale, but I would love to add in full transform/rotation.
  • I have cursor advancement, but I have no concept of wrapping or line breaks, which would be nice for multiline text on the screen.
  • The given string is completely drawn on the current single GPU thread. I would like to utilize the GPU threading more, perhaps through work graphs. Ideally it would be 1 glyph per thread, or even 1 line per thread.
  • While I have seen decent performance on various GPUs, I have seen terrible performance on some, so I would like to optimize for all hardware, if possible. For the 3-line text image above I was seeing these (completely unscientific) frame times:
    • GeForce RTX 4090 = 3 ms
    • GeForce 970M = 25 ms
    • Intel HD Graphics 620 = 19,000 ms (ouch!)

Tuesday, December 31, 2024

2024 Is Already Gone!

Here I sit, less than one hour away from 2025 kicking off and I suddenly remembered that I haven't posted a blog post this past year. I can't break my 16 year streak!

The biggest accomplishment was I got my Tailwheel Endorsement. I'm hoping to someday own a tailwheel plane myself. That makes me think I should post about the places I fly to on here. That's not about development/programming, but if I'm honest, this blog isn't really about that anymore anyway.

I've done a bit of travelling across the states this year, such as North Carolina, Tennessee, Washington DC, and Oregon (where I got to see the prototype Van's RV-15).

I'm going to be starting a new job in January, but I'll talk more about that in the future.

Catch you all in the new year!

Sunday, December 31, 2023

A Look Back Over 2023

This is my longest blog post by far!

Time Flies Like an Arrow

It's been 21 months since I last posted on here, which is a ridiculous amount of time. But, importantly (to me at least), I still haven't missed a single year since I began this blog 16 years ago. Dang, that makes me feel old. So, what have I been busy doing for this period of time?

Work

A lot of my time, energy, and mental problem-solving have been spent toward work. I've been busy leading a team where we come up with new rendering tech for Killing Floor 3. I probably can't go into much detail without divulging trade secrets, but my team focuses on making state-of-the-art gore systems. I would love for us to be able to present our solution as a GDC talk, or something similar.

Pipeline

I've been continuing work on Pipeline off and on, but it's going much slower than I anticipated. I'd love to implement some cutting edge features like raytracing, more mesh shaders, indirect drawing, and virtual texturing (with sampler feedback), but I find myself getting distracted with serialization, UI interface, and pondering the best ways to make undo/redo systems. Looking over the new shader Work Graphs, I wonder when I'll actually catch up to the latest graphics features. I'd also love to tinker with the new DirectML features.

Lately, I've been focused on parsing the C++ DX12 header to auto-generate most of the boilerplate code for me automatically, but that is also taking far more effort than I originally anticipated. I find myself making very custom parsing and generation code to create the wrappers, which kind of defeats the original intention and purpose. Ideally, the auto-generation would create the XML serialization and ImGui methods with little to no specialized generation code. Should this instead auto-generate the starting point instead of the the final methods? Something for me to think about and consider.

Birthday

This year I turned 40. That number still seems very large to me and I don't feel anywhere close to what I thought that age would feel like.

What do they do with engineers who turn 40? They take them out back and shoot them. - Primer

To celebrate my birthday, I travelled to St. Croix in the US Virgin Islands. I had never been to the Caribbean before, and it was beautiful. My goal is to eventually visit the South Pacific and in some ways, the Caribbean islands could be considered a "poor man's" South Pacific. Okay, maybe not that poor of a man.

I'm a big fan of whiskey, so I decided to splurge for my 40th birthday. I bought a $400 bottle of Japanese whiskey. I figured a bottle that cost $10 per year of my life was worth it. Honestly, I would say it's not the best whiskey I've ever had, but it was quite tasty.

Pilot's License

I'm very proud to say that just weeks before my 40th birthday I took and passed my checkride to be able to get my Private Pilot's License! I actually began my pilot lessons back in 2008, which is 14 years ago. I stopped my lessons in 2010, thinking I would be taking a temporary hiatus, which ended up lasting over 11 years. You just never know how life will distract you and lead you in different directions.

I don't yet have my own plane and I'm not a member of a flying club or anything, so the only way I can fly currently is to rent a plane, which is quite expensive. I've been researching lots of different aircraft trying to figure out which one best fits my mission. I've been mainly considering the Cessna 170, Ran's S-21 Outbound, Van's RV-14 or RV-15 (not yet available), or Zenith CH-750 Super Duty. In fact, this past summer I went and visited the Zenith factory in Missouri. Who knows what I'll eventually end up with though. It will likely be a completely different and random plane that I can afford to buy and fly while I potentially build my own plane.

I would also love to continue to extend my license with a tailwheel endorsement and an instrument rating. 

Bucket List

Getting my pilot's license makes me think of my unofficial "bucket list" of things I want to accomplish in my life. I now have several published games with my name in the credits, which was one of my major life goals. Getting a pilot's license and my own plane is another.

What remaining goals to I have?

  • Be the author of a published fiction book

I've been working on a multi-book story for nearly 20 years at this point. I've written several drafts of the first book but I never got it to a state where I can even send it to a publisher.

  • Be the lead engineer/designer of my own published indie game

I've attempted to work with artists in my free-time at least twice now to make an indie game. The problem is that it's hard to keep myself and others motived on a project that we're all doing for free, with the hope of success later in the future. One of goals would be to try to use as little human and financial resources as possible. This raises the question of generative AI, the latest hot topic. I could likely utilize generative AI for art, animation, music, sound effects, text to speech, etc. These would all greatly help a single developer be able to make a full and complete game in a  reasonable amount of time.

Changing Note-Taking & Blogging

While I do have this dev blog, you'll notice that I don't update it very often anymore. This directly correlates to my general note-taking in life. I will write a bunch of notes in one place and then never gather them together into anything meaningful. And they often get lost over time and especially after job changes. (For example, I used to have tons of notes for Unity, but now that I haven't used Unity professionally for over 6 years, I have no idea where those notes went or how useful they still are.)

I've often admired friends and co-workers who keep copious, well-organized notes. Unsurprisingly, this made them extremely knowledgeable and also made them grow tremendously. I would like to apply these same aspects to myself.

I started using Tangent Notes, mainly for a way to organize my world-building for my book series I mentioned above. However, I soon found it very helpful for organizing all of my notes. I transitioned over to Obsidian, mainly due to Obsidian Sync, and I now have separate "vaults" for novel-writing, hobbies, work, and even blog posts.

Speaking of these blog posts. I think it's time that I transition away from Blogger/Blogspot. It's served we well over the past 16 years, but I now want something I have more direct control over. Ideally, I can write all of my posts in Markdown in Obsidian and export those as HTML to be hosted on GitHub (or my own server). I've already begun some work in this regard, but it's not ready for a full transition yet.

Closing

2023 was a big year for me with some big accomplishments and changes. I consider myself to be very lucky with my life and career and I hope that continues though 2024 and into the future.

Until next time...

Sunday, March 20, 2022

Announcing Pipeline!

In my last post, I mentioned that I was working on something big in my free-time and that more information would be coming around GDC.

Well, GDC begins tomorrow, so it's time for me to announce Pipeline.



Pipeline is a data-driven, interactive, runtime editor of the entire DirectX 12 pipeline. It merges together my love of low-level graphics rendering and tools.

It's still very early and nowhere near a full 1.0 release, but I figured it was usable enough to have folks start tinkering with it and give me feedback.

Check out sample projects here.

Read documentation here.

If you encounter any problems or want to request features, you can do that here.

I'm excited to continue to improve Pipeline by adding new features and making it a much more powerful tool for graphics developers to utilize.

Friday, December 31, 2021

A Look Back Over 2021

It's hard to believe that 2021 is already over and done with. Little did I know that when the global pandemic rolled-in in March 2020, it'd still be lingering about in December 2021.

A lot has changed for me over this past year.

Back in January, Vicarious Visions was moved under/merged with/taken over by Blizzard. Previously, they were under Activision, which itself was under the larger entity known as Activision Blizzard King. It all gets confusing. Not only did this transition mean that VV would no longer be able to work on Activision games (Tony Hawk, Crash Bandicoot, Call of Duty, etc), but it also meant that the entire studio (outside of a small handful of folks) would be entirely devoted to supporting Diablo.

From January to July, I was specifically working on the Tools team on Diablo IV. I won't get into the details here, but I'll just say that that position was less than ideal for me and the direction I wanted my career to take.

In July, I got married! The ceremony was on a beautiful day on top of a mountain in Vermont.

In July, I also left VV/Blizzard. I had been at VV for longer than any other place in my career, and it was hard to make the decision to leave. They allowed me to branch out, try new things, take risks, and grow my skills way more than anywhere else I ever worked. I will forever be thankful to them for that.

On my second-to-last day at VV, the news broke about the widespread gender discrimination at Blizzard, which I was saddened, but unfortunately not surprised, to hear about.

In August, I began working at Tripwire Interactive as their Lead Engine Programmer. My team helped finish up the raytracing update for Maneater that was released on the Xbox Series X and PlayStation 5 in November. We are now hard at work on an unannounced title.

Meanwhile, I've also been working on something big in my free-time. I'm not quite ready to fully unveil it to the world, but know that it combines my love of graphics and my background in tools. I've given myself a deadline of GDC to have something shareable, so look for more information in March 2022.

Hang in there everyone! Everything can, and will, get better.

Thursday, September 17, 2020

A Positive Note

2020 has been a crazy/scary/disappointing/frustrating year for many different reasons.

Thankfully, I've had at least one big great thing happen this year.

I was officially a graphics engineer on a game that some folks called "one of the best games ever made": Tony Hawk's Pro Skater 1+2.


It's crazy to think of where I started, back with those simple XNA projects. It just goes to show you that you never know where you might end up!

Here's to the rest of 2020 and the hope that there are many more happy moments! We need them.

Tuesday, December 31, 2019

Another Year

I started this devblog 11 years ago (wow!) to showcase the game development tech I had been working on in my free time.

Almost three years ago I (finally) got a job in an actual AAA game dev studio. In those three years I've been busy working on several games but I've also been very active on a private, internal devblog shared by the studio.

Unfortunately, that leaves little desire to write different devblogs in my freetime.  On a positive note, I'm scratching the devblog itch with something officially supported by my employer, which is great.

So the lack of posts here shouldn't been seen as me no longer working on any game dev projects.  In fact, quite the opposite is true.

I'm not going to commit to posting here more, because that frankly seems untenable. I hope to keep up the minimum of one per year, which I barely hit this year.

Onward to 2020!