Introduce fonts v2 & add freetype font implementation

This commit is contained in:
Nikita Lisitsa 2024-08-07 15:42:48 +03:00
parent a4d666096e
commit 61626b9179
3 changed files with 448 additions and 2 deletions

View file

@ -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)

View file

@ -0,0 +1,95 @@
#pragma once
#include <psemek/geom/vector.hpp>
#include <psemek/geom/box.hpp>
#include <psemek/util/span.hpp>
#if defined(PSEMEK_GRAPHICS_API_OPENGL)
#error "Fonts v2 don't support OpenGL yet"
#elif defined(PSEMEK_GRAPHICS_API_WEBGPU)
#include <psemek/wgpu/device.hpp>
#include <psemek/wgpu/texture_view.hpp>
#endif
#include <psemek/io/stream.hpp>
#include <string_view>
#include <vector>
#include <memory>
#include <filesystem>
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<float, 2> position;
geom::box<float, 2> 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<int, 2> size() const = 0;
virtual int xheight() const = 0;
virtual std::vector<shaped_glyph> const & shape(std::string_view str, shape_options const & options, geom::point<float, 2> & pen) = 0;
virtual std::vector<shaped_glyph> const & shape(std::u32string_view str, shape_options const & options, geom::point<float, 2> & pen) = 0;
virtual std::vector<shaped_glyph> const & shape(std::string_view str, shape_options const & options)
{
geom::point<float, 2> pen{0.f, 0.f};
return shape(str, options, pen);
}
virtual std::vector<shaped_glyph> const & shape(std::u32string_view str, shape_options const & options)
{
geom::point<float, 2> 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<font> create(font_type type, int size) = 0;
virtual ~font_builder() {}
};
std::unique_ptr<font_builder> load_freetype_font(wgpu::device device, std::filesystem::path const & path);
}

View file

@ -0,0 +1,346 @@
#include <psemek/fonts/font_v2.hpp>
#include <psemek/gfx/pixmap.hpp>
#include <psemek/util/exception.hpp>
#include <psemek/util/to_string.hpp>
#include <psemek/util/unicode.hpp>
#include <psemek/util/hash_table.hpp>
#include <psemek/log/log.hpp>
#include <psemek/io/file_stream.hpp>
#include <ft2build.h>
#include FT_FREETYPE_H
#include <iomanip>
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<face_shared> 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<int, 2> size() const override
{
return {size_, size_};
}
int xheight() const override
{
return xheight_;
}
std::vector<shaped_glyph> const & shape(std::string_view str, shape_options const & options, geom::point<float, 2> & pen) override
{
return shape_impl(util::utf8_range(str), options, pen);
}
std::vector<shaped_glyph> const & shape(std::u32string_view str, shape_options const & options, geom::point<float, 2> & pen) override
{
return shape_impl(str, options, pen);
}
private:
std::shared_ptr<face_shared> state_;
font_type type_;
int size_;
int xheight_;
static constexpr int page_size = 256;
static constexpr int padding = 1;
struct page
{
util::array<std::uint8_t, 2> pixmap;
int current_row_start = 0;
int current_row_height = 0;
int current_row_x = 0;
wgpu::texture texture;
std::unique_ptr<wgpu::texture_view> texture_view;
bool needs_update = true;
};
std::vector<page> pages_;
struct glyph_data
{
int page;
geom::box<int, 2> part;
geom::vector<int, 2> offset;
geom::vector<int, 2> advance;
};
util::hash_map<char32_t, std::uint32_t> glyph_mapping_;
util::hash_map<std::uint32_t, glyph_data> glyphs_;
std::vector<shaped_glyph> shaped_text_;
template <typename String>
std::vector<shaped_glyph> const & shape_impl(String const & string, shape_options const & options, geom::point<float, 2> & 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<wgpu::texture_view>(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<int>(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<float>(data->part[0].min) / page_size;
result.texcoords[0].max = static_cast<float>(data->part[0].max) / page_size;
result.texcoords[1].min = static_cast<float>(data->part[1].min) / page_size;
result.texcoords[1].max = static_cast<float>(data->part[1].max) / page_size;
pen += geom::cast<float>(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<face_shared> state)
: state_(state)
{}
std::string_view name() const override
{
return FT_Get_Postscript_Name(state_->face);
}
std::unique_ptr<font> 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<freetype_font>(state_, type, size);
}
private:
std::shared_ptr<face_shared> state_;
};
}
std::unique_ptr<font_builder> 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<freetype_font_builder>(std::make_shared<face_shared>(device, face));
log::debug() << "Loaded font " << result->name() << " (" << path << ")";
return result;
}
}