Files
WickedEngine/WickedEngine/wiFont.cpp
T
Turánszki János f277a00596 GUI updates (#490)
2022-07-28 20:59:25 +02:00

716 lines
22 KiB
C++

#include "wiFont.h"
#include "wiRenderer.h"
#include "wiResourceManager.h"
#include "wiHelper.h"
#include "shaders/ShaderInterop_Font.h"
#include "wiBacklog.h"
#include "wiTextureHelper.h"
#include "wiRectPacker.h"
#include "wiSpinLock.h"
#include "wiPlatform.h"
#include "wiEventHandler.h"
#include "wiTimer.h"
#include "wiUnorderedMap.h"
#include "wiUnorderedSet.h"
#include "wiVector.h"
#include "Utility/arial.h"
#include "Utility/stb_truetype.h"
#include <fstream>
#include <mutex>
using namespace wi::graphics;
namespace wi::font
{
#define WHITESPACE_SIZE ((float(params.size) + params.spacingX) * 0.25f)
#define TAB_SIZE (WHITESPACE_SIZE * 4)
#define LINEBREAK_SIZE ((float(params.size) + params.spacingY))
namespace font_internal
{
static BlendState blendState;
static RasterizerState rasterizerState;
static DepthStencilState depthStencilState;
static DepthStencilState depthStencilState_depth_test;
static Shader vertexShader;
static Shader pixelShader;
static PipelineState PSO;
static PipelineState PSO_depth_test;
static thread_local wi::Canvas canvas;
static Texture texture;
struct FontStyle
{
std::string name;
wi::vector<uint8_t> fontBuffer; // only used if loaded from file, need to keep alive
stbtt_fontinfo fontInfo;
int ascent, descent, lineGap;
void Create(const std::string& newName, const uint8_t* data, size_t size)
{
name = newName;
int offset = stbtt_GetFontOffsetForIndex(data, 0);
if (!stbtt_InitFont(&fontInfo, data, offset))
{
wi::backlog::post("Failed to load font: " + name + " (file was unrecognized, it must be a .ttf file)");
}
stbtt_GetFontVMetrics(&fontInfo, &ascent, &descent, &lineGap);
}
void Create(const std::string& newName)
{
if (wi::helper::FileRead(newName, fontBuffer))
{
Create(newName, fontBuffer.data(), fontBuffer.size());
}
else
{
wi::backlog::post("Failed to load font: " + name + " (file could not be opened)");
}
}
};
static wi::vector<std::unique_ptr<FontStyle>> fontStyles;
struct Glyph
{
float x;
float y;
float width;
float height;
float tc_left;
float tc_right;
float tc_top;
float tc_bottom;
const FontStyle* fontStyle = nullptr;
};
static wi::unordered_map<int32_t, Glyph> glyph_lookup;
static wi::unordered_map<int32_t, wi::rectpacker::Rect> rect_lookup;
struct SDF
{
static constexpr int padding = 5;
static constexpr unsigned char onedge_value = 180;
static constexpr float pixel_dist_scale = float(onedge_value) / float(padding);
int width;
int height;
int xoff;
int yoff;
wi::vector<uint8_t> bitmap;
};
static wi::unordered_map<int32_t, SDF> sdf_lookup;
// pack glyph identifiers to a 32-bit hash:
// height: 10 bits (height supported: 0 - 1023)
// style: 6 bits (number of font styles supported: 0 - 63)
// code: 16 bits (character code range supported: 0 - 65535)
constexpr int32_t glyphhash(int code, int style, int height) { return ((code & 0xFFFF) << 16) | ((style & 0x3F) << 10) | (height & 0x3FF); }
constexpr int codefromhash(int64_t hash) { return int((hash >> 16) & 0xFFFF); }
constexpr int stylefromhash(int64_t hash) { return int((hash >> 10) & 0x3F); }
constexpr int heightfromhash(int64_t hash) { return int((hash >> 0) & 0x3FF); }
static wi::unordered_set<int32_t> pendingGlyphs;
static wi::SpinLock glyphLock;
struct ParseStatus
{
Cursor cursor;
uint32_t quadCount = 0;
size_t last_word_begin = 0;
bool start_new_word = false;
};
static thread_local wi::vector<FontVertex> vertexList;
ParseStatus ParseText(const wchar_t* text, size_t text_length, const Params& params)
{
ParseStatus status;
status.cursor = params.cursor;
vertexList.clear();
auto word_wrap = [&] {
status.start_new_word = true;
if (status.last_word_begin > 0 && params.h_wrap >= 0 && status.cursor.position.x >= params.h_wrap - 1)
{
// Word ended and wrap detected, push down last word by one line:
float word_offset = vertexList[status.last_word_begin].pos.x + WHITESPACE_SIZE;
for (size_t i = status.last_word_begin; i < status.quadCount * 4; ++i)
{
vertexList[i].pos.x -= word_offset;
vertexList[i].pos.y += LINEBREAK_SIZE;
}
status.cursor.position.x -= word_offset;
status.cursor.position.y += LINEBREAK_SIZE;
status.cursor.size.x = std::max(status.cursor.size.x, status.cursor.position.x);
status.cursor.size.y = std::max(status.cursor.size.y, status.cursor.position.y + LINEBREAK_SIZE);
}
};
status.cursor.size.y = status.cursor.position.y + LINEBREAK_SIZE;
for (size_t i = 0; i < text_length; ++i)
{
int code = (int)text[i];
const int32_t hash = glyphhash(code, params.style, params.size);
if (glyph_lookup.count(hash) == 0)
{
// glyph not packed yet, so add to pending list:
std::scoped_lock locker(glyphLock);
pendingGlyphs.insert(hash);
continue;
}
if (code == '\n')
{
word_wrap();
status.cursor.position.x = 0;
status.cursor.position.y += LINEBREAK_SIZE;
}
else if (code == ' ')
{
word_wrap();
status.cursor.position.x += WHITESPACE_SIZE;
}
else if (code == '\t')
{
word_wrap();
status.cursor.position.x += TAB_SIZE;
}
else
{
const Glyph& glyph = glyph_lookup.at(hash);
const float glyphWidth = glyph.width;
const float glyphHeight = glyph.height;
const float glyphOffsetX = glyph.x;
const float glyphOffsetY = glyph.y;
const float fontScale = stbtt_ScaleForPixelHeight(&glyph.fontStyle->fontInfo, (float)params.size);
const size_t vertexID = size_t(status.quadCount) * 4;
vertexList.resize(vertexID + 4);
status.quadCount++;
if (status.start_new_word)
{
status.last_word_begin = vertexID;
}
status.start_new_word = false;
const float left = status.cursor.position.x + glyphOffsetX;
const float right = left + glyphWidth;
const float top = status.cursor.position.y + glyphOffsetY;
const float bottom = top + glyphHeight;
vertexList[vertexID + 0].pos = float2(left, top);
vertexList[vertexID + 1].pos = float2(right, top);
vertexList[vertexID + 2].pos = float2(left, bottom);
vertexList[vertexID + 3].pos = float2(right, bottom);
vertexList[vertexID + 0].uv = float2(glyph.tc_left, glyph.tc_top);
vertexList[vertexID + 1].uv = float2(glyph.tc_right, glyph.tc_top);
vertexList[vertexID + 2].uv = float2(glyph.tc_left, glyph.tc_bottom);
vertexList[vertexID + 3].uv = float2(glyph.tc_right, glyph.tc_bottom);
int advance, lsb;
stbtt_GetCodepointHMetrics(&glyph.fontStyle->fontInfo, code, &advance, &lsb);
status.cursor.position.x += advance * fontScale;
status.cursor.position.x += params.spacingX;
if (text_length > 1 && i < text_length - 1 && text[i + 1])
{
int code_next = (int)text[i + 1];
int kern = stbtt_GetCodepointKernAdvance(&glyph.fontStyle->fontInfo, code, code_next);
status.cursor.position.x += kern * fontScale;
}
}
status.cursor.size.x = std::max(status.cursor.size.x, status.cursor.position.x);
status.cursor.size.y = std::max(status.cursor.size.y, status.cursor.position.y + LINEBREAK_SIZE);
}
word_wrap();
return status;
}
thread_local static std::string char_temp_buffer;
thread_local static std::wstring wchar_temp_buffer;
ParseStatus ParseText(const char* text, size_t text_length, const Params& params)
{
// the temp buffers are used to avoid allocations of string objects:
char_temp_buffer = text;
wi::helper::StringConvert(char_temp_buffer, wchar_temp_buffer);
return ParseText(wchar_temp_buffer.c_str(), wchar_temp_buffer.length(), params);
}
void CommitText(void* vertexList_GPU)
{
std::memcpy(vertexList_GPU, vertexList.data(), sizeof(FontVertex) * vertexList.size());
}
}
using namespace font_internal;
void LoadShaders()
{
wi::renderer::LoadShader(ShaderStage::VS, vertexShader, "fontVS.cso");
wi::renderer::LoadShader(ShaderStage::PS, pixelShader, "fontPS.cso");
PipelineStateDesc desc;
desc.vs = &vertexShader;
desc.ps = &pixelShader;
desc.bs = &blendState;
desc.dss = &depthStencilState;
desc.rs = &rasterizerState;
desc.pt = PrimitiveTopology::TRIANGLESTRIP;
wi::graphics::GetDevice()->CreatePipelineState(&desc, &PSO);
desc.dss = &depthStencilState_depth_test;
wi::graphics::GetDevice()->CreatePipelineState(&desc, &PSO_depth_test);
}
void Initialize()
{
wi::Timer timer;
// add default font if there is none yet:
if (fontStyles.empty())
{
AddFontStyle("arial", arial, sizeof(arial));
}
GraphicsDevice* device = wi::graphics::GetDevice();
RasterizerState rs;
rs.fill_mode = FillMode::SOLID;
rs.cull_mode = CullMode::NONE;
rs.front_counter_clockwise = true;
rs.depth_bias = 0;
rs.depth_bias_clamp = 0;
rs.slope_scaled_depth_bias = 0;
rs.depth_clip_enable = false;
rs.multisample_enable = false;
rs.antialiased_line_enable = false;
rasterizerState = rs;
BlendState bd;
bd.render_target[0].blend_enable = true;
bd.render_target[0].src_blend = Blend::SRC_ALPHA;
bd.render_target[0].dest_blend = Blend::INV_SRC_ALPHA;
bd.render_target[0].blend_op = BlendOp::ADD;
bd.render_target[0].src_blend_alpha = Blend::ONE;
bd.render_target[0].dest_blend_alpha = Blend::INV_SRC_ALPHA;
bd.render_target[0].blend_op_alpha = BlendOp::ADD;
bd.render_target[0].render_target_write_mask = ColorWrite::ENABLE_ALL;
bd.independent_blend_enable = false;
blendState = bd;
DepthStencilState dsd;
dsd.depth_enable = false;
dsd.stencil_enable = false;
depthStencilState = dsd;
dsd.depth_enable = true;
dsd.depth_write_mask = DepthWriteMask::ZERO;
dsd.depth_func = ComparisonFunc::GREATER;
depthStencilState_depth_test = dsd;
static wi::eventhandler::Handle handle1 = wi::eventhandler::Subscribe(wi::eventhandler::EVENT_RELOAD_SHADERS, [](uint64_t userdata) { LoadShaders(); });
LoadShaders();
wi::backlog::post("wi::font Initialized (" + std::to_string((int)std::round(timer.elapsed())) + " ms)");
}
void UpdateAtlas()
{
std::scoped_lock locker(glyphLock);
// If there are pending glyphs, render them and repack the atlas:
if (!pendingGlyphs.empty())
{
for (int32_t hash : pendingGlyphs)
{
const int code = codefromhash(hash);
int style = stylefromhash(hash);
const float height = (float)heightfromhash(hash);
FontStyle* fontStyle = fontStyles[style].get();
int glyphIndex = stbtt_FindGlyphIndex(&fontStyle->fontInfo, code);
if (glyphIndex == 0)
{
// Try fallback to an other font style that has this character:
style = 0;
while (glyphIndex == 0 && style < fontStyles.size())
{
fontStyle = fontStyles[style].get();
glyphIndex = stbtt_FindGlyphIndex(&fontStyle->fontInfo, code);
style++;
}
}
float fontScaling = stbtt_ScaleForPixelHeight(&fontStyle->fontInfo, height);
SDF& sdf = sdf_lookup[hash];
sdf.width = 0;
sdf.height = 0;
sdf.xoff = 0;
sdf.yoff = 0;
unsigned char* bitmap = stbtt_GetGlyphSDF(&fontStyle->fontInfo, fontScaling, glyphIndex, sdf.padding, sdf.onedge_value, sdf.pixel_dist_scale, &sdf.width, &sdf.height, &sdf.xoff, &sdf.yoff);
sdf.bitmap.resize(sdf.width * sdf.height);
std::memcpy(sdf.bitmap.data(), bitmap, sdf.bitmap.size());
stbtt_FreeSDF(bitmap, nullptr);
wi::rectpacker::Rect rect = {};
rect.w = sdf.width;
rect.h = sdf.height;
rect.id = hash;
rect_lookup[hash] = rect;
Glyph& glyph = glyph_lookup[hash];
glyph.x = float(sdf.xoff);
glyph.y = float(sdf.yoff) + float(fontStyle->ascent) * fontScaling;
glyph.width = float(sdf.width);
glyph.height = float(sdf.height);
glyph.fontStyle = fontStyle;
}
pendingGlyphs.clear();
// Setup packer, this will allocate memory if needed:
static thread_local wi::rectpacker::State packer;
packer.clear();
for (auto& it : rect_lookup)
{
packer.add_rect(it.second);
}
// Perform packing and process the result if successful:
if (packer.pack(4096))
{
// Retrieve texture atlas dimensions:
const int bitmapWidth = packer.width;
const int bitmapHeight = packer.height;
const float inv_width = 1.0f / bitmapWidth;
const float inv_height = 1.0f / bitmapHeight;
// Create the CPU-side texture atlas and fill with transparency (0):
wi::vector<uint8_t> bitmap(size_t(bitmapWidth) * size_t(bitmapHeight));
std::fill(bitmap.begin(), bitmap.end(), 0);
// Iterate all packed glyph rectangles:
for (auto& rect : packer.rects)
{
const int32_t hash = rect.id;
const wchar_t code = codefromhash(hash);
const int style = stylefromhash(hash);
const float height = (float)heightfromhash(hash);
Glyph& glyph = glyph_lookup[hash];
SDF& sdf = sdf_lookup[hash];
for (int row = 0; row < sdf.height; ++row)
{
uint8_t* dst = bitmap.data() + rect.x + (rect.y + row) * bitmapWidth;
uint8_t* src = sdf.bitmap.data() + row * sdf.width;
std::memcpy(dst, src, sdf.width);
}
// Compute texture coordinates for the glyph:
glyph.tc_left = float(rect.x);
glyph.tc_right = glyph.tc_left + float(rect.w);
glyph.tc_top = float(rect.y);
glyph.tc_bottom = glyph.tc_top + float(rect.h);
glyph.tc_left *= inv_width;
glyph.tc_right *= inv_width;
glyph.tc_top *= inv_height;
glyph.tc_bottom *= inv_height;
}
// Upload the CPU-side texture atlas bitmap to the GPU:
wi::texturehelper::CreateTexture(texture, bitmap.data(), bitmapWidth, bitmapHeight, Format::R8_UNORM);
}
else
{
assert(0); // rect packing failure
}
}
}
const Texture* GetAtlas()
{
return &texture;
}
int AddFontStyle(const std::string& fontName)
{
for (size_t i = 0; i < fontStyles.size(); i++)
{
const FontStyle& fontStyle = *fontStyles[i];
if (!fontStyle.name.compare(fontName))
{
return int(i);
}
}
fontStyles.push_back(std::make_unique<FontStyle>());
fontStyles.back()->Create(fontName);
return int(fontStyles.size() - 1);
}
int AddFontStyle(const std::string& fontName, const uint8_t* data, size_t size)
{
for (size_t i = 0; i < fontStyles.size(); i++)
{
const FontStyle& fontStyle = *fontStyles[i];
if (!fontStyle.name.compare(fontName))
{
return int(i);
}
}
fontStyles.push_back(std::make_unique<FontStyle>());
fontStyles.back()->Create(fontName, data, size);
return int(fontStyles.size() - 1);
}
template<typename T>
Cursor Draw_internal(const T* text, size_t text_length, const Params& params, CommandList cmd)
{
if (text_length <= 0)
{
return Cursor();
}
ParseStatus status = ParseText(text, text_length, params);
if (status.quadCount > 0)
{
GraphicsDevice* device = wi::graphics::GetDevice();
GraphicsDevice::GPUAllocation mem = device->AllocateGPU(sizeof(FontVertex) * status.quadCount * 4, cmd);
if (!mem.IsValid())
{
return status.cursor;
}
CommitText(mem.data);
FontConstants font = {};
font.buffer_index = device->GetDescriptorIndex(&mem.buffer, SubresourceType::SRV);
font.buffer_offset = (uint32_t)mem.offset;
font.texture_index = device->GetDescriptorIndex(&texture, SubresourceType::SRV);
if (font.buffer_index < 0 || font.texture_index < 0)
{
return status.cursor;
}
device->EventBegin("Font", cmd);
if (params.isDepthTestEnabled())
{
device->BindPipelineState(&PSO_depth_test, cmd);
}
else
{
device->BindPipelineState(&PSO, cmd);
}
font.flags = 0;
if (params.isHDR10OutputMappingEnabled())
{
font.flags |= FONT_FLAG_OUTPUT_COLOR_SPACE_HDR10_ST2084;
}
if (params.isLinearOutputMappingEnabled())
{
font.flags |= FONT_FLAG_OUTPUT_COLOR_SPACE_LINEAR;
font.hdr_scaling = params.hdr_scaling;
}
XMFLOAT3 offset = XMFLOAT3(0, 0, 0);
float vertical_flip = params.customProjection == nullptr ? 1.0f : -1.0f;
if (params.h_align == WIFALIGN_CENTER)
offset.x -= status.cursor.size.x / 2;
else if (params.h_align == WIFALIGN_RIGHT)
offset.x -= status.cursor.size.x;
if (params.v_align == WIFALIGN_CENTER)
offset.y -= status.cursor.size.y / 2 * vertical_flip;
else if (params.v_align == WIFALIGN_BOTTOM)
offset.y -= status.cursor.size.y * vertical_flip;
XMMATRIX M = XMMatrixTranslation(offset.x, offset.y, offset.z);
M = M * XMMatrixScaling(params.scaling, params.scaling, params.scaling);
M = M * XMMatrixRotationZ(params.rotation);
if (params.customRotation != nullptr)
{
M = M * (*params.customRotation);
}
M = M * XMMatrixTranslation(params.position.x, params.position.y, params.position.z);
if (params.customProjection != nullptr)
{
M = XMMatrixScaling(1, -1, 1) * M; // reason: screen projection is Y down (like UV-space) and that is the common case for image rendering. But custom projections will use the "world space"
M = M * (*params.customProjection);
}
else
{
// Asserts will check that a proper canvas was set for this cmd with wi::image::SetCanvas()
// The canvas must be set to have dpi aware rendering
assert(canvas.width > 0);
assert(canvas.height > 0);
assert(canvas.dpi > 0);
M = M * canvas.GetProjection();
}
if (params.shadowColor.getA() > 0)
{
// font shadow render:
XMStoreFloat4x4(&font.transform, XMMatrixTranslation(params.shadow_offset_x, params.shadow_offset_y, 0) * M);
font.color = params.shadowColor.rgba;
font.sdf_threshold_top = wi::math::Lerp(float(SDF::onedge_value) / 255.0f, 0, std::max(0.0f, params.shadow_bolden));
font.sdf_threshold_bottom = wi::math::Lerp(font.sdf_threshold_top, 0, std::max(0.0f, params.shadow_softness));
device->BindDynamicConstantBuffer(font, CBSLOT_FONT, cmd);
device->DrawInstanced(4, status.quadCount, 0, 0, cmd);
}
// font base render:
XMStoreFloat4x4(&font.transform, M);
font.color = params.color.rgba;
font.sdf_threshold_top = wi::math::Lerp(float(SDF::onedge_value) / 255.0f, 0, std::max(0.0f, params.bolden));
font.sdf_threshold_bottom = wi::math::Lerp(font.sdf_threshold_top, 0, std::max(0.0f, params.softness));
device->BindDynamicConstantBuffer(font, CBSLOT_FONT, cmd);
device->DrawInstanced(4, status.quadCount, 0, 0, cmd);
device->EventEnd(cmd);
}
return status.cursor;
}
void SetCanvas(const wi::Canvas& current_canvas)
{
canvas = current_canvas;
}
Cursor Draw(const char* text, size_t text_length, const Params& params, CommandList cmd)
{
return Draw_internal(text, text_length, params, cmd);
}
Cursor Draw(const wchar_t* text, size_t text_length, const Params& params, CommandList cmd)
{
return Draw_internal(text, text_length, params, cmd);
}
Cursor Draw(const char* text, const Params& params, CommandList cmd)
{
return Draw_internal(text, strlen(text), params, cmd);
}
Cursor Draw(const wchar_t* text, const Params& params, CommandList cmd)
{
return Draw_internal(text, wcslen(text), params, cmd);
}
Cursor Draw(const std::string& text, const Params& params, CommandList cmd)
{
return Draw_internal(text.c_str(), text.length(), params, cmd);
}
Cursor Draw(const std::wstring& text, const Params& params, CommandList cmd)
{
return Draw_internal(text.c_str(), text.length(), params, cmd);
}
XMFLOAT2 TextSize(const char* text, size_t text_length, const Params& params)
{
if (text_length == 0)
{
return XMFLOAT2(0, 0);
}
return ParseText(text, text_length, params).cursor.size;
}
XMFLOAT2 TextSize(const wchar_t* text, size_t text_length, const Params& params)
{
if (text_length == 0)
{
return XMFLOAT2(0, 0);
}
return ParseText(text, text_length, params).cursor.size;
}
XMFLOAT2 TextSize(const char* text, const Params& params)
{
size_t text_length = strlen(text);
if (text_length == 0)
{
return XMFLOAT2(0, 0);
}
return ParseText(text, text_length, params).cursor.size;
}
XMFLOAT2 TextSize(const wchar_t* text, const Params& params)
{
size_t text_length = wcslen(text);
if (text_length == 0)
{
return XMFLOAT2(0, 0);
}
return ParseText(text, text_length, params).cursor.size;
}
XMFLOAT2 TextSize(const std::string& text, const Params& params)
{
if (text.empty())
{
return XMFLOAT2(0, 0);
}
return ParseText(text.c_str(), text.length(), params).cursor.size;
}
XMFLOAT2 TextSize(const std::wstring& text, const Params& params)
{
if (text.empty())
{
return XMFLOAT2(0, 0);
}
return ParseText(text.c_str(), text.length(), params).cursor.size;
}
float TextWidth(const char* text, size_t text_length, const Params& params)
{
return TextSize(text, text_length, params).x;
}
float TextWidth(const wchar_t* text, size_t text_length, const Params& params)
{
return TextSize(text, text_length, params).x;
}
float TextWidth(const char* text, const Params& params)
{
return TextSize(text, params).x;
}
float TextWidth(const wchar_t* text, const Params& params)
{
return TextSize(text, params).x;
}
float TextWidth(const std::string& text, const Params& params)
{
return TextSize(text, params).x;
}
float TextWidth(const std::wstring& text, const Params& params)
{
return TextSize(text, params).x;
}
float TextHeight(const char* text, size_t text_length, const Params& params)
{
return TextSize(text, text_length, params).y;
}
float TextHeight(const wchar_t* text, size_t text_length, const Params& params)
{
return TextSize(text, text_length, params).y;
}
float TextHeight(const char* text, const Params& params)
{
return TextSize(text, params).y;
}
float TextHeight(const wchar_t* text, const Params& params)
{
return TextSize(text, params).y;
}
float TextHeight(const std::string& text, const Params& params)
{
return TextSize(text, params).y;
}
float TextHeight(const std::wstring& text, const Params& params)
{
return TextSize(text, params).y;
}
}