From 61626b91799b5f70ee6c84902e2844b9a486a863 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Wed, 7 Aug 2024 15:42:48 +0300 Subject: [PATCH] Introduce fonts v2 & add freetype font implementation --- libs/fonts/CMakeLists.txt | 9 +- libs/fonts/include/psemek/fonts/font_v2.hpp | 95 ++++++ libs/fonts/source/freetype.cpp | 346 ++++++++++++++++++++ 3 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 libs/fonts/include/psemek/fonts/font_v2.hpp create mode 100644 libs/fonts/source/freetype.cpp diff --git a/libs/fonts/CMakeLists.txt b/libs/fonts/CMakeLists.txt index 13d861e6..c81d806b 100644 --- a/libs/fonts/CMakeLists.txt +++ b/libs/fonts/CMakeLists.txt @@ -1,8 +1,13 @@ +find_package(Freetype REQUIRED) + file(GLOB_RECURSE PSEMEK_FONTS_HEADERS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "include/*.hpp") file(GLOB_RECURSE PSEMEK_FONTS_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "source/*.cpp") psemek_add_library(psemek-fonts ${PSEMEK_FONTS_HEADERS} ${PSEMEK_FONTS_SOURCES}) -target_include_directories(psemek-fonts PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(psemek-fonts PUBLIC psemek-util psemek-geom psemek-gfx rapidjson) +target_include_directories(psemek-fonts PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ${FREETYPE_INCLUDE_DIRS}) +target_link_libraries(psemek-fonts PUBLIC psemek-util psemek-geom psemek-gfx rapidjson ${FREETYPE_LIBRARY}) +if(PSEMEK_GRAPHICS_API STREQUAL WEBGPU) + target_link_libraries(psemek-fonts PUBLIC psemek-wgpu) +endif() psemek_glob_resources(psemek-fonts resources psemek/fonts/resources) diff --git a/libs/fonts/include/psemek/fonts/font_v2.hpp b/libs/fonts/include/psemek/fonts/font_v2.hpp new file mode 100644 index 00000000..1a570fc9 --- /dev/null +++ b/libs/fonts/include/psemek/fonts/font_v2.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include + +#if defined(PSEMEK_GRAPHICS_API_OPENGL) +#error "Fonts v2 don't support OpenGL yet" +#elif defined(PSEMEK_GRAPHICS_API_WEBGPU) +#include +#include +#endif + +#include + +#include +#include +#include +#include + +namespace psemek::fonts +{ + + enum class font_type + { + bitmap, + sdf, + msdf, + }; + +#if defined(PSEMEK_GRAPHICS_API_OPENGL) + // TODO +#elif defined(PSEMEK_GRAPHICS_API_WEBGPU) + using texture_type = wgpu::texture_view; +#endif + + struct shaped_glyph + { + texture_type const * texture; + geom::box position; + geom::box texcoords; + }; + + struct shape_options + { + enum direction_t + { + left_to_right, + right_to_left, + top_to_bottom, + bottom_to_top, + } direction = left_to_right; + + float scale = 1.f; + }; + + struct font + { + virtual font_type type() const = 0; + + virtual std::string_view name() const = 0; + + virtual geom::vector size() const = 0; + virtual int xheight() const = 0; + + virtual std::vector const & shape(std::string_view str, shape_options const & options, geom::point & pen) = 0; + virtual std::vector const & shape(std::u32string_view str, shape_options const & options, geom::point & pen) = 0; + + virtual std::vector const & shape(std::string_view str, shape_options const & options) + { + geom::point pen{0.f, 0.f}; + return shape(str, options, pen); + } + + virtual std::vector const & shape(std::u32string_view str, shape_options const & options) + { + geom::point pen{0.f, 0.f}; + return shape(str, options, pen); + } + + virtual ~font() {} + }; + + struct font_builder + { + virtual std::string_view name() const = 0; + + virtual std::unique_ptr create(font_type type, int size) = 0; + + virtual ~font_builder() {} + }; + + std::unique_ptr load_freetype_font(wgpu::device device, std::filesystem::path const & path); + +} diff --git a/libs/fonts/source/freetype.cpp b/libs/fonts/source/freetype.cpp new file mode 100644 index 00000000..c78a96fd --- /dev/null +++ b/libs/fonts/source/freetype.cpp @@ -0,0 +1,346 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include FT_FREETYPE_H + +#include + +namespace psemek::fonts +{ + + namespace + { + + void ft_check_result(FT_Error error, std::string const & message) + { + if (error != 0) + { + if (auto error_str = FT_Error_String(error)) + throw util::exception(message + error_str); + else + throw util::exception(message + util::to_string("0x", std::hex, error)); + } + } + + struct ft_initializer + { + ft_initializer() + { + ft_check_result(FT_Init_FreeType(&library_), "Failed to initialize Freetype library: "); + + int major, minor, patch; + FT_Library_Version(library_, &major, &minor, &patch); + log::info() << "Initialized Freetype " << major << '.' << minor << '.' << patch; + } + + ~ft_initializer() + { + if (auto error = FT_Done_FreeType(library_); error != 0) + { + if (auto error_str = FT_Error_String(error)) + log::warning() << "Failed to deinitialize Freetype library: " << error_str; + else + log::warning() << "Failed to deinitialize Freetype library: 0x" << std::hex << error; + } + } + + FT_Library library() const + { + return library_; + } + + private: + FT_Library library_; + }; + + FT_Library ft_library() + { + static ft_initializer initializer; + + return initializer.library(); + } + + struct face_shared + { + wgpu::device device; + FT_Face face; + + ~face_shared() + { + FT_Done_Face(face); + } + }; + + struct freetype_font + : font + { + freetype_font(std::shared_ptr state, font_type type, int size) + : state_(state) + , type_(type) + , size_(size) + { + font::shape("x", {}); + xheight_ = -shaped_text_[0].position[1].min; + shaped_text_.clear(); + } + + font_type type() const override + { + return type_; + } + + std::string_view name() const override + { + return FT_Get_Postscript_Name(state_->face); + } + + geom::vector size() const override + { + return {size_, size_}; + } + + int xheight() const override + { + return xheight_; + } + + std::vector const & shape(std::string_view str, shape_options const & options, geom::point & pen) override + { + return shape_impl(util::utf8_range(str), options, pen); + } + + std::vector const & shape(std::u32string_view str, shape_options const & options, geom::point & pen) override + { + return shape_impl(str, options, pen); + } + + private: + std::shared_ptr state_; + font_type type_; + int size_; + int xheight_; + + static constexpr int page_size = 256; + static constexpr int padding = 1; + + struct page + { + util::array pixmap; + + int current_row_start = 0; + int current_row_height = 0; + int current_row_x = 0; + + wgpu::texture texture; + std::unique_ptr texture_view; + bool needs_update = true; + }; + + std::vector pages_; + + struct glyph_data + { + int page; + geom::box part; + geom::vector offset; + geom::vector advance; + }; + + util::hash_map glyph_mapping_; + util::hash_map glyphs_; + + std::vector shaped_text_; + + template + std::vector const & shape_impl(String const & string, shape_options const & options, geom::point & pen) + { + (void)options; // TODO: support options + + shaped_text_.clear(); + + auto face = state_->face; + + ft_check_result(FT_Set_Char_Size(face, 0, size_ << 6, 0, 0), "Failed to select freetype face size: "); + + bool need_update_pages = false; + + for (char32_t ch : string) + { + std::uint32_t glyph_id; + + if (auto it = glyph_mapping_.find(ch); it != glyph_mapping_.end()) + { + glyph_id = it->second; + } + else + { + glyph_id = FT_Get_Char_Index(face, ch); + glyph_mapping_[ch] = glyph_id; + } + + glyph_data * data = nullptr; + + if (auto it = glyphs_.find(glyph_id); it != glyphs_.end()) + { + data = &(it->second); + } + else + { + data = &glyphs_[glyph_id]; + + FT_Load_Glyph(face, glyph_id, type_ == font_type::bitmap ? FT_LOAD_TARGET_LIGHT : FT_LOAD_DEFAULT); + FT_Render_Glyph(face->glyph, type_ == font_type::bitmap ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_SDF); + + if (face->glyph->bitmap.width + 2 * padding > page_size || face->glyph->bitmap.rows + 2 * padding > page_size) + { + data->page = 0; + data->part = {{{0, 0}, {0, 0}}}; + data->offset = {0, 0}; + data->advance = {size_, 0}; + log::warning() << "Glyph with ID " << glyph_id << " (U+" << std::hex << (int)ch << ") is larger than font atlas page: " << face->glyph->bitmap.width << "x" << face->glyph->bitmap.rows; + } + else + { + data->offset[0] = face->glyph->bitmap_left; + data->offset[1] = - face->glyph->bitmap_top; + data->advance[0] = face->glyph->advance.x >> 6; + data->advance[1] = face->glyph->advance.y >> 6; + + bool need_new_page = false; + + if (pages_.empty()) + { + need_new_page = true; + } + else + { + if (pages_.back().current_row_x + face->glyph->bitmap.width + 2 * padding > page_size) + { + pages_.back().current_row_start += pages_.back().current_row_height; + pages_.back().current_row_height = 0; + pages_.back().current_row_x = 0; + } + + if (pages_.back().current_row_start + face->glyph->bitmap.rows + 2 * padding > page_size) + { + need_new_page = true; + } + } + + if (need_new_page) + { + pages_.emplace_back(); + pages_.back().pixmap.resize({page_size, page_size}, 0); + + pages_.back().texture = state_->device.create_texture({ + .usage = wgpu::texture::usage::copy_dst | wgpu::texture::usage::texture_binding, + .dimension = wgpu::texture::dimension::_2d, + .size = {page_size, page_size, 1}, + .format = wgpu::texture::format::r8unorm, + }); + + pages_.back().texture_view = std::make_unique(pages_.back().texture.create_view(wgpu::texture_view::descriptor{ + .format = wgpu::texture::format::r8unorm, + .dimension = wgpu::texture_view::dimension::_2d, + })); + } + + data->page = pages_.size() - 1; + data->part[0].min = pages_.back().current_row_x + padding; + data->part[0].max = data->part[0].min + face->glyph->bitmap.width; + data->part[1].min = pages_.back().current_row_start + padding; + data->part[1].max = data->part[1].min + face->glyph->bitmap.rows; + + for (int y = 0; y < face->glyph->bitmap.rows; ++y) + { + auto src_begin = face->glyph->bitmap.buffer + y * face->glyph->bitmap.pitch; + auto src_end = src_begin + face->glyph->bitmap.width; + auto dst_begin = pages_.back().pixmap.data() + (pages_.back().current_row_start + y + padding) * page_size + pages_.back().current_row_x + padding; + + std::copy(src_begin, src_end, dst_begin); + } + + pages_.back().current_row_x += face->glyph->bitmap.width + 2 * padding; + geom::make_max(pages_.back().current_row_height, face->glyph->bitmap.rows + 2 * padding); + + pages_.back().needs_update = true; + need_update_pages = true; + } + } + + auto & result = shaped_text_.emplace_back(); + result.texture = pages_[data->page].texture_view.get(); + result.position[0].min = pen[0] + data->offset[0]; + result.position[0].max = result.position[0].min + data->part[0].length(); + result.position[1].min = pen[1] + data->offset[1]; + result.position[1].max = result.position[1].min + data->part[1].length(); + result.texcoords[0].min = static_cast(data->part[0].min) / page_size; + result.texcoords[0].max = static_cast(data->part[0].max) / page_size; + result.texcoords[1].min = static_cast(data->part[1].min) / page_size; + result.texcoords[1].max = static_cast(data->part[1].max) / page_size; + + pen += geom::cast(data->advance); + } + + if (need_update_pages) + { + for (auto & page : pages_) + { + if (!page.needs_update) continue; + page.needs_update = false; + + state_->device.get_queue().write_texture({.texture = page.texture}, {(char *)page.pixmap.data(), page.pixmap.size()}, {.bytes_per_row = page_size, .rows_per_image = page_size}, {page_size, page_size, 1}); + } + } + + return shaped_text_; + } + }; + + struct freetype_font_builder + : font_builder + { + freetype_font_builder(std::shared_ptr state) + : state_(state) + {} + + std::string_view name() const override + { + return FT_Get_Postscript_Name(state_->face); + } + + std::unique_ptr create(font_type type, int size) override + { + if (type == font_type::msdf) + throw util::exception("MSDF fonts are not supported by Freetype"); + return std::make_unique(state_, type, size); + } + + private: + std::shared_ptr state_; + }; + + } + + std::unique_ptr load_freetype_font(wgpu::device device, std::filesystem::path const & path) + { + FT_Face face; + ft_check_result(FT_New_Face(ft_library(), path.c_str(), 0, &face), "Failed to load font " + path.string() + ": "); + + auto result = std::make_unique(std::make_shared(device, face)); + + log::debug() << "Loaded font " << result->name() << " (" << path << ")"; + + return result; + } + +}