I'm trying to render text using SDL. Obviously SDL does not support rendering text by itself, so I went with this approach:
- load font file
- raster glyphs in the font to a bitmap
- pack all bitmaps in a large texture, forming a spritesheet of glyphs
- render text as a sequence of glyph-sprites: copy rectangles from the texture to the target
First steps are handled using FreeType library. It can generate bitmaps for many kinds of fonts and provide a lot of extra info about the glyphs. FreeType-generated bitmaps are (by default) alpha channel only. For every glyph I basically get a 2D array of A values in range 0 - 255. For simplicity reasons the MCVE below needs only SDL, I already embedded FreeType-generated bitmap in the source code.
Now, the question is: how should I manage the texture that consists of such bitmaps?
- What blending mode should I use?
- What modulation should I use?
- What should the texture be filled with? FreeType provides alpha channel only, SDL generally wants a texture of RGBA pixels. What values should I use for RGB?
- How do I draw text in specific color? I don't want to make a separate texture for each color.
- FreeType documentation says: For optimal rendering on a screen the bitmap should be used as an alpha channel in linear blending with gamma correction. SDL blending mode documentation doesn't list anything named linear blending so I used a custom one but I'm not sure if I got it right.
- I'm not sure if I got some of SDL calls right as some of them are poorly documented (I already know that locking with empty rectangles crashes on Direct3D), especially how to copy data using
SDL_LockTexture.
#include <string>
#include <stdexcept>
#include <SDL.h>
constexpr unsigned char pixels[] = {
0, 0, 0, 0, 0, 0, 0, 30, 33, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 169, 255, 155, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 83, 255, 255, 229, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 189, 233, 255, 255, 60, 0, 0, 0, 0, 0,
0, 0, 0, 0, 33, 254, 83, 250, 255, 148, 0, 0, 0, 0, 0,
0, 0, 0, 0, 129, 227, 2, 181, 255, 232, 3, 0, 0, 0, 0,
0, 0, 0, 2, 224, 138, 0, 94, 255, 255, 66, 0, 0, 0, 0,
0, 0, 0, 68, 255, 48, 0, 15, 248, 255, 153, 0, 0, 0, 0,
0, 0, 0, 166, 213, 0, 0, 0, 175, 255, 235, 4, 0, 0, 0,
0, 0, 16, 247, 122, 0, 0, 0, 88, 255, 255, 71, 0, 0, 0,
0, 0, 105, 255, 192, 171, 171, 171, 182, 255, 255, 159, 0, 0, 0,
0, 0, 203, 215, 123, 123, 123, 123, 123, 196, 255, 239, 6, 0, 0,
0, 44, 255, 108, 0, 0, 0, 0, 0, 75, 255, 255, 77, 0, 0,
0, 142, 252, 22, 0, 0, 0, 0, 0, 5, 238, 255, 164, 0, 0,
5, 234, 184, 0, 0, 0, 0, 0, 0, 0, 156, 255, 242, 8, 0,
81, 255, 95, 0, 0, 0, 0, 0, 0, 0, 68, 255, 255, 86, 0,
179, 249, 14, 0, 0, 0, 0, 0, 0, 0, 3, 245, 255, 195, 0
};
[[noreturn]] inline void throw_error(const char* desc, const char* sdl_err)
{
throw std::runtime_error(std::string(desc) + sdl_err);
}
void update_pixels(
SDL_Texture& texture,
const SDL_Rect& texture_rect,
const unsigned char* src_alpha,
int src_size_x,
int src_size_y)
{
void* pixels;
int pitch;
if (SDL_LockTexture(&texture, &texture_rect, &pixels, &pitch))
throw_error("could not lock texture: ", SDL_GetError());
auto pixel_buffer = reinterpret_cast<unsigned char*>(pixels);
for (int y = 0; y < src_size_y; ++y) {
for (int x = 0; x < src_size_x; ++x) {
// this assumes SDL_PIXELFORMAT_RGBA8888
unsigned char* const rgba = pixel_buffer + x * 4;
unsigned char& r = rgba[0];
unsigned char& g = rgba[1];
unsigned char& b = rgba[2];
unsigned char& a = rgba[3];
r = 0xff;
g = 0xff;
b = 0xff;
a = src_alpha[x];
}
src_alpha += src_size_y;
pixel_buffer += pitch;
}
SDL_UnlockTexture(&texture);
}
int main(int /* argc */, char* /* argv */[])
{
if (SDL_Init(SDL_INIT_VIDEO) < 0)
throw_error("could not init SDL: ", SDL_GetError());
SDL_Window* window = SDL_CreateWindow("Hello World",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
1024, 768,
SDL_WINDOW_RESIZABLE);
if (!window)
throw_error("could not create window: ", SDL_GetError());
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, 0);
if (!renderer)
throw_error("could not create renderer: ", SDL_GetError());
SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, 512, 512);
if (!texture)
throw_error("could not create texture: ", SDL_GetError());
SDL_SetTextureColorMod(texture, 255, 0, 0);
SDL_Rect src_rect;
src_rect.x = 0;
src_rect.y = 0;
src_rect.w = 15;
src_rect.h = 17;
update_pixels(*texture, src_rect, pixels, src_rect.w, src_rect.h);
/*
* FreeType documentation: For optimal rendering on a screen the bitmap should be used as
* an alpha channel in linear blending with gamma correction.
*
* The blending used is therefore:
* dstRGB = (srcRGB * srcA) + (dstRGB * (1 - srcA))
* dstA = (srcA * 0) + (dstA * 1) = dstA
*/
SDL_BlendMode blend_mode = SDL_ComposeCustomBlendMode(
SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, SDL_BLENDOPERATION_ADD,
SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_ONE, SDL_BLENDOPERATION_ADD);
if (SDL_SetTextureBlendMode(texture, blend_mode))
throw_error("could not set texture blending: ", SDL_GetError());
while (true) {
SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
SDL_RenderClear(renderer);
SDL_Rect dst_rect;
dst_rect.x = 100;
dst_rect.y = 100;
dst_rect.w = src_rect.w;
dst_rect.h = src_rect.h;
SDL_RenderCopy(renderer, texture, &src_rect, &dst_rect);
SDL_RenderPresent(renderer);
SDL_Delay(16);
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_KEYUP:
switch (event.key.keysym.sym) {
case SDLK_ESCAPE:
return 0;
}
break;
case SDL_QUIT:
return 0;
}
}
}
return 0;
}
Expected result: red letter "A" on yellow background.
Actual result: malformed red lines inside black square on yellow background. 
I suspect that lines are broken because there is a bug within pointer arithmetics inside update_pixels but I have no idea what's causing the black square.