#include "font.hpp" #include "asset-manager.hpp" #include "dynamic-array.hpp" #include "pk.h" #include "window.hpp" #include "static-plane.hpp" #include "vendor-stb-image-include.h" #include "game-settings.hpp" #include "ecs.hpp" #include "../embed/embedded-fonts.h" #include // TODO threading // suggestions: // easy: give ft a mutex and lock for critical work // ft->bindings.instance* counter and length make atomic const char *PKE_PROJECT_FONT_TITLE = "title: "; const char *PKE_PROJECT_FONT_TEXTURE_HANDLE = "texture_handle: "; const char *PKE_PROJECT_FONT_GLYPH_DETAILS_HANDLE = "glyph_details_handle: "; const char *PKE_PROJECT_FONT_MSDF_MIN_SCALE = "msdf_min_scale: "; const char *PKE_PROJECT_FONT_MSDF_PX_RANGE = "msdf_px_range: "; const char *PKE_PROJECT_FONT_SPACING_GEOMETRY_SCALE = "spacing_geometry_scale: "; const char *PKE_PROJECT_FONT_SPACING_EM_SIZE = "spacing_em_size: "; const char *PKE_PROJECT_FONT_SPACING_ASCENDER_Y = "spacing_ascender_y: "; const char *PKE_PROJECT_FONT_SPACING_DESCENDER_Y = "spacing_descender_y: "; const char *PKE_PROJECT_FONT_SPACING_LINE_HEIGHT = "spacing_line_height: "; const char *PKE_PROJECT_FONT_SPACING_UNDERLINE_Y = "spacing_underline_y: "; const char *PKE_PROJECT_FONT_SPACING_UNDERLINE_THICKNESS = "spacing_underline_thickness: "; TypeSafeInt_B(FontTypeIndex); TypeSafeInt_B(FontRenderIndex); TypeSafeInt_B(FONT_GLYPH_CHAR_FLAG); TypeSafeInt_B(FONT_RENDER_SURFACE_AREA_TYPE_FLAG); struct FontTypeData { FontType *arr_ft; FontTypeIndex n_ft{0}; FontTypeIndex h_ft{0}; } ftd; struct FontInstanceBufferItem { glm::mat4 pos_scale; glm::vec4 fg_color; glm::vec4 bg_color; glm::vec2 sprite_region_min; glm::vec2 sprite_region_max; glm::vec2 bounding_region_min; glm::vec2 bounding_region_max; float width; float padding[3]; }; // BucketContainer bktFont; uint32_t utf8_to_unicode(const char* str, uint32_t &out) { uint32_t i = 0; out = 0; // Check the leading byte to determine the number of bytes in the character if ((str[i] & 0x80) == 0) { // 1 byte character (ASCII) out = str[i]; i = 1; } else if ((str[i] & 0xE0) == 0xC0) { // 2 byte character out = str[i] & 0x1F; out <<= 6; out |= (str[i + 1] & 0x3F); i = 2; } else if ((str[i] & 0xF0) == 0xE0) { // 3 byte character out = str[i] & 0x0F; out <<= 6; out |= (str[i + 1] & 0x3F); out <<= 6; out |= (str[i + 2] & 0x3F); i = 3; } else if ((str[i] & 0xF8) == 0xF0) { // 4 byte character out = str[i] & 0x07; out <<= 6; out |= (str[i + 1] & 0x3F); out <<= 6; out |= (str[i + 2] & 0x3F); out <<= 6; out |= (str[i + 3] & 0x3F); i = 4; } else { // Invalid UTF-8 byte sequence out = 0; return 0; } return i; } float FontType_Inner_LookAheadWordLength(const FontType *const ft, const FontRender *const fr, uint32_t index, float font_glyph_spacing) { uint32_t i; float ret = 0; FontGlyphChar *fgc; for (i = index; i < fr->n_glyphs; ++i) { fgc = &ft->glyphs[fr->glyph_indices[i]]; if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_WHITESPACE) == true) { break; } ret += fgc->advance * font_glyph_spacing; } return ret; } void FontType_Inner_CalcTransforms(const FontType *ft, FontRender *fr, FontInstanceBufferItem *ptr_dst) { assert(ft != nullptr); assert(fr != nullptr); assert(ptr_dst != nullptr); FontGlyphChar *fgc; FontInstanceBufferItem *buf_item; bool new_word = true; uint32_t i; float font_glyph_spacing; float cursor_head, line_index, line_height; glm::vec2 glyph_size; glm::vec3 translate; glm::vec3 scale; // TODO 2025-04-10 - JCB PERF // consider adding early-out when we exceed the bounding-box limits. // TODO 2025-02-18 - JCB // Trying to get pixel-perfect rendering with mannequin-7. // Everything is now sized and positioned correctly. // However, there is a ghost layer around the letter. font_glyph_spacing = ft->spacing.em_size * fr->settings.char_scale; cursor_head = 0; line_index = 0; line_height = font_glyph_spacing * ft->spacing.line_height * fr->settings.line_height_scale; for (i = 0; i < fr->n_glyphs; ++i) { translate = glm::vec3(0); scale = glm::vec3(1); fgc = &ft->glyphs[fr->glyph_indices[i]]; if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_NEW_LINE) == true) { // 10 line feed '\n' // 13 carriage return '\r' if ((fgc->unicode == 10 || fgc->unicode == 13) && i+1 < fr->n_glyphs) { if ((ft->glyphs[fr->glyph_indices[i+1]].unicode == 10 || ft->glyphs[fr->glyph_indices[i+1]].unicode == 13) && ft->glyphs[fr->glyph_indices[i+1]].unicode != fgc->unicode) { i += 1;// burn /r/n and /n/r } } line_index += 1; cursor_head = 0; continue; } if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_WHITESPACE) == true) { // x = 2 // a = 4 // 6 = 2 + 4 cursor_head += fgc->advance * font_glyph_spacing; if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_ALIGN_ADVANCE) == true) { // x2 = x - (a - (x % a)) // x2 = 6 - (4 - (6 & 4)) // x2 = 6 - (4 - 2) // x2 = 6 - (2) // x2 = 4 cursor_head -= (ft->spacing.em_size * font_glyph_spacing) - fmod(cursor_head, (ft->spacing.em_size * font_glyph_spacing)); } new_word = true; continue; } if (new_word == true) { float word_width = FontType_Inner_LookAheadWordLength(ft, fr, i, font_glyph_spacing); if (cursor_head + word_width > fr->settings.surface_area_size.x) { line_index += 1; cursor_head = 0; } new_word = false; } // left, bottom, right, top // x, y, z, w glyph_size.x = fgc->plane_bounds.z - fgc->plane_bounds.x; glyph_size.x *= font_glyph_spacing; glyph_size.y = fgc->plane_bounds.w - fgc->plane_bounds.y; glyph_size.y *= font_glyph_spacing; // shrink the correctly sized box to screen size scale *= glm::vec3(glyph_size.x / (float)Extent.width, glyph_size.y / (float)Extent.height, 1.0); // move to appropriate position + char placement // ((val) - (width/2)) / (width/2) // box position translate.x = fr->settings.surface_area_pos.x; // current char position on x axis translate.x += cursor_head; // place the left line of the glyph at the cursor head translate.x += (glyph_size.x / 2.0); // shift the glyph by the defined offset translate.x += fgc->plane_bounds.x * font_glyph_spacing; translate.x -= (Extent.width / 2.0); translate.x /= (Extent.width / 2.0); // ((val) - (height/2)) / (height/2) // box position translate.y = fr->settings.surface_area_pos.y; // baseline - current line (+1 to not draw above the box) translate.y += ((line_index + 1) * line_height); // places the top line of the glyph on the baseline translate.y += (font_glyph_spacing / 2.0) + (glyph_size.y / 2.0); // move glyph to the height relative to the baseline (cursor) translate.y -= (fgc->plane_bounds.w * font_glyph_spacing); translate.y -= (Extent.height / 2.0); translate.y /= (Extent.height / 2.0); cursor_head += fgc->advance * font_glyph_spacing; buf_item = &ptr_dst[i]; buf_item->pos_scale = glm::translate(glm::mat4(1), translate); buf_item->pos_scale = glm::scale(buf_item->pos_scale, scale); buf_item->fg_color = glm::vec4(0.4, 0.9, 0.5, 0.8); buf_item->bg_color = glm::vec4(0.0, 0.0, 0.0, 0.0); buf_item->sprite_region_min = fgc->sprite_region_min; buf_item->sprite_region_max = fgc->sprite_region_max; buf_item->bounding_region_min.x = fr->settings.surface_area_pos.x; buf_item->bounding_region_min.y = fr->settings.surface_area_pos.y; buf_item->bounding_region_max.x = fr->settings.surface_area_pos.x + fr->settings.surface_area_size.x; buf_item->bounding_region_max.y = fr->settings.surface_area_pos.y + fr->settings.surface_area_size.y; buf_item->bounding_region_min.x -= (Extent.width / 2.0); buf_item->bounding_region_min.x /= (Extent.width / 2.0); buf_item->bounding_region_max.x -= (Extent.width / 2.0); buf_item->bounding_region_max.x /= (Extent.width / 2.0); buf_item->bounding_region_min.y -= (Extent.height / 2.0); buf_item->bounding_region_min.y /= (Extent.height / 2.0); buf_item->bounding_region_max.y -= (Extent.height / 2.0); buf_item->bounding_region_max.y /= (Extent.height / 2.0); buf_item->width = (fr->settings.char_scale / ft->msdf_settings.minimum_scale) * ft->msdf_settings.px_range; } } void FontType_Init() { ftd.h_ft = FontTypeIndex{0}; ftd.n_ft = FontTypeIndex{5}; ftd.arr_ft = pk_new(5); for (FontTypeIndex_T i = 0; i < 5; ++i) { FontType *ft = &ftd.arr_ft[i]; ft->glyphs = nullptr; ft->renders = nullptr; } AssetKey ak_img = "fnt_mquin_img\0\0"; AssetKey ak_glyphs = "fnt_mquin_gly\0\0"; AssetHandle ah_img = AM_Register_Static(ak_img, PKE_ASSET_TYPE_TEXTURE, embedded_fonts[0].data, embedded_fonts[0].size); AssetHandle ah_glyphs = AM_Register_Static(ak_glyphs, PKE_ASSET_TYPE_UNSET, embedded_fonts[1].data, embedded_fonts[1].size); FontTypeMSDFSettings msdf_s; msdf_s.minimum_scale = 144; msdf_s.px_range = 4; FontTypeSpacing spacing; spacing.geometry_scale = 0.0625; spacing.em_size = 1; spacing.ascender_y = 0.875; spacing.descender_y = -0.125; spacing.line_height = 1.125; spacing.underline_y = 0.0625; FontType_RegisterFont(cstring_to_pk_cstr("fnt_mquin_7y"), ah_img, ah_glyphs, &msdf_s, &spacing); } void FontType_Teardown() { FontTypeIndex i; for (i = FontTypeIndex{0}; i < ftd.h_ft; ++i) { FontType_Unload(i); } if (ftd.arr_ft != nullptr) pk_delete(ftd.arr_ft, (FontTypeIndex_T)ftd.n_ft); } // TODO - Memory Pressure // 2025-02-19 - JCB // Font buffers only grow and never shrink. // Consider checking if we can shrink buffers. void FontType_Tick(double delta) { (void)delta; VkResult vkResult; FontInstanceBufferItem *fibis = nullptr; size_t index; FontType *ft; FontRender *fr; for (FontTypeIndex_T i = 0; i < (FontTypeIndex_T)ftd.h_ft; ++i) { index = 0; ft = &ftd.arr_ft[i]; if (pkeSettings.rt.was_framebuffer_resized == false && ft->gr.should_update_instance_buffer == false) { continue; } if (ft->bindings.instance_counter == 0) continue; ft->gr.should_update_instance_buffer = false; // TODO specific bucket fibis = pk_new(ft->bindings.instance_counter); for (FontRenderIndex_T k = 0; k < (FontRenderIndex_T)ft->h_render; ++k) { fr = &ft->renders[k]; FontType_Inner_CalcTransforms(ft, fr, &fibis[index]); index += fr->n_glyphs; } PKVK_TmpBufferDetails tmpBufferDetails{}; PKVK_BeginBuffer(graphicsFamilyIndex, sizeof(FontInstanceBufferItem) * ft->bindings.instance_counter, tmpBufferDetails); { VkCommandBufferBeginInfo vkCommandBufferBeginInfo; vkCommandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; vkCommandBufferBeginInfo.pNext = nullptr; // TODO consider single-use? vkCommandBufferBeginInfo.flags = 0; vkCommandBufferBeginInfo.pInheritanceInfo = nullptr; vkResult = vkBeginCommandBuffer(tmpBufferDetails.cmdBuffer, &vkCommandBufferBeginInfo); assert(vkResult == VK_SUCCESS); memcpy(tmpBufferDetails.deviceData, fibis, sizeof(FontInstanceBufferItem) * ft->bindings.instance_counter); VkBufferCopy vk_buffer_copy{}; vk_buffer_copy.srcOffset = 0; vk_buffer_copy.dstOffset = 0; vk_buffer_copy.size = sizeof(FontInstanceBufferItem) * ft->bindings.instance_counter; vkCmdCopyBuffer(tmpBufferDetails.cmdBuffer, tmpBufferDetails.buffer, ft->bindings.bd_instance.buffer, 1, &vk_buffer_copy); vkResult = vkEndCommandBuffer(tmpBufferDetails.cmdBuffer); assert(vkResult == VK_SUCCESS); VkSubmitInfo submitInfo; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pNext = nullptr; submitInfo.waitSemaphoreCount = 0; submitInfo.pWaitSemaphores = nullptr; submitInfo.pWaitDstStageMask = nullptr; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &tmpBufferDetails.cmdBuffer; submitInfo.signalSemaphoreCount = 0; submitInfo.pSignalSemaphores = nullptr; vkResult = vkQueueSubmit(tmpBufferDetails.queue, 1, &submitInfo, nullptr); assert(vkResult == VK_SUCCESS); vkResult = vkQueueWaitIdle(tmpBufferDetails.queue); assert(vkResult == VK_SUCCESS); } PKVK_EndBuffer(tmpBufferDetails); pk_delete(fibis, ft->bindings.instance_counter); } } void FontType_Serialize(std::ostream &stream, FontType *ft) { FontTypeSpacing sp{}; FontTypeMSDFSettings msdf{}; memset(&sp, 0, sizeof(sp)); NULL_CHAR_ARR(handleStr, AssetKeyLength + 2); const Asset *txtr = AM_Get(ft->fontTextureAssetHandle); const Asset *glyphs = AM_Get(ft->glyphDetailsAssetHandle); stream << PKE_PROJECT_FONT_TITLE << ft->title.val << std::endl; snprintf(handleStr, AssetKeyLength + 1, "%s", txtr->key); stream << PKE_PROJECT_FONT_TEXTURE_HANDLE << handleStr << std::endl; snprintf(handleStr, AssetKeyLength + 1, "%s", glyphs->key); stream << PKE_PROJECT_FONT_GLYPH_DETAILS_HANDLE << handleStr << std::endl; if (ft->msdf_settings.minimum_scale != msdf.minimum_scale) { stream << PKE_PROJECT_FONT_MSDF_MIN_SCALE << ft->msdf_settings.minimum_scale << std::endl; } if (ft->msdf_settings.px_range != msdf.px_range) { stream << PKE_PROJECT_FONT_MSDF_PX_RANGE << ft->msdf_settings.px_range << std::endl; } if (ft->spacing.geometry_scale != sp.geometry_scale) { stream << PKE_PROJECT_FONT_SPACING_GEOMETRY_SCALE << ft->spacing.geometry_scale << std::endl; } if (ft->spacing.em_size != sp.em_size) { stream << PKE_PROJECT_FONT_SPACING_EM_SIZE << ft->spacing.em_size << std::endl; } if (ft->spacing.ascender_y != sp.ascender_y) { stream << PKE_PROJECT_FONT_SPACING_ASCENDER_Y << ft->spacing.ascender_y << std::endl; } if (ft->spacing.descender_y != sp.descender_y) { stream << PKE_PROJECT_FONT_SPACING_DESCENDER_Y << ft->spacing.descender_y << std::endl; } if (ft->spacing.line_height != sp.line_height) { stream << PKE_PROJECT_FONT_SPACING_LINE_HEIGHT << ft->spacing.line_height << std::endl; } if (ft->spacing.underline_y != sp.underline_y) { stream << PKE_PROJECT_FONT_SPACING_UNDERLINE_Y << ft->spacing.underline_y << std::endl; } if (ft->spacing.underline_thickness != sp.underline_thickness) { stream << PKE_PROJECT_FONT_SPACING_UNDERLINE_THICKNESS << ft->spacing.underline_thickness << std::endl; } AM_Release(ft->glyphDetailsAssetHandle); AM_Release(ft->fontTextureAssetHandle); } void FontType_Deserialize(std::istream &stream) { uint64_t i; NULL_CHAR_ARR(readLine, 128); FontTypeSpacing sp{}; FontTypeMSDFSettings msdf{}; pk_cstr title; AssetKey fontTextureKey; AssetKey glyphDetailsKey; memset(&sp, 0, sizeof(sp)); while (memset(readLine, 0, 128), stream.getline(readLine, 128)) { if (strcmp("", readLine) == 0) { FontType_RegisterFont(title, AM_GetHandle(fontTextureKey), AM_GetHandle(glyphDetailsKey), &msdf, &sp); return; } if (strstr(readLine, PKE_PROJECT_FONT_TITLE)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_TITLE); title.length = strlen(readLine + prefixLen); title.reserved = title.length + 1; title.val = pk_new(title.reserved); strncpy(const_cast(title.val), readLine + prefixLen, title.reserved); continue; } if (strstr(readLine, PKE_PROJECT_FONT_TEXTURE_HANDLE)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_TEXTURE_HANDLE); for (i = 0; i < AssetKeyLength; ++i) { fontTextureKey[i] = readLine[prefixLen + i]; } continue; } if (strstr(readLine, PKE_PROJECT_FONT_GLYPH_DETAILS_HANDLE)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_GLYPH_DETAILS_HANDLE); for (i = 0; i < AssetKeyLength; ++i) { glyphDetailsKey[i] = readLine[prefixLen + i]; } continue; } if (strstr(readLine, PKE_PROJECT_FONT_MSDF_MIN_SCALE)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_MSDF_MIN_SCALE); auto result = pk_stn(&msdf.minimum_scale, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } if (strstr(readLine, PKE_PROJECT_FONT_MSDF_PX_RANGE)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_MSDF_PX_RANGE); auto result = pk_stn(&msdf.px_range, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } if (strstr(readLine, PKE_PROJECT_FONT_SPACING_GEOMETRY_SCALE)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_SPACING_GEOMETRY_SCALE); auto result = pk_stn(&sp.geometry_scale, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } if (strstr(readLine, PKE_PROJECT_FONT_SPACING_EM_SIZE)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_SPACING_EM_SIZE); auto result = pk_stn(&sp.em_size, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } if (strstr(readLine, PKE_PROJECT_FONT_SPACING_ASCENDER_Y)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_SPACING_ASCENDER_Y); auto result = pk_stn(&sp.ascender_y, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } if (strstr(readLine, PKE_PROJECT_FONT_SPACING_DESCENDER_Y)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_SPACING_DESCENDER_Y); auto result = pk_stn(&sp.descender_y, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } if (strstr(readLine, PKE_PROJECT_FONT_SPACING_LINE_HEIGHT)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_SPACING_LINE_HEIGHT); auto result = pk_stn(&sp.line_height, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } if (strstr(readLine, PKE_PROJECT_FONT_SPACING_UNDERLINE_Y)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_SPACING_UNDERLINE_Y); auto result = pk_stn(&sp.underline_y, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } if (strstr(readLine, PKE_PROJECT_FONT_SPACING_UNDERLINE_THICKNESS)) { uint64_t prefixLen = strlen(PKE_PROJECT_FONT_SPACING_UNDERLINE_THICKNESS); auto result = pk_stn(&sp.underline_thickness, readLine + prefixLen, nullptr); assert(result == PK_STN_RES_SUCCESS); continue; } } } FontType* FontType_Get(FontTypeIndex idx) { assert(idx < ftd.h_ft); return &ftd.arr_ft[static_cast(idx)]; } FontType* FontType_GetFonts(FontTypeIndex &count) { count = ftd.h_ft; return ftd.arr_ft; } FontTypeIndex FontType_RegisterFont(pk_cstr title, AssetHandle fontTextureHandle, AssetHandle glyphsHandle, FontTypeMSDFSettings *msdf_settings, FontTypeSpacing *spacing) { VkResult vkResult; pk_arr arr_vert_mem_reqs; memset(&arr_vert_mem_reqs, 0, sizeof(pk_arr)); arr_vert_mem_reqs.alignment = alignof(VkMemoryRequirements); arr_vert_mem_reqs.stride = sizeof(VkMemoryRequirements); constexpr VkDeviceSize startingGlyphCount = 4; assert(fontTextureHandle != AssetHandle_MAX); assert(glyphsHandle != AssetHandle_MAX); assert(msdf_settings != nullptr); assert(spacing != nullptr); const Asset *fontTexture = AM_Get(fontTextureHandle); FontTypeIndex idx = ftd.h_ft; ftd.h_ft += FontTypeIndex{1}; if (ftd.h_ft >= ftd.n_ft) { ftd.n_ft += FontTypeIndex{5}; FontType *arr = pk_new((FontTypeIndex_T)ftd.n_ft); if (idx > FontTypeIndex{0}) { memcpy(arr, ftd.arr_ft, sizeof(FontType) * (FontTypeIndex_T)idx); } if (ftd.arr_ft != nullptr && ftd.arr_ft != CAFE_BABE(FontType)) pk_delete(ftd.arr_ft, (FontTypeIndex_T)idx); ftd.arr_ft = arr; } FontTypeIndex_T idx_t = (FontTypeIndex_T)idx; FontType *ft = &ftd.arr_ft[idx_t]; ft->title = title; ft->fontTextureAssetHandle = fontTextureHandle; ft->glyphDetailsAssetHandle = glyphsHandle; ft->renders = pk_new(startingGlyphCount); ft->h_render = FontRenderIndex{0}; ft->n_render = FontRenderIndex{startingGlyphCount}; ft->msdf_settings = *msdf_settings; ft->spacing = *spacing; const Asset *glyphs = AM_Get(glyphsHandle); ft->n_glyphs = glyphs->size / sizeof(FontGlyphChar); ft->glyphs = (FontGlyphChar*)glyphs->ptr; int txtr_x, txtr_y, txtr_chan; stbi_uc *txtr_bytes = stbi_load_from_memory((unsigned char*)fontTexture->ptr, fontTexture->size, &txtr_x, &txtr_y, &txtr_chan, 4); assert(txtr_bytes != nullptr); assert(txtr_chan == 4); uint32_t imageSizeBytes = txtr_x * txtr_y * txtr_chan; ft->atlas_size.x = (float)txtr_x; ft->atlas_size.y = (float)txtr_y; ECS_CreateEntity(ft); if (window != NULL) { // buffers glm::vec2 atlas_size_buffer[4]; for (int i = 0; i < 4; ++i) { atlas_size_buffer[i] = ft->atlas_size; } pkvk_buffer_create_data create_data{}; create_data.buffer_byte_length[0] = sizeof(pkeIntrinsicsPlane.vert[0]) * 4; create_data.buffer_byte_length[1] = sizeof(pkeIntrinsicsPlane.uv[0]) * 4; create_data.buffer_byte_length[2] = sizeof(atlas_size_buffer[0]) * 4; create_data.buffer_byte_length[3] = sizeof(pkeIntrinsicsPlane.index[0]) * 6; create_data.buffer_byte_length[4] = sizeof(FontInstanceBufferItem); create_data.src_data[0] = &pkeIntrinsicsPlane.vert; create_data.src_data[1] = &pkeIntrinsicsPlane.uv; create_data.src_data[2] = &atlas_size_buffer; create_data.src_data[3] = &pkeIntrinsicsPlane.index; create_data.src_data[4] = nullptr; create_data.n_buffers = 5; create_data.index_index = 3; create_data.index_instance = 4; pkvk_buffer_create_data_out out{}; pkvk_buffer_create(&create_data, &out); ft->bindings.bd_vertex.buffer = out.buffers[0]; ft->bindings.bd_vertex.firstBinding = 0; ft->bindings.bd_vertex.bindingCount = 1; ft->bindings.bd_uv.buffer = out.buffers[1]; ft->bindings.bd_uv.firstBinding = 1; ft->bindings.bd_uv.bindingCount = 1; ft->bindings.bd_atlas_size.buffer = out.buffers[2]; ft->bindings.bd_atlas_size.firstBinding = 2; ft->bindings.bd_atlas_size.bindingCount = 1; ft->bindings.bd_index.buffer = out.buffers[3]; ft->bindings.bd_index.firstBinding = 0; ft->bindings.bd_index.bindingCount = 1; ft->bindings.index_count = 6; ft->bindings.bd_instance.buffer = out.buffers[4]; ft->bindings.bd_instance.firstBinding = 3; ft->bindings.bd_instance.bindingCount = 1; ft->gr.deviceMemoryVert = out.device_memory_vertex; ft->gr.deviceMemoryInst = out.device_memory_instance; ft->bindings.instance_buffer_max_count = out.memory_requirements_instance.size / sizeof(FontInstanceBufferItem); // texture pkvk_texture_upload_data txtr_data{}; txtr_data.bytes = txtr_bytes; txtr_data.n_bytes = imageSizeBytes; txtr_data.width = (uint32_t)txtr_x; txtr_data.height = (uint32_t)txtr_y; pkvk_texture_upload_data_out txtr_out{}; pkvk_texture_upload(&txtr_data, &txtr_out); ft->gr.textureImage = txtr_out.image; ft->gr.textureImageView = txtr_out.image_view; ft->gr.deviceMemoryTexture = txtr_out.device_memory; free(txtr_bytes); /* * Vulkan descriptor sets */ VkDescriptorPoolSize descriptorPoolSizes[1]; descriptorPoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; descriptorPoolSizes[0].descriptorCount = (uint32_t)1; VkDescriptorPoolCreateInfo vkDescriptorPoolCreateInfo; vkDescriptorPoolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; vkDescriptorPoolCreateInfo.pNext = nullptr; vkDescriptorPoolCreateInfo.flags = 0; vkDescriptorPoolCreateInfo.maxSets = (uint32_t)1; vkDescriptorPoolCreateInfo.poolSizeCount = (uint32_t)1; vkDescriptorPoolCreateInfo.pPoolSizes = descriptorPoolSizes; // consider making me a global pool vkResult = vkCreateDescriptorPool(vkDevice, &vkDescriptorPoolCreateInfo, vkAllocator, &ft->gr.vkDescriptorPool); assert(vkResult == VK_SUCCESS); VkDescriptorSetAllocateInfo vkDescriptorSetAllocateInfo; vkDescriptorSetAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; vkDescriptorSetAllocateInfo.pNext = nullptr; vkDescriptorSetAllocateInfo.descriptorPool = ft->gr.vkDescriptorPool; vkDescriptorSetAllocateInfo.descriptorSetCount = (uint32_t)1; vkDescriptorSetAllocateInfo.pSetLayouts = &pkePipelines.descr_layouts.named.txtr; vkResult = vkAllocateDescriptorSets(vkDevice, &vkDescriptorSetAllocateInfo, &ft->gr.vkDescriptorSet); assert(vkResult == VK_SUCCESS); VkDescriptorImageInfo textureDescriptorInfo; textureDescriptorInfo.sampler = global_sampler; textureDescriptorInfo.imageView = ft->gr.textureImageView; textureDescriptorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkWriteDescriptorSet writeDescriptorSet; writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writeDescriptorSet.pNext = nullptr; writeDescriptorSet.dstSet = ft->gr.vkDescriptorSet; writeDescriptorSet.dstBinding = (uint32_t)0; writeDescriptorSet.dstArrayElement = (uint32_t)0; writeDescriptorSet.descriptorCount = (uint32_t)1; writeDescriptorSet.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writeDescriptorSet.pImageInfo = &textureDescriptorInfo; writeDescriptorSet.pBufferInfo = nullptr; writeDescriptorSet.pTexelBufferView = nullptr; vkUpdateDescriptorSets(vkDevice, (uint32_t)1, &writeDescriptorSet, 0, nullptr); } else { free(txtr_bytes); } AM_Release(fontTextureHandle); AM_Release(glyphsHandle); return idx; } void FontType_Unload(FontTypeIndex idx) { FontType *ft = &ftd.arr_ft[(FontTypeIndex_T)idx]; // TODO specific bucket if (ft->renders != nullptr) { for (FontRenderIndex_T i = 0; i < (FontRenderIndex_T)ft->h_render; ++i) { if (ft->renders[i].glyph_indices != nullptr) { pk_delete(ft->renders[i].glyph_indices, ft->renders[i].n_glyphs); } } pk_delete(ft->renders, (FontTypeIndex_T)ft->n_render); } if (ft->gr.vkDescriptorSet != VK_NULL_HANDLE && ft->gr.vkDescriptorPool != VK_NULL_HANDLE) { // 2023-09-27 - JCB (copied from entities.cpp) // We are not setting the pool flag for allowing freeing descriptor sets // so all we need to do is destroy the pool // If we switch to a global pool, we will need to free here, and // destroy the pool outside of this loop vkDestroyDescriptorPool(vkDevice, ft->gr.vkDescriptorPool, vkAllocator); ft->gr.vkDescriptorSet = VK_NULL_HANDLE; ft->gr.vkDescriptorPool = VK_NULL_HANDLE; } if (ft->bindings.bd_vertex.buffer != VK_NULL_HANDLE) vkDestroyBuffer(vkDevice, ft->bindings.bd_vertex.buffer, vkAllocator); ft->bindings.bd_vertex.buffer = VK_NULL_HANDLE; ft->bindings.bd_vertex.firstBinding = 0; ft->bindings.bd_vertex.bindingCount = 0; ft->bindings.bd_vertex.offsets[0] = 0; if (ft->bindings.bd_uv.buffer != VK_NULL_HANDLE) vkDestroyBuffer(vkDevice, ft->bindings.bd_uv.buffer, vkAllocator); ft->bindings.bd_uv.buffer = VK_NULL_HANDLE; ft->bindings.bd_uv.firstBinding = 0; ft->bindings.bd_uv.bindingCount = 0; ft->bindings.bd_uv.offsets[0] = 0; if (ft->bindings.bd_atlas_size.buffer != VK_NULL_HANDLE) vkDestroyBuffer(vkDevice, ft->bindings.bd_atlas_size.buffer, vkAllocator); ft->bindings.bd_atlas_size.buffer = VK_NULL_HANDLE; ft->bindings.bd_atlas_size.firstBinding = 0; ft->bindings.bd_atlas_size.bindingCount = 0; ft->bindings.bd_atlas_size.offsets[0] = 0; if (ft->bindings.bd_index.buffer != VK_NULL_HANDLE) vkDestroyBuffer(vkDevice, ft->bindings.bd_index.buffer, vkAllocator); ft->bindings.bd_index.buffer = VK_NULL_HANDLE; ft->bindings.bd_index.bindingCount = 0; ft->bindings.bd_index.offsets[0] = 0; ft->bindings.index_count = 0; if (ft->bindings.bd_instance.buffer != VK_NULL_HANDLE) vkDestroyBuffer(vkDevice, ft->bindings.bd_instance.buffer, vkAllocator); ft->bindings.bd_instance.buffer = VK_NULL_HANDLE; ft->bindings.bd_instance.firstBinding = 0; ft->bindings.bd_instance.bindingCount = 0; ft->bindings.instance_counter = 0; ft->bindings.instance_buffer_max_count = 0; ft->bindings.bd_instance.offsets[0] = 0; if (ft->gr.textureImageView != VK_NULL_HANDLE) vkDestroyImageView(vkDevice, ft->gr.textureImageView, vkAllocator); ft->gr.textureImageView = VK_NULL_HANDLE; if (ft->gr.textureImage != VK_NULL_HANDLE) vkDestroyImage(vkDevice, ft->gr.textureImage, vkAllocator); ft->gr.textureImage = VK_NULL_HANDLE; if (ft->gr.deviceMemoryInst != VK_NULL_HANDLE) vkFreeMemory(vkDevice, ft->gr.deviceMemoryInst, vkAllocator); ft->gr.deviceMemoryInst = VK_NULL_HANDLE; if (ft->gr.deviceMemoryVert != VK_NULL_HANDLE) vkFreeMemory(vkDevice, ft->gr.deviceMemoryVert, vkAllocator); ft->gr.deviceMemoryVert = VK_NULL_HANDLE; if (ft->gr.deviceMemoryTexture != VK_NULL_HANDLE) vkFreeMemory(vkDevice, ft->gr.deviceMemoryTexture, vkAllocator); ft->gr.deviceMemoryTexture = VK_NULL_HANDLE; if (ft->title.reserved != 0 && ft->title.val != nullptr) { pk_delete(ft->title.val, ft->title.reserved); ft->title.val = nullptr; ft->title.length = 0; } ECS_MarkForRemoval(ft); } // TODO perf? // 2025-02-19 - JCB // We only create a new buffer if it is needed. // However, the new buffer is exactly the length it needs to be and no greater. // Consider using a heuristic to allocate a buffer larger than needed. // At the time of writing, the only way to do this is to un-register text. FontRenderHandle FontType_AddStringRender(FontTypeIndex idx_ft, const pk_cstr &&str, FontRenderSettings *settings, Entity_Base *parent, pk_uuid uuid) { assert(settings != nullptr); PKVK_TmpBufferDetails tmpBufferDetails{}; VkDeviceSize byteCount; VkResult vkResult; FontType *ft = &ftd.arr_ft[(FontTypeIndex_T)idx_ft]; FontRender *fr; FontGlyphChar *fgc; uint32_t i, ii, u, count; int32_t l, m, r; FontRenderIndex idx_fr = ft->h_render; ft->h_render += FontRenderIndex{1}; if (ft->h_render >= ft->n_render) { ft->n_render += FontRenderIndex{5}; FontRender *arr = pk_new((FontRenderIndex_T)ft->n_render); memcpy(arr, ft->renders, sizeof(FontRender) * (FontRenderIndex_T)idx_fr); pk_delete(ft->renders, (FontRenderIndex_T)idx_fr); ft->renders = arr; } fr = &ft->renders[(FontRenderIndex_T)idx_fr]; *fr = {}; fr->uuid = uuid; ECS_CreateEntity(fr, parent); fr->fr_handle.index_ft = idx_ft; fr->fr_handle.index_fr = idx_fr; fr->settings = *settings; fr->text = str; if (window != NULL) { // insert new characters into tmp buffer { DynArray glyph_indices; glyph_indices.Reserve(str.length); count = 0; for (i = 0; i < str.length;) { fgc = nullptr; u = 0; // determine unicode char ii = utf8_to_unicode(&str.val[i], u); if (ii == 0) { fprintf(stderr, "failed to determine unicode for character: at byte index: '%i'\n", i); i += 1; continue; } i += ii; // binary search for glyph details l = 0; r = ft->n_glyphs - 1; do { m = l + (r-l)/2; fgc = &ft->glyphs[m]; if (fgc->unicode < u) l = m + 1; else r = m - 1; } while (fgc->unicode != u && l <= r); if (fgc->unicode != u) { fprintf(stderr, "font: '%s' does not contain unicode character '%u'\n", ft->title.val, u); continue; } count += 1; glyph_indices.Push(m); } // TODO specific bucket fr->n_glyphs = count; fr->glyph_indices = pk_new(count); for (i = 0; i < count; ++i) { fr->glyph_indices[i] = glyph_indices[i]; } } VkDeviceSize item_count_orig = ft->bindings.instance_counter; VkDeviceSize item_length_new = PK_MAX(ft->bindings.instance_buffer_max_count, ft->bindings.instance_counter + count); // copy existing buffer to new buffer // create new buffer if (ft->bindings.instance_buffer_max_count < item_length_new) { byteCount = sizeof(FontInstanceBufferItem) * item_length_new; VkBuffer newBuffer; VkBufferCreateInfo bufferCI; bufferCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferCI.pNext = nullptr; bufferCI.flags = {}; bufferCI.sharingMode = VK_SHARING_MODE_EXCLUSIVE; bufferCI.queueFamilyIndexCount = 1; bufferCI.pQueueFamilyIndices = &graphicsFamilyIndex; bufferCI.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; bufferCI.size = byteCount; vkResult = vkCreateBuffer(vkDevice, &bufferCI, vkAllocator, &newBuffer); assert(vkResult == VK_SUCCESS); VkMemoryRequirements vkMemReqs; vkGetBufferMemoryRequirements(vkDevice, newBuffer, &vkMemReqs); assert(sizeof(FontInstanceBufferItem) % vkMemReqs.alignment == 0); vkDestroyBuffer(vkDevice, newBuffer, vkAllocator); newBuffer = VK_NULL_HANDLE; bufferCI.size = vkMemReqs.size + (vkMemReqs.alignment - (vkMemReqs.size & vkMemReqs.alignment)); vkResult = vkCreateBuffer(vkDevice, &bufferCI, vkAllocator, &newBuffer); assert(vkResult == VK_SUCCESS); VkDeviceMemory new_memory; VkMemoryAllocateInfo vkMemoryAllocateInfo; vkMemoryAllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; vkMemoryAllocateInfo.pNext = nullptr; vkMemoryAllocateInfo.allocationSize = bufferCI.size; vkMemoryAllocateInfo.memoryTypeIndex = FindMemoryTypeIndex(vkMemReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); vkResult = vkAllocateMemory(vkDevice, &vkMemoryAllocateInfo, vkAllocator, &new_memory); assert(vkResult == VK_SUCCESS); vkResult = vkBindBufferMemory(vkDevice, newBuffer, new_memory, 0); assert(vkResult == VK_SUCCESS); if (ft->bindings.bd_instance.buffer != VK_NULL_HANDLE && ft->bindings.instance_counter > 0) { PKVK_BeginBuffer(transferFamilyIndex, byteCount, tmpBufferDetails, PKVK_TmpBufferFlags_NONE); VkCommandBufferBeginInfo vkCommandBufferBeginInfo; vkCommandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; vkCommandBufferBeginInfo.pNext = nullptr; // TODO consider single-use? vkCommandBufferBeginInfo.flags = 0; vkCommandBufferBeginInfo.pInheritanceInfo = nullptr; vkResult = vkBeginCommandBuffer(tmpBufferDetails.cmdBuffer, &vkCommandBufferBeginInfo); assert(vkResult == VK_SUCCESS); VkBufferCopy vk_buffer_copy{}; vk_buffer_copy.srcOffset = 0; vk_buffer_copy.dstOffset = 0; vk_buffer_copy.size = sizeof(FontInstanceBufferItem) * ft->bindings.instance_counter; assert(vk_buffer_copy.size != 0); vkCmdCopyBuffer(tmpBufferDetails.cmdBuffer, ft->bindings.bd_instance.buffer, newBuffer, 1, &vk_buffer_copy); vkResult = vkEndCommandBuffer(tmpBufferDetails.cmdBuffer); assert(vkResult == VK_SUCCESS); VkSubmitInfo submitInfo; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pNext = nullptr; submitInfo.waitSemaphoreCount = 0; submitInfo.pWaitSemaphores = nullptr; submitInfo.pWaitDstStageMask = nullptr; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &tmpBufferDetails.cmdBuffer; submitInfo.signalSemaphoreCount = 0; submitInfo.pSignalSemaphores = nullptr; vkResult = vkQueueSubmit(tmpBufferDetails.queue, 1, &submitInfo, nullptr); assert(vkResult == VK_SUCCESS); vkResult = vkQueueWaitIdle(tmpBufferDetails.queue); assert(vkResult == VK_SUCCESS); PKVK_EndBuffer(tmpBufferDetails, PKVK_TmpBufferFlags_NONE); } if (ft->bindings.bd_instance.buffer != VK_NULL_HANDLE) vkDestroyBuffer(vkDevice, ft->bindings.bd_instance.buffer, vkAllocator); if (ft->gr.deviceMemoryInst != VK_NULL_HANDLE) vkFreeMemory(vkDevice, ft->gr.deviceMemoryInst, vkAllocator); ft->gr.deviceMemoryInst = new_memory; ft->bindings.bd_instance.buffer = newBuffer; } ft->bindings.instance_buffer_max_count = item_length_new; ft->bindings.instance_counter += count; // create tmp local buffer & copy data to graphics card byteCount = sizeof(FontInstanceBufferItem) * count; PKVK_BeginBuffer(transferFamilyIndex, byteCount, tmpBufferDetails); { VkCommandBufferBeginInfo vkCommandBufferBeginInfo; vkCommandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; vkCommandBufferBeginInfo.pNext = nullptr; // TODO consider single-use? vkCommandBufferBeginInfo.flags = 0; vkCommandBufferBeginInfo.pInheritanceInfo = nullptr; vkResult = vkBeginCommandBuffer(tmpBufferDetails.cmdBuffer, &vkCommandBufferBeginInfo); assert(vkResult == VK_SUCCESS); FontType_Inner_CalcTransforms(ft, fr, (FontInstanceBufferItem*)tmpBufferDetails.deviceData); VkBufferCopy vk_buffer_copy{}; vk_buffer_copy.srcOffset = 0; vk_buffer_copy.dstOffset = sizeof(FontInstanceBufferItem) * item_count_orig; vk_buffer_copy.size = sizeof(FontInstanceBufferItem) * count; vkCmdCopyBuffer(tmpBufferDetails.cmdBuffer, tmpBufferDetails.buffer, ft->bindings.bd_instance.buffer, 1, &vk_buffer_copy); vkResult = vkEndCommandBuffer(tmpBufferDetails.cmdBuffer); assert(vkResult == VK_SUCCESS); VkSubmitInfo submitInfo; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pNext = nullptr; submitInfo.waitSemaphoreCount = 0; submitInfo.pWaitSemaphores = nullptr; submitInfo.pWaitDstStageMask = nullptr; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &tmpBufferDetails.cmdBuffer; submitInfo.signalSemaphoreCount = 0; submitInfo.pSignalSemaphores = nullptr; vkResult = vkQueueSubmit(tmpBufferDetails.queue, 1, &submitInfo, nullptr); assert(vkResult == VK_SUCCESS); vkResult = vkQueueWaitIdle(tmpBufferDetails.queue); assert(vkResult == VK_SUCCESS); } PKVK_EndBuffer(tmpBufferDetails); } return fr->fr_handle; } FontRender *FontType_GetFontRender(FontRenderHandle frh) { assert(frh.index_ft < ftd.h_ft); FontType *ft = &ftd.arr_ft[static_cast(frh.index_ft)]; assert(frh.index_fr < ft->h_render); return &ft->renders[static_cast(frh.index_fr)]; } void FontType_UpdateStringRender(FontRenderHandle frh, FontRenderSettings *settings) { assert(settings != nullptr); assert((FontTypeIndex_T)ftd.h_ft > (FontTypeIndex_T)frh.index_ft); FontType *ft = &ftd.arr_ft[(FontTypeIndex_T)frh.index_ft]; assert((FontRenderIndex_T)ft->h_render > (FontRenderIndex_T)frh.index_fr); ft->gr.should_update_instance_buffer = true; ft->renders[(FontRenderIndex_T)frh.index_fr].settings = *settings; } void FontType_RemoveStringRender(FontRenderHandle handle) { FontRender *fr; uint32_t buffer_start_index; FontType *ft = &ftd.arr_ft[(FontTypeIndex_T)handle.index_ft]; // hack, but works ft->gr.should_update_instance_buffer = true; fr = &ft->renders[(FontRenderIndex_T)handle.index_fr]; ft->bindings.instance_counter -= fr->n_glyphs; buffer_start_index = fr->buffer_start_index; fr->n_glyphs = 0; for (FontRenderIndex_T i = (FontRenderIndex_T)handle.index_fr + 1; FontRenderIndex{i} < ft->h_render; ++i) { fr = &ft->renders[i]; fr->buffer_start_index = buffer_start_index; buffer_start_index += fr->n_glyphs; } if (fr->text.reserved > 0) { // 2025-04-16 - JCB // not passing a specific bucket because pk_cstr doesn't store it. // font.cpp assumes the user has already cloned the string if necessary - only assigns the value. pk_delete(fr->text.val, fr->text.reserved); } ECS_MarkForRemoval(fr); }