/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "text.h" #include "text_resources.h" #include "syscall/syscall.h" #include "applib/fonts/fonts.h" #include "applib/fonts/fonts_private.h" #include "resource/resource_ids.auto.h" #include "system/logging.h" #include "system/passert.h" #include "system/profiler.h" #include "util/math.h" #include "util/size.h" #include #include #include #define RLE4_UNITS_BIT_WIDTH (4) #define RLE4_UNITS_PER_BYTE (8 / RLE4_UNITS_BIT_WIDTH) static const size_t s_font_md_size[] = { 0, // There currently is no font version 0. This makes decoding much easier & consistent sizeof(FontMetaDataV1), sizeof(FontMetaData), sizeof(FontMetaDataV3) }; static uint8_t prv_font_hash(Codepoint codepoint, uint8_t table_size) { return (codepoint % table_size); } static Codepoint prv_offset_table_get_codepoint(FontCache *font_cache, const FontMetaData *md, int index) { const bool offset_16 = HAS_FEATURE(md->version, VERSION_FIELD_FEATURE_OFFSET_16); if (md->codepoint_bytes == 2) { return offset_16 ? font_cache->offsets_buffer_2_2[index].codepoint : font_cache->offsets_buffer_2_4[index].codepoint; } else { return offset_16 ? font_cache->offsets_buffer_4_2[index].codepoint : font_cache->offsets_buffer_4_4[index].codepoint; } } static uint32_t prv_offset_table_get_offset(FontCache *font_cache, const FontMetaData *md, int index) { const bool offset_16 = HAS_FEATURE(md->version, VERSION_FIELD_FEATURE_OFFSET_16); if (md->codepoint_bytes == 2) { return offset_16 ? font_cache->offsets_buffer_2_2[index].offset : font_cache->offsets_buffer_2_4[index].offset; } else { return offset_16 ? font_cache->offsets_buffer_4_2[index].offset : font_cache->offsets_buffer_4_4[index].offset; } } static uint32_t prv_offset_table_entry_size(const FontMetaData *md) { const bool offset_16 = HAS_FEATURE(md->version, VERSION_FIELD_FEATURE_OFFSET_16); if (md->codepoint_bytes == 2) { return offset_16 ? sizeof(OffsetTableEntry_2_2) : sizeof(OffsetTableEntry_2_4); } else { return offset_16 ? sizeof(OffsetTableEntry_4_2) : sizeof(OffsetTableEntry_4_4); } } static int prv_offset_table_get_id(const FontMetaData *md, Codepoint codepoint) { if (FONT_VERSION(md->version) == FONT_VERSION_1) { return (1); } else { return prv_font_hash(codepoint, md->hash_table_size); } } static int prv_load_offset_table(Codepoint codepoint, FontCache *font_cache, const FontResource *font_res) { const int table_id = prv_offset_table_get_id(&font_res->md, codepoint); if (table_id == font_cache->offset_table_id) { return font_cache->offset_table_size; } size_t num_bytes, offset, num_entries; const uint8_t version = FONT_VERSION(font_res->md.version); if (version == FONT_VERSION_1) { offset = s_font_md_size[FONT_VERSION_1]; num_bytes = font_res->md.number_of_glyphs * prv_offset_table_entry_size(&font_res->md); num_entries = font_res->md.number_of_glyphs; PBL_ASSERTN(num_bytes <= sizeof(font_cache->offsets_buffer_2_2)); } else { FontHashTableEntry table_entry; Codepoint hash_entry_offset = s_font_md_size[version] + table_id * sizeof(FontHashTableEntry); // find which bucket the codepoint was put into TODO: cache hash table? PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "HTE read: table_id:%d, cp:%"PRIx32", offset:%"PRIx32, table_id, codepoint, hash_entry_offset); SYS_PROFILER_NODE_START(text_render_flash); sys_resource_load_range(font_res->app_num, font_res->resource_id, hash_entry_offset, (uint8_t*)&table_entry, sizeof(FontHashTableEntry)); SYS_PROFILER_NODE_STOP(text_render_flash); offset = s_font_md_size[version] + (sizeof(FontHashTableEntry) * font_res->md.hash_table_size) + table_entry.offset; num_bytes = table_entry.count * prv_offset_table_entry_size(&font_res->md); num_entries = table_entry.count; PBL_ASSERTN(num_bytes <= sizeof(font_cache->offsets_buffer_4_4)); } PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "HT read: offset: %zx, bytes: %zu", offset, num_bytes); SYS_PROFILER_NODE_START(text_render_flash); sys_resource_load_range(font_res->app_num, font_res->resource_id, offset, (uint8_t *)font_cache->offsets_buffer_4_4, num_bytes); SYS_PROFILER_NODE_STOP(text_render_flash); font_cache->offset_table_id = table_id; font_cache->offset_table_size = num_entries; return num_entries; } static uint32_t prv_get_cache_key(const FontResource *font_res, Codepoint codepoint) { // Ideally we'd be able to use the full app_num, resource_id and codepoint combined into a key // in a unique matter, but unfortunately there aren't enough bits. Note that this value needs to // be unique, there's no collision handling and if one does occur you'll just end up reading the // wrong metadata. Luckily we don't need to store all the bits due to assumptions we can make. // We know that for a given FontCache we'll only use a combination of fonts from the running app // and system fonts and we'll never use custom fonts from two different app banks at the same // time. This means we only need a single bit to store whether the font is for the system bank // (bank 0) or an app bank (bank > 0). // resource_id is technically a full 32-bit id but in practice it's much smaller. The firmware // only uses 400~ unique resources at the time of writing so 14 bits (16384 resources) should be // enough. // Therefore our key layout becomes: // is_app:1 // resource_id:14 // codepoint:17 const bool is_app = (font_res->app_num != 0); return (is_app ? 1 << 31 : 0) | ((font_res->resource_id << 17) | (codepoint & 0x0001FFFF)); } static uint32_t prv_get_glyph_table_offset(FontCache *font_cache, Codepoint codepoint, const FontResource *font_res) { int min_idx = 0; int max_idx = prv_load_offset_table(codepoint, font_cache, font_res); uint32_t offset = 0; while (max_idx >= min_idx) { int mid_idx = (max_idx + min_idx) / 2; Codepoint codepoint_at_mid_idx = prv_offset_table_get_codepoint(font_cache, &font_res->md, mid_idx); if (codepoint_at_mid_idx < codepoint) { min_idx = mid_idx + 1; } else if (codepoint_at_mid_idx > codepoint) { max_idx = mid_idx - 1; } else { offset = prv_offset_table_get_offset(font_cache, &font_res->md, mid_idx); break; } } return offset; } static uint32_t prv_get_glyph_data_offset(Codepoint codepoint, FontCache *font_cache, const FontResource *font_res) { const uint32_t offset = prv_get_glyph_table_offset(font_cache, codepoint, font_res); if (offset <= 0) { return 0; } // Compute the offset of the glyph data (relative to the beginning // of the font blob). // // See: https://pebbletechnology.atlassian.net/wiki/display/DEV/Pebble+Resource+Pack+Format uint32_t address; const uint32_t version = FONT_VERSION(font_res->md.version); if (version == FONT_VERSION_1) { address = s_font_md_size[FONT_VERSION_1] + (font_res->md.number_of_glyphs * prv_offset_table_entry_size(&font_res->md)) + (sizeof(uint32_t) * offset); } else { address = s_font_md_size[version] + (sizeof(FontHashTableEntry) * font_res->md.hash_table_size) + (prv_offset_table_entry_size(&font_res->md) * font_res->md.number_of_glyphs) + offset; } return address; } //! Decode RLE4 decoded glyph data in place. //! @return g or NULL on error. #define RLE4_SYMBOL_MASK 0x08 #define RLE4_LENGTH_MASK 0x07 static GlyphData *prv_decompress_glyph_data(GlyphData *g, uint8_t *src) { // This RLE4 decompressor expects GlyphData to be formatted like this: // [
| | ] // *src is a pointer to the beginning of // // Once decompressed, Glyph Data will be formatted like this: // [
| | ] // // The glyph is decoded in-place, so obviously, it's imperative that and // not overlap at any time. This is checked by fontgen.py and confirmed // by the code below. // // RLE4 data is encoded as a stream of RLE Units. // 0 1 2 3 // +-+-+-+-+ // |*| Len | Where * is the encoded symbol [0,1] and 'Len + 1' is the number of symbols // +-+-+-+-+ in the run [1,8]. For example, 1000 expands to '1' and 0100 expands to '00000' // // RLE Units are packed as pairs -- two to a byte. // // Decoding is done by expanding the bit patterns into a buffer until we have at least 8 bits of // pixels to write. PBL_ASSERTN(g); PBL_ASSERTN(src > (uint8_t *)g->data); uint8_t *dst = (uint8_t *)g->data; unsigned total_pixels_decoded = 0; unsigned num_rle_units = g->header.num_rle_units; // Decoded pixel buffer. At least 16 bits (to hold 2 decoded RLE4s) uint16_t buf = 0; int8_t buf_num_bits = 0; PBL_ASSERTN(num_rle_units <= (CACHE_GLYPH_SIZE * RLE4_UNITS_PER_BYTE)); while (num_rle_units) { PBL_ASSERTN(src < &((uint8_t *)g->data)[CACHE_GLYPH_SIZE]); uint8_t rle_unit_pair = *src++; for (unsigned int i = 0; i < RLE4_UNITS_PER_BYTE; ++i) { if (!num_rle_units) { break; // Handle a padded, odd number of rle units } // Number of bits in this run uint8_t length = (rle_unit_pair & RLE4_LENGTH_MASK) + 1; // Symbol of this run. We don't need to generate a pattern of 0s. ;-) if (rle_unit_pair & RLE4_SYMBOL_MASK) { uint8_t pattern = ((1 << length) - 1); // List of 'length' 1s buf |= (pattern << buf_num_bits); } buf_num_bits += length; total_pixels_decoded += length; // Store 8 bits worth of pixels if (buf_num_bits >= 8) { PBL_ASSERTN(dst < src); *dst++ = (buf & 0xFF); buf >>= 8; buf_num_bits -= 8; } // Now process the second nibble rle_unit_pair >>= 4; num_rle_units--; } } // Flush out any remaining pixels while (buf_num_bits > 0) { PBL_ASSERTN(dst < &((uint8_t *)g->data)[CACHE_GLYPH_SIZE]); *dst++ = (buf & 0xFF); buf >>= 8; buf_num_bits -= 8; } // Fix-up the height to reflect the bit pattern instead of the number of RLE units. if (g->header.width_px) { g->header.height_px = total_pixels_decoded / g->header.width_px; } return g; } static bool prv_load_glyph_bitmap(Codepoint codepoint, const FontResource *font_res, LineCacheData *data) { GlyphData *g = &data->glyph_data; const size_t bitmap_offset = (FONT_VERSION(font_res->md.version) == FONT_VERSION_1) ? sizeof(GlyphHeaderDataV1) : sizeof(GlyphHeaderData); const uint32_t bitmap_addr = data->resource_offset + bitmap_offset; size_t glyph_size_bytes; // Handle RLE4 compressed glyphs. header.height_px has been 'borrowed' to mean the number of // 4-bit RLE units used to encode the glyph. We determine the height by decoding the number of // bits and then dividing by the width. glyph.height_px must be updated! if (HAS_FEATURE(font_res->md.version, VERSION_FIELD_FEATURE_RLE4)) { // Two RLE4 units per byte. Round up to the next whole byte glyph_size_bytes = (g->header.height_px + (RLE4_UNITS_PER_BYTE - 1)) / RLE4_UNITS_PER_BYTE; } else { // Number of bytes, make sure we round up to the next whole byte glyph_size_bytes = ((g->header.width_px * g->header.height_px) + (8 - 1)) / 8; } PBL_ASSERT(glyph_size_bytes <= CACHE_GLYPH_SIZE, "text codepoint %"PRIx32" is %zu bytes, overflowing %zu max size", codepoint, glyph_size_bytes, CACHE_GLYPH_SIZE); if (glyph_size_bytes) { uint8_t *target; PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "GD read: cp: %"PRIx32", res_bank: %"PRIu32", res_id: %"PRIu32", " "offset: %"PRIx32", bytes: %zu", codepoint, font_res->app_num, font_res->resource_id, bitmap_addr, glyph_size_bytes); if (HAS_FEATURE(font_res->md.version, VERSION_FIELD_FEATURE_RLE4)) { // Load the glyph data at the end of the buffer target = &((uint8_t *)g->data)[CACHE_GLYPH_SIZE - glyph_size_bytes]; } else { target = (uint8_t *)g->data; } SYS_PROFILER_NODE_START(text_render_flash); size_t num_bytes_loaded = sys_resource_load_range(font_res->app_num, font_res->resource_id, bitmap_addr, target, glyph_size_bytes); SYS_PROFILER_NODE_STOP(text_render_flash); if (glyph_size_bytes && !num_bytes_loaded) { PBL_LOG(LOG_LEVEL_WARNING, "Failed to load glyph bitmap from resources; cp: %"PRIx32", addr: %"PRIx32, codepoint, bitmap_addr); return false; } if (HAS_FEATURE(font_res->md.version, VERSION_FIELD_FEATURE_RLE4)) { SYS_PROFILER_NODE_START(text_render_compress); g = prv_decompress_glyph_data(g, target); SYS_PROFILER_NODE_STOP(text_render_compress); } } data->is_bitmap_loaded = true; return true; } static const GlyphData *prv_get_glyph_metadata_from_spi(Codepoint codepoint, FontCache *font_cache, const FontResource *font_res, bool need_bitmap) { const uint32_t cache_key = prv_get_cache_key(font_res, codepoint); LineCacheData *cached = NULL; // If we don't have bitmap caching, we have a single glyph_buffer that contains the last used // glyph. If this matches the glyph we're looking for right now, that's what we want to use. // Potentially this also has the bitmap loaded already. #if !CAPABILITY_HAS_GLYPH_BITMAP_CACHING if (font_cache->glyph_buffer_key == cache_key) { cached = (LineCacheData *)(font_cache->glyph_buffer); } #endif PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "looking up cp: %"PRIx32", key:%"PRIx32, codepoint, cache_key); // If the glyph_buffer doesn't match this glyph, or we have bitmap caching, check the // keyed_circular_cache for this glyph. if (!cached) { cached = keyed_circular_cache_get(&font_cache->line_cache, cache_key); #if !CAPABILITY_HAS_GLYPH_BITMAP_CACHING // If we don't have bitmap caching, the keyed_circular_cache entry cannot store the bitmap. // Therefore, we need to copy the matched entry into `glyph_buffer` which does have the space // to store the bitmap. if (cached) { memcpy(font_cache->glyph_buffer, cached, sizeof(LineCacheData)); font_cache->glyph_buffer_key = cache_key; // Point `cached` at the glyph buffer. cached = (LineCacheData *)(font_cache->glyph_buffer); cached->is_bitmap_loaded = false; } #endif } if (cached) { if (cached->resource_offset == 0) { // missing character return NULL; } if (need_bitmap && !cached->is_bitmap_loaded && !prv_load_glyph_bitmap(codepoint, font_res, cached)) { return NULL; } return &cached->glyph_data; } // We missed the cache, so we need to build a new cache entry. LineCacheData *data = &font_cache->cache_data_scratch; data->is_bitmap_loaded = false; data->resource_offset = prv_get_glyph_data_offset(codepoint, font_cache, font_res); GlyphData *g = &data->glyph_data; if (data->resource_offset == 0) { PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "offset for cp: %"PRIx32" is NULL", codepoint); // Put the missing character into our cache so we don't waste time looking for it again keyed_circular_cache_push(&font_cache->line_cache, cache_key, data); return NULL; } size_t num_bytes_loaded; if (FONT_VERSION(font_res->md.version) == FONT_VERSION_1) { GlyphHeaderDataV1 header; PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "LGMD READ: offset: %"PRIx32", bytes: %zu", data->resource_offset, sizeof(header)); SYS_PROFILER_NODE_START(text_render_flash); num_bytes_loaded = sys_resource_load_range(font_res->app_num, font_res->resource_id, data->resource_offset, (uint8_t *)&header, sizeof(header)); SYS_PROFILER_NODE_STOP(text_render_flash); // convert to a GlyphHeaderData struct memcpy(&g->header, &header, sizeof(GlyphHeaderData)); g->header.horiz_advance = header.horiz_advance; } else { PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "GMD read: cp: %"PRIx32", offset: %"PRId32", bytes: %zu", codepoint, data->resource_offset, sizeof(GlyphHeaderData)); SYS_PROFILER_NODE_START(text_render_flash); num_bytes_loaded = sys_resource_load_range(font_res->app_num, font_res->resource_id, data->resource_offset, (uint8_t *)&g->header, sizeof(GlyphHeaderData)); SYS_PROFILER_NODE_STOP(text_render_flash); } if (!num_bytes_loaded) { PBL_LOG(LOG_LEVEL_WARNING, "Failed to load glyph metadata from resources; cp: %"PRIx32", offset: %"PRIx32, codepoint, data->resource_offset); return NULL; } LineCacheData *final_data; #if !CAPABILITY_HAS_GLYPH_BITMAP_CACHING // Copy the info into the glyph_buffer. // This must be done _before_ loading the bitmap, otherwise loading the bitmap may modify the // metadata! We will use `glyph_buffer` as the final data, and leave `data` as the uncooked // version, that way we can push `data` into the circular cache. memcpy(font_cache->glyph_buffer, data, sizeof(LineCacheData)); font_cache->glyph_buffer_key = cache_key; final_data = (LineCacheData *)(font_cache->glyph_buffer); #else final_data = data; #endif if (need_bitmap && !prv_load_glyph_bitmap(codepoint, font_res, final_data)) { return NULL; } // We push `data`, which will be cooked data if the bitmap is stored along with it, or // the uncooked data if it's not. In reality, this only matters to compressed glyphs, since // compressed glyphs are the only case where the metadata gets modified. // The only time the data is cooked is when loading a bitmap and the glyph is compressed, in // which case the `num_rle_units` field is turned back into `height_px`. keyed_circular_cache_push(&font_cache->line_cache, cache_key, data); // We return `final_data` though, because that has the actual metadata info that needs to be // used. return &final_data->glyph_data; } static void prv_check_font_cache(FontCache *font_cache, const FontResource *font_res) { // Invalidate the offset table if (font_cache->cached_font != font_res) { font_cache->offset_table_id = -1; font_cache->cached_font = font_res; } } static bool prv_load_font_res(ResAppNum app_num, uint32_t resource_id, FontResource *font_res, bool is_extended) { font_res->resource_id = resource_id; font_res->app_num = app_num; if (resource_id != RESOURCE_ID_FONT_FALLBACK_INTERNAL && !sys_resource_is_valid(app_num, resource_id)) { if (!is_extended) { PBL_LOG(LOG_LEVEL_WARNING, "Invalid text resource id %"PRId32, resource_id); } return false; } if (app_num == SYSTEM_APP && !sys_resource_get_and_cache(app_num, resource_id)) { return false; } PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "FMD read: bytes:%d", (int)sizeof(FontMetaDataV3)); FontMetaDataV3 header; SYS_PROFILER_NODE_START(text_render_flash); uint32_t bytes_read = sys_resource_load_range(app_num, resource_id, 0, (uint8_t*)&header, sizeof(FontMetaDataV3)); SYS_PROFILER_NODE_STOP(text_render_flash); if (bytes_read != sizeof(FontMetaDataV3)) { PBL_LOG(LOG_LEVEL_ERROR, "Tried to load resource too small to have metadata for res %"PRId32, resource_id); return false; } memcpy(&font_res->md, &header, sizeof(FontMetaData)); switch (header.version) { case FONT_VERSION_1: // no hash table, no variable codepoint size, no feature bits font_res->md.hash_table_size = 0; // Version 1 fonts do use 16 bit offsets and 16 bit codepoints. This simplifies the code above font_res->md.codepoint_bytes = 2; font_res->md.version |= VERSION_FIELD_FEATURE_OFFSET_16; break; case FONT_VERSION_2: break; case FONT_VERSION_3: // Make sure that the font header is internally consistent PBL_ASSERTN(header.size == sizeof(FontMetaDataV3)); // HACK alert: Copy the feature bits to the top two bits of the header version. if (header.features & FEATURE_OFFSET_16) { font_res->md.version |= VERSION_FIELD_FEATURE_OFFSET_16; } if (header.features & FEATURE_RLE4) { font_res->md.version |= VERSION_FIELD_FEATURE_RLE4; } break; default: PBL_LOG(LOG_LEVEL_ERROR, "Unknown font resource version %"PRIu8, header.version); return false; } return true; } static const FontResource *prv_font_res_for_codepoint(Codepoint codepoint, const FontInfo *font_info) { if (!codepoint_is_latin(codepoint) && !codepoint_is_emoji(codepoint) && !codepoint_is_special(codepoint) && font_info->extended) { // Latin & emoji codepoints are in base, others are in extension return (&font_info->extension); } else if (codepoint_is_emoji(codepoint) && font_info->base.app_num == SYSTEM_APP) { // Assuming we are using base FontInfo *emoji_font = fonts_get_system_emoji_font_for_size(font_info->max_height); if (emoji_font) { return &emoji_font->base; } } return (&font_info->base); } static void prv_resource_changed_callback(void *data) { FontInfo *font_info = (FontInfo *)data; font_info->loaded = false; font_info->extended = false; } /////////////////////////// // Public API bool text_resources_init_font(ResAppNum app_num, uint32_t font_resource, uint32_t extended_resource, FontInfo *font_info) { // load the base of the font or bail if (!font_resource || !prv_load_font_res(app_num, font_resource, &font_info->base, false /* is_extended */)) { return false; } // look for an extension font and load it if (extended_resource) { // if you want 3rd party apps to use extended fonts, you'll have to unwatch when they unload // and create a syscall for resource_watch PBL_ASSERTN(app_num == SYSTEM_APP); if (font_info->extension_changed_cb == NULL) { font_info->extension_changed_cb = resource_watch(app_num, extended_resource, prv_resource_changed_callback, font_info); } font_info->extended = prv_load_font_res(app_num, extended_resource, &font_info->extension, true /* is_extended */); } font_info->max_height = MAX(font_info->extension.md.max_height, font_info->base.md.max_height); font_info->loaded = true; return true; } static const GlyphData *prv_get_glyph(FontCache *font_cache, Codepoint codepoint, FontInfo *font_info, bool need_bitmap) { if (!font_info->loaded) { sys_font_reload_font(font_info); } // if we cannot find the codepoint we are looking for, we should always be // able to find the wildcard (square box) or ' ' character to display. We use // the wildcard codepoint from the base font in case the extension pack has // been deleted const Codepoint codepoint_list[] = { codepoint, font_info->base.md.wildcard_codepoint, ' ' }; for (unsigned int i = 0; i < ARRAY_LENGTH(codepoint_list); i++) { const FontResource *font_res = prv_font_res_for_codepoint(codepoint_list[i], font_info); prv_check_font_cache(font_cache, font_res); const GlyphData *data = prv_get_glyph_metadata_from_spi(codepoint_list[i], font_cache, font_res, need_bitmap); if (data) { return data; } } PBL_LOG(LOG_LEVEL_WARNING, "failed to load glyph or wildcard"); return NULL; } int8_t text_resources_get_glyph_horiz_advance(FontCache *font_cache, const Codepoint codepoint, FontInfo *font_info) { const GlyphData *g = prv_get_glyph(font_cache, codepoint, font_info, false /* need_bitmap */); if (!g) { return 0; } return g->header.horiz_advance; } const GlyphData *text_resources_get_glyph(FontCache *font_cache, const Codepoint codepoint, FontInfo *font_info) { return prv_get_glyph(font_cache, codepoint, font_info, true /* need_bitmap */); }