#include "font.hpp" #include "asset-manager.hpp" #include "pk.h" #include "window.hpp" #include "static-plane.hpp" #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(FONT_GLYPH_CHAR_FLAG); TypeSafeInt_B(FONT_RENDER_FLAG); TypeSafeInt_B(FONT_RENDER_SURFACE_AREA_TYPE_FLAG); static struct FontTypeData { pk_bkt_arr_t fonts{}; pk_membucket *bkt; } ftd; struct FontInstanceBufferItem { glm::mat4 pos_scale; glm::vec4 color_foreground; glm::vec4 color_background; 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]; }; void FontType_Unload(FontTypeHandle font_type_handle); 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 *char_count = nullptr) { 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; } if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_NEW_LINE) == true) { break; } ret += fgc->advance * font_glyph_spacing; } if (char_count != nullptr) { *char_count = i - index; } return ret; } float FontType_Inner_LookAheadWordLength_NoPadding(const FontType *const ft, const FontRender *const fr, uint32_t index, float font_glyph_spacing, uint32_t *char_count = nullptr) { 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; } if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_NEW_LINE) == true) { break; } ret += fgc->advance * font_glyph_spacing; if (i == index) { // if -x, makes word larger // if +x, makes word shorter ret -= fgc->plane_bounds.x * font_glyph_spacing; } } if (i != index) { fgc = &ft->glyphs[fr->glyph_indices[i-1]]; ret -= (fgc->advance - fgc->plane_bounds.z) * font_glyph_spacing; } if (char_count != nullptr) { *char_count = i - index; } return ret; } float FontType_Inner_LookAheadLineLength(const FontType *const ft, const FontRender *const fr, const uint32_t index, float font_glyph_spacing, uint32_t *char_count = nullptr) { uint32_t i, ii; float sz, ret = 0; bool front_whitespace = true; FontGlyphChar *fgc, *fgc2; // When centering, we want to burn white-space characters: // at the front. // at the back. 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_NEW_LINE) == true) { if (i != index) { fgc2 = &ft->glyphs[fr->glyph_indices[i-1]]; if (PK_HAS_FLAG(fgc2->flags, FONT_GLYPH_CHAR_FLAGS_WHITESPACE) == true) { ret -= fgc2->advance * font_glyph_spacing; } } break; } if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_WHITESPACE) == true) { if (front_whitespace == true && PK_HAS_FLAG(fr->settings.surface_area_type_flags, FONT_RENDER_SURFACE_AREA_TYPE_FLAGS_CENTER_HORIZONTAL)) { // burn white-spacing at the front continue; } sz = fgc->advance * font_glyph_spacing; if (ret + sz > fr->settings.surface_area_size.x) { break; } ret += sz; fgc2 = nullptr; if (index != 0) { fgc2 = &ft->glyphs[fr->glyph_indices[i-1]]; if (!PK_HAS_FLAG(fgc2->flags, FONT_GLYPH_CHAR_FLAGS_NEW_LINE)) { fgc2 = nullptr; } } if (i == index && fgc2 != nullptr) { ret = 0; } continue; } sz = FontType_Inner_LookAheadWordLength(ft, fr, i, font_glyph_spacing, &ii); if (ret + sz > fr->settings.surface_area_size.x) { break; } // -1 because the for loop is about to +1. // On words that are not the last, not subtracting 1 burns the next character (bad) // On the last word, not subtracting 1 causes the char_count to be off-by-one i += ii-1; ret += sz; if (front_whitespace == true) { // if -x, makes line larger // if +x, makes line shorter ret -= fgc->plane_bounds.x * font_glyph_spacing; } front_whitespace = false; } // If we are at the end of a line: // Burn all white-space until we have a non-white-space // subtract the extra width of `advance` (char spacing, don't need it) ii = 1; while (i != 0 && i - ii >= index && i - ii > 0) { fgc = &ft->glyphs[fr->glyph_indices[i-ii]]; ret -= fgc->advance * font_glyph_spacing; if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_WHITESPACE)) { ii += 1; continue; } // literal right edge of the char ret += fgc->plane_bounds.z * font_glyph_spacing; break; } if (char_count != nullptr) { *char_count = i - index; } return ret; } float FontType_Inner_LookAheadLineCount(const FontType *const ft, const FontRender *const fr, const uint32_t index, float font_glyph_spacing) { uint32_t ii = 0, i = index; FontGlyphChar *fgc, *fgc2; float ret = 0; bool burn = false; while (i < fr->n_glyphs) { fgc = &ft->glyphs[fr->glyph_indices[i]]; if (fgc->unicode == 10 || fgc->unicode == 13) { if (i+1 < fr->n_glyphs) { fgc2 = &ft->glyphs[fr->glyph_indices[i+1]]; if (fgc->unicode != fgc2->unicode && (fgc2->unicode == 10 || fgc2->unicode == 13)) { ++i; } } ++i; if (burn == false) { ret += 1; } burn = false; continue; } FontType_Inner_LookAheadLineLength(ft, fr, i, font_glyph_spacing, &ii); if (ii == 0) { ++i; continue; } i += ii; ret += 1; burn = true; } return ret; } bool FontType_Inner_CalcTransforms(const FontType *ft, FontRender *fr, FontInstanceBufferItem *ptr_dst, uint32_t *count) { assert(ft != nullptr); assert(fr != nullptr); assert(ptr_dst != nullptr); FontGlyphChar *fgc, *fgc2; FontInstanceBufferItem *buf_item; bool new_word = true; bool first_word_of_line = true; uint32_t i, ii; float font_glyph_spacing; float cursor_head, line_index, line_height, line_length, line_offset; glm::vec2 glyph_size; glm::vec3 translate; glm::vec3 scale; if (PK_HAS_FLAG(FONT_RENDER_FLAG_T(fr->settings.flags), FONT_RENDER_FLAG_T(FONT_RENDER_FLAG_VISIBILITY_INVISIBLE))) { return false; } // 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. // 2025-09-22 - JCB // I believe this is because of how the anti-aliasing works. // When this gets re-visited, attempt to disable anti-aliasing entirely. // Suggestion: require min distance to be 0? // Also note that I don't think we're doing any calculations to make sure the // left-most pixel is exact to the Extent (screen size). // pixel-perfect font rendering might require its own shader. font_glyph_spacing = ft->spacing.em_size * fr->settings.char_scale; cursor_head = 0; line_index = 0; line_offset = 0; line_height = font_glyph_spacing * ft->spacing.line_height * fr->settings.line_height_scale; line_offset -= (font_glyph_spacing * fr->settings.line_height_scale) / 2.0; line_length = FontType_Inner_LookAheadLineLength(ft, fr, 0, font_glyph_spacing); if (PK_HAS_FLAG(fr->settings.surface_area_type_flags, FONT_RENDER_SURFACE_AREA_TYPE_FLAGS_CENTER_HORIZONTAL)) { cursor_head = (fr->settings.surface_area_size.x - line_length) / 2.0; } if (PK_HAS_FLAG(fr->settings.surface_area_type_flags, FONT_RENDER_SURFACE_AREA_TYPE_FLAGS_CENTER_VERTICAL)) { float text_height = FontType_Inner_LookAheadLineCount(ft, fr, 0, font_glyph_spacing) * line_height; line_offset += (fr->settings.surface_area_size.y - text_height) / 2.0; } for (i = 0, ii = 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 (i+1 < fr->n_glyphs) { fgc2 = &ft->glyphs[fr->glyph_indices[i+1]]; if (fgc->unicode != fgc2->unicode && (fgc->unicode == 10 || fgc->unicode == 13) && (fgc2->unicode == 10 || fgc2->unicode == 13)) { i += 1;// burn /r/n and /n/r } } line_index += 1; new_word = true; first_word_of_line = true; cursor_head = 0; line_length = FontType_Inner_LookAheadLineLength(ft, fr, i+1, font_glyph_spacing); if (PK_HAS_FLAG(fr->settings.surface_area_type_flags, FONT_RENDER_SURFACE_AREA_TYPE_FLAGS_CENTER_HORIZONTAL)) { cursor_head = (fr->settings.surface_area_size.x - line_length) / 2.0; } continue; } if (PK_HAS_FLAG(fgc->flags, FONT_GLYPH_CHAR_FLAGS_WHITESPACE) == true) { // 2025-11-18 JCB TODO // This is tricky. // if we wrapped, we want to swallow any remaining spaces... Maybe? // - YES a space between words // - NO a tab? 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)); } else { // x = 2 // a = 4 // 6 = 2 + 4 cursor_head += fgc->advance * font_glyph_spacing; } new_word = true; continue; } if (new_word == true) { float word_width = FontType_Inner_LookAheadWordLength(ft, fr, i, font_glyph_spacing); if (first_word_of_line == false && cursor_head + word_width > fr->settings.surface_area_size.x) { line_index += 1; cursor_head = 0; line_length = FontType_Inner_LookAheadLineLength(ft, fr, i, font_glyph_spacing); if (PK_HAS_FLAG(fr->settings.surface_area_type_flags, FONT_RENDER_SURFACE_AREA_TYPE_FLAGS_CENTER_HORIZONTAL)) { cursor_head = (fr->settings.surface_area_size.x - line_length) / 2.0; } } if (first_word_of_line == true) { cursor_head -= fgc->plane_bounds.x * font_glyph_spacing; } } first_word_of_line = false; 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 // Reminder that we are converting -1,1 positioning to screen-space. // This means that we have to shift by half the width and half the height // in order to position the box correctly (glyph_size). // ; Without this, the character would be OOB on the left and/or top. // ((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) // - first line ignore line height, just use EM translate.y += font_glyph_spacing; translate.y += (line_index * line_height); // - if vertically centered translate.y += line_offset; // 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[ii++]; buf_item->pos_scale = glm::translate(glm::mat4(1), translate); buf_item->pos_scale = glm::scale(buf_item->pos_scale, scale); buf_item->color_foreground = fr->settings.color_foreground; buf_item->color_background = fr->settings.color_background; 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; } if (count != nullptr) { (*count) += ii; }; return true; } void FontType_Init() { FontTypeHandle fti; ftd.bkt = pk_mem_bucket_create(__FILE__, PK_MEM_DEFAULT_BUCKET_SIZE, PK_MEMBUCKET_FLAG_NONE); new (&ftd.fonts) pk_bkt_arr_t (pk_bkt_arr_handle_MAX, ftd.bkt, ftd.bkt); union pke_asset_details ak_img_details { .texture = { .width = 952, .height = 952, }, }; 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, &ak_img_details); 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; fti = FontType_RegisterFont(cstring_to_pk_cstr("fnt_mquin_7y"), ah_img, ah_glyphs, &msdf_s, &spacing); ftd.fonts[fti].entity_flags |= ENTITY_FLAG_DO_NOT_SERIALIZE; } void FontType_Teardown() { bool b; pk_iter_t iter_ft{}; b = pk_bkt_arr_iter_begin(&ftd.fonts, &iter_ft); while (b == true) { FontType_Unload(iter_ft->font_type_handle); b = pk_bkt_arr_iter_increment(&ftd.fonts, &iter_ft); } pk_bkt_arr_teardown(&ftd.fonts); pk_mem_bucket_destroy(ftd.bkt); ftd.bkt = nullptr; } // 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; uint32_t index; bool b; pk_iter_t ft{}; b = pk_bkt_arr_iter_begin(&ftd.fonts, &ft); while (b) { index = 0; pk_bkt_arr_t::FN_Iter iter_fn; iter_fn.func = [&ft, &index](FontRender *fr) { if (fr->isMarkedForRemoval == true) { if (fr->text.reserved > 0) { pk_delete_arr(fr->text.val, fr->text.reserved); } pk_bkt_arr_free_handle(&ft->renders, fr->font_render_handle); ft->gr.should_update_instance_buffer = true; return; } // skip text if not valid. // reminder: happens intentionally in FontType_RemoveStringRender. if (fr->text.val == nullptr) { return; } index += fr->n_glyphs; }; pk_bkt_arr_iterate(&ft->renders, iter_fn.invoke, &iter_fn); if (ft->isMarkedForRemoval == true) { FontType_Unload(FontTypeHandle{ft.id.bkt.b, ft.id.bkt.i}); b = pk_bkt_arr_iter_increment(&ftd.fonts, &ft); continue; } if (pkeSettings.rt.was_framebuffer_resized == false && ft->gr.should_update_instance_buffer == false) { b = pk_bkt_arr_iter_increment(&ftd.fonts, &ft); continue; } if (index == 0) { ft->bindings.instance_counter = 0; b = pk_bkt_arr_iter_increment(&ftd.fonts, &ft); continue; } ft->gr.should_update_instance_buffer = false; pk_arr_t fibis{}; pk_arr_resize(&fibis, index); index = 0; iter_fn.func = [&ft, &index, &fibis](FontRender *fr) { if (fr->isMarkedForRemoval == true) { return; } // skip text if not valid. // reminder: happens intentionally in FontType_RemoveStringRender. if (fr->text.val == nullptr) { return; } FontType_Inner_CalcTransforms(ft, fr, &fibis[index], &index); }; pk_bkt_arr_iterate(&ft->renders, iter_fn.invoke, &iter_fn); ft->bindings.instance_counter = index; /* early-out because vulkan is not initialized */ if (window == NULL) { b = pk_bkt_arr_iter_increment(&ftd.fonts, &ft); continue; } // check recreate buffer // check against index because it's the real count // fibis.next includes whitespace & unhandled characters if (ft->bindings.instance_buffer_max_count < index) { 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.offsets[0] = 0; vkFreeMemory(vkDevice, ft->gr.deviceMemoryInst, vkAllocator); ft->gr.deviceMemoryInst = VK_NULL_HANDLE; } pkvk_buffer_create_data create_data{}; create_data.buffer_byte_length[0] = fibis.stride * index; create_data.n_buffers = 1; create_data.index_instance = 0; create_data.index_index = -1; pkvk_buffer_create_data_out out{}; pkvk_buffer_create(&create_data, &out); ft->bindings.bd_instance.buffer = out.buffers[0]; ft->gr.deviceMemoryInst = out.device_memory_instance; ft->bindings.instance_buffer_max_count = out.memory_requirements_instance.size / sizeof(FontInstanceBufferItem); } PKVK_TmpBufferDetails tmpBufferDetails{}; PKVK_BeginBuffer(graphicsFamilyIndex, (VkDeviceSize)fibis.stride * (VkDeviceSize)index, tmpBufferDetails); assert(tmpBufferDetails.buffer != VK_NULL_HANDLE); { 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.data, (size_t)fibis.stride * (size_t)index); VkBufferCopy vk_buffer_copy{}; vk_buffer_copy.srcOffset = 0; vk_buffer_copy.dstOffset = 0; vk_buffer_copy.size = (VkDeviceSize)fibis.stride * (VkDeviceSize)index; 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_arr_reset(&fibis); b = pk_bkt_arr_iter_increment(&ftd.fonts, &ft); } } void FontType_Serialize(std::ostream &stream, FontType *ft) { FontTypeSpacing sp{}; FontTypeMSDFSettings msdf{}; 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; 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_arr(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(FontTypeHandle font_type_handle) { assert(pk_bkt_arr_handle_validate(&ftd.fonts, font_type_handle) == PK_BKT_ARR_HANDLE_VALIDATION_VALID); return &ftd.fonts[font_type_handle]; } FontType* FontType_GetByTitle(const pk_cstr title) { assert(title.val != nullptr); bool b; pk_iter_t iter_ft{}; b = pk_bkt_arr_iter_begin(&ftd.fonts, &iter_ft); while(b) { if (iter_ft->title.val == title.val || strcmp(iter_ft->title.val, title.val) == 0) return (FontType*)iter_ft.data; b = pk_bkt_arr_iter_increment(&ftd.fonts, &iter_ft); } return nullptr; } pk_bkt_arr* FontType_GetFonts() { return &ftd.fonts; } FontTypeHandle FontType_RegisterFont(pk_cstr title, AssetHandle fontTextureHandle, AssetHandle glyphsHandle, FontTypeMSDFSettings *msdf_settings, FontTypeSpacing *spacing) { VkResult vkResult; const Asset *fontTexture = nullptr; const Asset *glyphs = nullptr; FontTypeHandle font_type_handle; FontType *ft; assert(fontTextureHandle != AssetHandle_MAX); assert(glyphsHandle != AssetHandle_MAX); assert(msdf_settings != nullptr); assert(spacing != nullptr); fontTexture = AM_Get(fontTextureHandle); font_type_handle = FontTypeHandle {pk_bkt_arr_new_handle(&ftd.fonts)}; ft = &ftd.fonts[font_type_handle]; new (ft) FontType {}; ft->font_type_handle = font_type_handle; ft->title = title; ft->fontTextureAssetHandle = fontTextureHandle; ft->glyphDetailsAssetHandle = glyphsHandle; new (&ft->renders) pk_bkt_arr_t{pk_bkt_arr_handle_MAX, ftd.bkt, ftd.bkt}; ft->msdf_settings = *msdf_settings; ft->spacing = *spacing; glyphs = AM_Get(glyphsHandle); ft->n_glyphs = glyphs->size / sizeof(FontGlyphChar); ft->glyphs = (FontGlyphChar*)glyphs->ptr; ft->atlas_size.x = (float)fontTexture->details.texture.width; ft->atlas_size.y = (float)fontTexture->details.texture.height; 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.texture_assets[0] = fontTexture; txtr_data.n_textures = 1; pkvk_texture_upload_data_out txtr_out{}; pkvk_texture_upload(&txtr_data, &txtr_out); ft->gr.textureImage = txtr_out.images[0]; ft->gr.textureImageView = txtr_out.image_views[0]; ft->gr.deviceMemoryTexture = txtr_out.device_memory; /* * 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); } AM_Release(fontTextureHandle); AM_Release(glyphsHandle); return font_type_handle; } void FontType_Unload(FontTypeHandle font_type_handle) { assert(pk_bkt_arr_handle_validate(&ftd.fonts, font_type_handle) == PK_BKT_ARR_HANDLE_VALIDATION_VALID); FontType *ft = &ftd.fonts[font_type_handle]; if (ft->renders.head_r.b > 0 || ft->renders.head_r.i > 0) { pk_bkt_arr_t::FN_Iter iter_fn{}; iter_fn.func = [](FontRender *fr) { if (fr->glyph_indices != nullptr) { pk_delete_arr(fr->glyph_indices, fr->n_glyphs, ftd.bkt); } }; pk_bkt_arr_iterate(&ft->renders, iter_fn.invoke, &iter_fn); pk_bkt_arr_teardown(&ft->renders); } 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_arr(ft->title.val, ft->title.reserved); ft->title.val = nullptr; ft->title.length = 0; } new (ft) FontType{}; pk_bkt_arr_free_handle(&ftd.fonts, font_type_handle); } void FontType_cstr_to_unicode(const FontType &ft, pk_arr_t &arr, const pk_cstr &str) { uint32_t i, ii, u; int32_t l, m, r; FontGlyphChar *fgc; 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; } pk_arr_append_t(&arr, uint32_t(m)); } } // 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. FontTypeRender FontType_AddStringRender(FontTypeHandle font_type_handle, const pk_cstr &&str, FontRenderSettings *settings, Entity_Base *parent, pk_uuid uuid) { assert(settings != nullptr); FontType *ft = &ftd.fonts[font_type_handle]; FontRender *fr; FontTypeRender ret{}; uint32_t i, count; ret.font_type_handle = font_type_handle; ret.font_render_handle = FontRenderHandle{pk_bkt_arr_new_handle(&ft->renders)}; fr = &ft->renders[ret.font_render_handle]; new (fr) FontRender{}; fr->uuid = uuid; ECS_CreateEntity(fr, parent); fr->font_type_handle = font_type_handle; fr->font_render_handle = ret.font_render_handle; fr->settings = *settings; fr->text = str; // insert new characters into tmp buffer pk_arr_t glyph_indices{pkeSettings.mem_bkt.game_transient}; pk_arr_reserve(&glyph_indices, str.length); FontType_cstr_to_unicode(*ft, glyph_indices, str); count = glyph_indices.next; fr->n_glyphs = count; fr->glyph_indices = pk_new_arr(count, ftd.bkt); for (i = 0; i < count; ++i) { fr->glyph_indices[i] = glyph_indices[i]; } glyph_indices.data = nullptr; ft->gr.should_update_instance_buffer = true; return ret; } FontRender *FontType_GetFontRender(FontTypeRender ftr) { if (pk_bkt_arr_handle_validate(&ftd.fonts, ftr.font_type_handle) != PK_BKT_ARR_HANDLE_VALIDATION_VALID) { return nullptr; } FontType *ft = &ftd.fonts[ftr.font_type_handle]; if (pk_bkt_arr_handle_validate(&ft->renders, ftr.font_render_handle) != PK_BKT_ARR_HANDLE_VALIDATION_VALID) { return nullptr; } return &ft->renders[ftr.font_render_handle]; } void FontType_UpdateStringRender(FontTypeRender ftr, FontRenderSettings *settings) { assert(settings != nullptr); assert(pk_bkt_arr_handle_validate(&ftd.fonts, ftr.font_type_handle) == PK_BKT_ARR_HANDLE_VALIDATION_VALID); FontType *ft = &ftd.fonts[ftr.font_type_handle]; assert(pk_bkt_arr_handle_validate(&ft->renders, ftr.font_render_handle) == PK_BKT_ARR_HANDLE_VALIDATION_VALID); ft->gr.should_update_instance_buffer = true; ft->renders[ftr.font_render_handle].settings = *settings; } void FontType_UpdateStringRenderText(FontTypeRender ftr, pk_cstr &&cstr) { uint32_t i, count; assert(pk_bkt_arr_handle_validate(&ftd.fonts, ftr.font_type_handle) == PK_BKT_ARR_HANDLE_VALIDATION_VALID); FontType *ft = &ftd.fonts[ftr.font_type_handle]; assert(pk_bkt_arr_handle_validate(&ft->renders, ftr.font_render_handle) == PK_BKT_ARR_HANDLE_VALIDATION_VALID); FontRender &fr = ft->renders[ftr.font_render_handle]; pk_cstr old_str = fr.text; if (window == NULL) { return; } pk_arr_t glyph_indices{}; glyph_indices.bkt = pkeSettings.mem_bkt.game_transient; pk_arr_reserve(&glyph_indices, PK_MAX(1, fr.n_glyphs + (cstr.length - old_str.length))); FontType_cstr_to_unicode(*ft, glyph_indices, cstr); count = glyph_indices.next; fr.n_glyphs = count; fr.glyph_indices = pk_new_arr(count, ftd.bkt); for (i = 0; i < count; ++i) { fr.glyph_indices[i] = glyph_indices[i]; } glyph_indices.data = nullptr; if (fr.text.reserved > 0 && fr.text.val != NULL) pk_delete_arr(fr.text.val, fr.text.reserved); fr.text = cstr; ft->gr.should_update_instance_buffer = true; } void FontType_RemoveStringRender(FontTypeRender ftr) { FontRender *fr; assert(pk_bkt_arr_handle_validate(&ftd.fonts, ftr.font_type_handle) == PK_BKT_ARR_HANDLE_VALIDATION_VALID); FontType *ft = &ftd.fonts[ftr.font_type_handle]; assert(pk_bkt_arr_handle_validate(&ft->renders, ftr.font_render_handle) == PK_BKT_ARR_HANDLE_VALIDATION_VALID); fr = &ft->renders[ftr.font_render_handle]; // 2025-12-03 JCB // This prevents text from being seen THIS tick. ft->gr.should_update_instance_buffer = true; if (fr->text.reserved > 0 && fr->text.val != NULL) pk_delete_arr(fr->text.val, fr->text.reserved); fr->text.val = nullptr; fr->text.length = 0; fr->text.reserved = 0; ECS_MarkForRemoval(fr); }