From f1f6b0ee7967e2ca2dceb17f7a2a81f7df7904a1 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 6 Sep 2026 15:11:34 +1000 Subject: [PATCH] ImGuiManager: Load FreeType/plutosvg dynamically --- dep/imgui/CMakeLists.txt | 12 +++- dep/imgui/imgui.vcxproj | 2 + dep/imgui/imgui.vcxproj.filters | 2 + dep/imgui/include/dyn_freetype.h | 47 +++++++++++++++ dep/imgui/include/dyn_plutosvg.h | 32 ++++++++++ dep/imgui/src/imgui_freetype.cpp | 53 +++++++++-------- scripts/appimage/make-appimage.sh | 1 + src/duckstation-qt/svgiconengine.cpp | 52 +++++++++------- src/duckstation-qt/svgwidget.cpp | 52 ++++++++-------- src/util/CMakeLists.txt | 17 ++++-- src/util/dyn_libs.cpp | 88 ++++++++++++++++++++++++++++ src/util/image.cpp | 24 +++++--- src/util/imgui_manager.cpp | 18 ++++-- src/util/util.props | 2 +- src/util/util.vcxproj | 3 +- src/util/util.vcxproj.filters | 1 + 16 files changed, 311 insertions(+), 95 deletions(-) create mode 100644 dep/imgui/include/dyn_freetype.h create mode 100644 dep/imgui/include/dyn_plutosvg.h create mode 100644 src/util/dyn_libs.cpp diff --git a/dep/imgui/CMakeLists.txt b/dep/imgui/CMakeLists.txt index 4b2a82307..66e5c5fc5 100644 --- a/dep/imgui/CMakeLists.txt +++ b/dep/imgui/CMakeLists.txt @@ -1,4 +1,6 @@ set(SRCS + include/dyn_freetype.h + include/dyn_plutosvg.h include/imconfig.h include/imgui.h include/imgui_freetype.h @@ -17,6 +19,12 @@ set(SRCS ) add_library(imgui ${SRCS}) -target_include_directories(imgui PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/src") +get_target_property(FREETYPE_INCLUDE_DIRS freetype INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(PLUTOSVG_INCLUDE_DIRS plutosvg::plutosvg INTERFACE_INCLUDE_DIRECTORIES) +target_include_directories(imgui PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/src" + "${FREETYPE_INCLUDE_DIRS}" + "${PLUTOSVG_INCLUDE_DIRS}" +) target_include_directories(imgui INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(imgui PUBLIC Freetype::Freetype plutosvg::plutosvg) diff --git a/dep/imgui/imgui.vcxproj b/dep/imgui/imgui.vcxproj index 80c431854..af4474b87 100644 --- a/dep/imgui/imgui.vcxproj +++ b/dep/imgui/imgui.vcxproj @@ -2,6 +2,8 @@ + + diff --git a/dep/imgui/imgui.vcxproj.filters b/dep/imgui/imgui.vcxproj.filters index 23791179e..3213cc272 100644 --- a/dep/imgui/imgui.vcxproj.filters +++ b/dep/imgui/imgui.vcxproj.filters @@ -9,6 +9,8 @@ + + diff --git a/dep/imgui/include/dyn_freetype.h b/dep/imgui/include/dyn_freetype.h new file mode 100644 index 000000000..fdf8002a9 --- /dev/null +++ b/dep/imgui/include/dyn_freetype.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include + +#include FT_FREETYPE_H // +#include FT_GLYPH_H // +#include FT_MULTIPLE_MASTERS_H // +#include FT_MODULE_H // +#include FT_SIZES_H // +#include FT_SYNTHESIS_H // + +class Error; + +#define DYN_FREETYPE_FUNCTIONS(X) \ + X(FT_New_Memory_Face) \ + X(FT_Select_Charmap) \ + X(FT_Get_MM_Var) \ + X(FT_Done_MM_Var) \ + X(FT_Done_Face) \ + X(FT_Get_Char_Index) \ + X(FT_Load_Glyph) \ + X(FT_GlyphSlot_Embolden) \ + X(FT_GlyphSlot_Oblique) \ + X(FT_New_Library) \ + X(FT_Add_Default_Modules) \ + X(FT_Property_Set) \ + X(FT_Done_Library) \ + X(FT_New_Size) \ + X(FT_Activate_Size) \ + X(FT_Set_Var_Design_Coordinates) \ + X(FT_Request_Size) \ + X(FT_Done_Size) \ + X(FT_Render_Glyph) + +struct DynFreeType +{ +#define ADD_FUNC(F) decltype(&::F) F; + DYN_FREETYPE_FUNCTIONS(ADD_FUNC) +#undef ADD_FUNC + + bool Open(Error* const error); +}; + +extern DynFreeType g_dyn_freetype; diff --git a/dep/imgui/include/dyn_plutosvg.h b/dep/imgui/include/dyn_plutosvg.h new file mode 100644 index 000000000..753354fa4 --- /dev/null +++ b/dep/imgui/include/dyn_plutosvg.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include + +class Error; + +#define DYN_PLUTOSVG_FUNCTIONS(X) \ + X(plutosvg_ft_svg_hooks) \ + X(plutosvg_document_load_from_data) \ + X(plutosvg_document_destroy) \ + X(plutosvg_document_get_width) \ + X(plutosvg_document_get_height) \ + X(plutosvg_document_render_to_surface) \ + X(plutovg_surface_destroy) \ + X(plutovg_surface_get_width) \ + X(plutovg_surface_get_height) \ + X(plutovg_surface_get_data) \ + X(plutovg_surface_get_stride) + +struct DynPlutoSvg +{ +#define ADD_FUNC(F) decltype(&::F) F; + DYN_PLUTOSVG_FUNCTIONS(ADD_FUNC) +#undef ADD_FUNC + + bool Open(Error* const error = nullptr); +}; + +extern DynPlutoSvg g_dyn_plutosvg; diff --git a/dep/imgui/src/imgui_freetype.cpp b/dep/imgui/src/imgui_freetype.cpp index e4e79e16b..3409682ed 100644 --- a/dep/imgui/src/imgui_freetype.cpp +++ b/dep/imgui/src/imgui_freetype.cpp @@ -41,6 +41,7 @@ #ifndef IMGUI_DISABLE #include "imgui_freetype.h" #include "imgui_internal.h" // ImMin,ImMax,ImFontAtlasBuild*, +#include "dyn_freetype.h" #include #include #include FT_FREETYPE_H // @@ -58,7 +59,7 @@ #error Lunasvg is not supported #endif #ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG -#include +#include "dyn_plutosvg.h" #endif #if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined (IMGUI_ENABLE_FREETYPE_PLUTOSVG) #if !((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12)) @@ -170,16 +171,16 @@ struct ImGui_ImplFreeType_FontSrcBakedData bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_font_loader_flags) { - FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)src->FontData, (FT_Long)src->FontDataSize, (FT_Long)src->FontNo, &FtFace); + FT_Error error = g_dyn_freetype.FT_New_Memory_Face(ft_library, (const FT_Byte*)src->FontData, (FT_Long)src->FontDataSize, (FT_Long)src->FontNo, &FtFace); if (error != 0) return false; - error = FT_Select_Charmap(FtFace, FT_ENCODING_UNICODE); + error = g_dyn_freetype.FT_Select_Charmap(FtFace, FT_ENCODING_UNICODE); if (error != 0) return false; FT_MM_Var* mmvar; WeightCoordIndex = -1; - if (FT_IS_SFNT(FtFace) && ((error = FT_Get_MM_Var(FtFace, &mmvar)) == 0)) + if (FT_IS_SFNT(FtFace) && ((error = g_dyn_freetype.FT_Get_MM_Var(FtFace, &mmvar)) == 0)) { VarDesignCoords = new FT_Fixed[mmvar->num_axis]; VarDesignNumAxis = mmvar->num_axis; @@ -199,7 +200,7 @@ bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFon VarDesignNumAxis = 0; } - FT_Done_MM_Var(ft_library, mmvar); + g_dyn_freetype.FT_Done_MM_Var(ft_library, mmvar); } // Convert to FreeType flags (NB: Bold and Oblique are processed separately) @@ -239,14 +240,14 @@ void ImGui_ImplFreeType_FontSrcData::CloseFont() if (FtFace) { - FT_Done_Face(FtFace); + g_dyn_freetype.FT_Done_Face(FtFace); FtFace = nullptr; } } static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_FontSrcData* src_data, uint32_t codepoint) { - uint32_t glyph_index = FT_Get_Char_Index(src_data->FtFace, codepoint); + uint32_t glyph_index = g_dyn_freetype.FT_Get_Char_Index(src_data->FtFace, codepoint); if (glyph_index == 0) return nullptr; @@ -255,7 +256,7 @@ static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_F // - https://github.com/ocornut/imgui/issues/4567 // - https://github.com/ocornut/imgui/issues/4566 // You can use FreeType 2.10, or the patched version of 2.11.0 in VcPkg, or probably any upcoming FreeType version. - FT_Error error = FT_Load_Glyph(src_data->FtFace, glyph_index, src_data->LoadFlags); + FT_Error error = g_dyn_freetype.FT_Load_Glyph(src_data->FtFace, glyph_index, src_data->LoadFlags); if (error) return nullptr; @@ -272,10 +273,10 @@ static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_F // Apply convenience transform (this is not picking from real "Bold"/"Italic" fonts! Merely applying FreeType helper transform. Oblique == Slanting) if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bold) - FT_GlyphSlot_Embolden(slot); + g_dyn_freetype.FT_GlyphSlot_Embolden(slot); if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Oblique) { - FT_GlyphSlot_Oblique(slot); + g_dyn_freetype.FT_GlyphSlot_Oblique(slot); //FT_BBox bbox; //FT_Outline_Get_BBox(&slot->outline, &bbox); //slot->metrics.width = bbox.xMax - bbox.xMin; @@ -381,7 +382,7 @@ static bool ImGui_ImplFreeType_LoaderInit(ImFontAtlas* atlas) bd->MemoryManager.realloc = &FreeType_Realloc; // https://www.freetype.org/freetype2/docs/reference/ft2-module_management.html#FT_New_Library - FT_Error error = FT_New_Library(&bd->MemoryManager, &bd->Library); + FT_Error error = g_dyn_freetype.FT_New_Library(&bd->MemoryManager, &bd->Library); if (error != 0) { IM_DELETE(bd); @@ -389,11 +390,11 @@ static bool ImGui_ImplFreeType_LoaderInit(ImFontAtlas* atlas) } // If you don't call FT_Add_Default_Modules() the rest of code may work, but FreeType won't use our custom allocator. - FT_Add_Default_Modules(bd->Library); + g_dyn_freetype.FT_Add_Default_Modules(bd->Library); #ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG // With plutosvg, use provided hooks - FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", plutosvg_ft_svg_hooks()); + g_dyn_freetype.FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", g_dyn_plutosvg.plutosvg_ft_svg_hooks()); #endif // IMGUI_ENABLE_FREETYPE_PLUTOSVG // Store our data @@ -406,7 +407,7 @@ static void ImGui_ImplFreeType_LoaderShutdown(ImFontAtlas* atlas) { ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData; IM_ASSERT(bd != nullptr); - FT_Done_Library(bd->Library); + g_dyn_freetype.FT_Done_Library(bd->Library); IM_DELETE(bd); atlas->FontLoaderData = nullptr; } @@ -451,8 +452,8 @@ static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* s IM_ASSERT(bd_baked_data != nullptr); IM_PLACEMENT_NEW(bd_baked_data) ImGui_ImplFreeType_FontSrcBakedData(); - FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize); - FT_Activate_Size(bd_baked_data->FtSize); + g_dyn_freetype.FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize); + g_dyn_freetype.FT_Activate_Size(bd_baked_data->FtSize); bd_baked_data->FtWeight = 0; if (bd_font_data->WeightCoordIndex >= 0) { @@ -460,11 +461,11 @@ static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* s if (bd_baked_data->FtWeight != 0) { bd_font_data->VarDesignCoords[bd_font_data->WeightCoordIndex] = bd_baked_data->FtWeight; - FT_Set_Var_Design_Coordinates(bd_font_data->FtFace, bd_font_data->VarDesignNumAxis, bd_font_data->VarDesignCoords); + g_dyn_freetype.FT_Set_Var_Design_Coordinates(bd_font_data->FtFace, bd_font_data->VarDesignNumAxis, bd_font_data->VarDesignCoords); } else { - FT_Set_Var_Design_Coordinates(bd_font_data->FtFace, 0, nullptr); + g_dyn_freetype.FT_Set_Var_Design_Coordinates(bd_font_data->FtFace, 0, nullptr); } } @@ -483,7 +484,7 @@ static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* s req.height = (uint32_t)(size * 64 * rasterizer_density); req.horiResolution = 0; req.vertResolution = 0; - FT_Request_Size(bd_font_data->FtFace, &req); + g_dyn_freetype.FT_Request_Size(bd_font_data->FtFace, &req); // Output if (src->MergeMode == false) @@ -509,14 +510,14 @@ static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src; IM_ASSERT(bd_baked_data != nullptr); bd_font_data->LastSize = (bd_font_data->LastSize == bd_baked_data->FtSize) ? nullptr : bd_font_data->LastSize; - FT_Done_Size(bd_baked_data->FtSize); + g_dyn_freetype.FT_Done_Size(bd_baked_data->FtSize); bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE() } static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x) { ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData; - uint32_t glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint); + uint32_t glyph_index = g_dyn_freetype.FT_Get_Char_Index(bd_font_data->FtFace, codepoint); if (glyph_index == 0) return false; @@ -527,7 +528,7 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf if ( bd_font_data->LastSize != bd_baked_data->FtSize) { bd_font_data->LastSize = bd_baked_data->FtSize; - FT_Activate_Size(bd_baked_data->FtSize); + g_dyn_freetype.FT_Activate_Size(bd_baked_data->FtSize); } if (bd_font_data->WeightCoordIndex >= 0 && bd_font_data->LastWeight != bd_baked_data->FtWeight) { @@ -535,11 +536,11 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf if (bd_baked_data->FtWeight != 0) { bd_font_data->VarDesignCoords[bd_font_data->WeightCoordIndex] = bd_baked_data->FtWeight; - FT_Set_Var_Design_Coordinates(bd_font_data->FtFace, bd_font_data->VarDesignNumAxis, bd_font_data->VarDesignCoords); + g_dyn_freetype.FT_Set_Var_Design_Coordinates(bd_font_data->FtFace, bd_font_data->VarDesignNumAxis, bd_font_data->VarDesignCoords); } else { - FT_Set_Var_Design_Coordinates(bd_font_data->FtFace, 0, nullptr); + g_dyn_freetype.FT_Set_Var_Design_Coordinates(bd_font_data->FtFace, 0, nullptr); } } @@ -566,7 +567,7 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf // Render glyph into a bitmap (currently held by FreeType) FT_Render_Mode render_mode = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Monochrome) ? FT_RENDER_MODE_MONO : FT_RENDER_MODE_NORMAL; - error = FT_Render_Glyph(slot, render_mode); + error = g_dyn_freetype.FT_Render_Glyph(slot, render_mode); const FT_Bitmap* ft_bitmap = &slot->bitmap; if (error != 0 || ft_bitmap == nullptr) return false; @@ -623,7 +624,7 @@ static bool ImGui_ImplFreetype_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontCo { IM_UNUSED(atlas); ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData; - int glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint); + int glyph_index = g_dyn_freetype.FT_Get_Char_Index(bd_font_data->FtFace, codepoint); return glyph_index != 0; } diff --git a/scripts/appimage/make-appimage.sh b/scripts/appimage/make-appimage.sh index 3585536a1..1cd3607c1 100755 --- a/scripts/appimage/make-appimage.sh +++ b/scripts/appimage/make-appimage.sh @@ -45,6 +45,7 @@ declare -a MANUAL_LIBS=( "libz.so.1" "libdiscord-rpc.so" + "libplutosvg.so.0" "libshaderc_shared.so" "libspirv-cross-c-shared.so.0" "libsqlite3.so.3" diff --git a/src/duckstation-qt/svgiconengine.cpp b/src/duckstation-qt/svgiconengine.cpp index d277dcbac..b651e261a 100644 --- a/src/duckstation-qt/svgiconengine.cpp +++ b/src/duckstation-qt/svgiconengine.cpp @@ -18,8 +18,8 @@ #include #include +#include #include -#include #include "moc_svgiconengine.cpp" @@ -55,7 +55,7 @@ static QString BuildCacheKey(const QString& resource_path, const QSize& size, qr /// Callback used when freeing the QImage. static void CleanupPlutoSVGSurface(void* surface) { - plutovg_surface_destroy(static_cast(surface)); + g_dyn_plutosvg.plutovg_surface_destroy(static_cast(surface)); } /// Renders the document at the given pixel dimensions with the given colour and inserts the @@ -75,13 +75,13 @@ static bool RenderSVGToPixmap(QPixmap& pm, const plutosvg_document* doc, const Q // Determine SVG intrinsic size and compute a uniform scale that fits inside physical, // preserving aspect ratio. - const float svg_w = plutosvg_document_get_width(doc); - const float svg_h = plutosvg_document_get_height(doc); + const float svg_w = g_dyn_plutosvg.plutosvg_document_get_width(doc); + const float svg_h = g_dyn_plutosvg.plutosvg_document_get_height(doc); QSize render_size = size; if (svg_w > 0.0f && svg_h > 0.0f) render_size = QSizeF(svg_w, svg_h).scaled(size, Qt::KeepAspectRatio).toSize(); - plutovg_surface_t* surface = plutosvg_document_render_to_surface( + plutovg_surface_t* surface = g_dyn_plutosvg.plutosvg_document_render_to_surface( doc, nullptr, render_size.width(), render_size.height(), ¤t_color, nullptr, nullptr); if (!surface) return false; @@ -89,9 +89,10 @@ static bool RenderSVGToPixmap(QPixmap& pm, const plutosvg_document* doc, const Q // plutovg surfaces are premultiplied ARGB (0xAARRGGBB, native-endian), which matches // QImage::Format_ARGB32_Premultiplied exactly, no pixel conversion required. // The cleanup function ensures the surface is destroyed when the QImage is done with the pixel data. - const QImage img(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface), - plutovg_surface_get_height(surface), plutovg_surface_get_stride(surface), - QImage::Format_ARGB32_Premultiplied, CleanupPlutoSVGSurface, surface); + const QImage img(g_dyn_plutosvg.plutovg_surface_get_data(surface), g_dyn_plutosvg.plutovg_surface_get_width(surface), + g_dyn_plutosvg.plutovg_surface_get_height(surface), + g_dyn_plutosvg.plutovg_surface_get_stride(surface), QImage::Format_ARGB32_Premultiplied, + CleanupPlutoSVGSurface, surface); pm = QPixmap::fromImage(img); if (pm.isNull()) @@ -125,7 +126,7 @@ SVGIconEngine::SVGIconEngine(const QString& resource_path) : m_resource_path(res SVGIconEngine::~SVGIconEngine() { if (m_document) - plutosvg_document_destroy(m_document); + g_dyn_plutosvg.plutosvg_document_destroy(m_document); } bool SVGIconEngine::ensureLoaded() const @@ -146,8 +147,9 @@ bool SVGIconEngine::ensureLoaded() const } // The document borrows the data pointer; m_svg_data must remain valid for the document's lifetime. - m_document = plutosvg_document_load_from_data(reinterpret_cast(m_svg_data.data()), - static_cast(m_svg_data.size()), -1.0f, -1.0f, nullptr, nullptr); + m_document = g_dyn_plutosvg.plutosvg_document_load_from_data(reinterpret_cast(m_svg_data.data()), + static_cast(m_svg_data.size()), -1.0f, -1.0f, + nullptr, nullptr); if (!m_document) { qCritical() << "Failed to parse SVG data: " << m_resource_path; @@ -238,6 +240,9 @@ QString SVGIconEngine::iconName() QIconEngine* SVGIconEnginePlugin::create(const QString& resource_path) { + if (!g_dyn_plutosvg.Open()) [[unlikely]] + return nullptr; + if (resource_path.endsWith(".svg", Qt::CaseInsensitive)) return new SVGIconEngine(resource_path); @@ -249,7 +254,7 @@ SVGImageHandler::SVGImageHandler() = default; SVGImageHandler::~SVGImageHandler() { if (m_document) - plutosvg_document_destroy(m_document); + g_dyn_plutosvg.plutosvg_document_destroy(m_document); } bool SVGImageHandler::canRead() const @@ -265,8 +270,9 @@ bool SVGImageHandler::canRead() const return false; } - m_document = plutosvg_document_load_from_data(reinterpret_cast(m_svg_data.data()), - static_cast(m_svg_data.size()), -1.0f, -1.0f, nullptr, nullptr); + m_document = g_dyn_plutosvg.plutosvg_document_load_from_data(reinterpret_cast(m_svg_data.data()), + static_cast(m_svg_data.size()), -1.0f, -1.0f, + nullptr, nullptr); if (!m_document) { qCritical() << "Failed to parse SVG data"; @@ -286,15 +292,15 @@ bool SVGImageHandler::read(QImage* image) QSize render_size = m_scaled_size; if (!render_size.isValid()) { - render_size = QSize(qMax(1, qRound(plutosvg_document_get_width(m_document))), - qMax(1, qRound(plutosvg_document_get_height(m_document)))); + render_size = QSize(qMax(1, qRound(g_dyn_plutosvg.plutosvg_document_get_width(m_document))), + qMax(1, qRound(g_dyn_plutosvg.plutosvg_document_get_height(m_document)))); } // Render with a solid currentColor so the SVG's own fill/stroke colours are preserved. // Callers that want palette tinting should use ThemeSVGIconEngine instead. const plutovg_color_t current_color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f}; - plutovg_surface_t* surface = plutosvg_document_render_to_surface( + plutovg_surface_t* surface = g_dyn_plutosvg.plutosvg_document_render_to_surface( m_document, nullptr, render_size.width(), render_size.height(), ¤t_color, nullptr, nullptr); if (!surface) return false; @@ -302,8 +308,9 @@ bool SVGImageHandler::read(QImage* image) // Wrap the surface pixels in a QImage; the cleanup function destroys the surface once Qt is done. // plutovg uses premultiplied ARGB (0xAARRGGBB, native-endian) == QImage::Format_ARGB32_Premultiplied. *image = - QImage(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface), plutovg_surface_get_height(surface), - plutovg_surface_get_stride(surface), QImage::Format_ARGB32_Premultiplied, CleanupPlutoSVGSurface, surface); + QImage(g_dyn_plutosvg.plutovg_surface_get_data(surface), g_dyn_plutosvg.plutovg_surface_get_width(surface), + g_dyn_plutosvg.plutovg_surface_get_height(surface), g_dyn_plutosvg.plutovg_surface_get_stride(surface), + QImage::Format_ARGB32_Premultiplied, CleanupPlutoSVGSurface, surface); return !image->isNull(); } @@ -323,8 +330,8 @@ QVariant SVGImageHandler::option(ImageOption option) const if (!canRead()) return {}; - return QSize(qMax(1, qRound(plutosvg_document_get_width(m_document))), - qMax(1, qRound(plutosvg_document_get_height(m_document)))); + return QSize(qMax(1, qRound(g_dyn_plutosvg.plutosvg_document_get_width(m_document))), + qMax(1, qRound(g_dyn_plutosvg.plutosvg_document_get_height(m_document)))); } return {}; @@ -354,6 +361,9 @@ QImageIOPlugin::Capabilities SVGImageHandlerPlugin::capabilities(QIODevice* devi QImageIOHandler* SVGImageHandlerPlugin::create(QIODevice* device, const QByteArray& format) const { + if (!g_dyn_plutosvg.Open()) [[unlikely]] + return nullptr; + SVGImageHandler* const handler = new SVGImageHandler(); handler->setDevice(device); handler->setFormat(format.isEmpty() ? QByteArray("svg") : format); diff --git a/src/duckstation-qt/svgwidget.cpp b/src/duckstation-qt/svgwidget.cpp index e93a0b49d..6dbbecb7b 100644 --- a/src/duckstation-qt/svgwidget.cpp +++ b/src/duckstation-qt/svgwidget.cpp @@ -9,14 +9,14 @@ #include #include +#include #include -#include #include "moc_svgwidget.cpp" static void CleanupSurface(void* surface) { - plutovg_surface_destroy(static_cast(surface)); + g_dyn_plutosvg.plutovg_surface_destroy(static_cast(surface)); } SVGWidget::SVGWidget(QWidget* parent) : QWidget(parent) @@ -48,7 +48,7 @@ void SVGWidget::setSource(const QString& resource_path) m_pixmap = {}; m_last_raster_size = {}; - if (resource_path.isEmpty()) + if (resource_path.isEmpty() || !g_dyn_plutosvg.Open()) { update(); return; @@ -63,8 +63,9 @@ void SVGWidget::setSource(const QString& resource_path) } // plutosvg borrows the raw pointer; m_svg_data must outlive m_document. - m_document = plutosvg_document_load_from_data(reinterpret_cast(m_svg_data.data()), - static_cast(m_svg_data.size()), -1.0f, -1.0f, nullptr, nullptr); + m_document = g_dyn_plutosvg.plutosvg_document_load_from_data(reinterpret_cast(m_svg_data.data()), + static_cast(m_svg_data.size()), -1.0f, -1.0f, + nullptr, nullptr); if (!m_document) { qCritical() << "PlutoSVGWidget: failed to parse SVG" << resource_path; @@ -82,7 +83,7 @@ void SVGWidget::destroyDocument() { if (m_document) { - plutosvg_document_destroy(m_document); + g_dyn_plutosvg.plutosvg_document_destroy(m_document); m_document = nullptr; } m_svg_data.deallocate(); @@ -113,8 +114,8 @@ void SVGWidget::rasterize() // Determine SVG intrinsic size and compute a uniform scale that fits inside physical, // preserving aspect ratio. - const float svg_w = plutosvg_document_get_width(m_document); - const float svg_h = plutosvg_document_get_height(m_document); + const float svg_w = g_dyn_plutosvg.plutosvg_document_get_width(m_document); + const float svg_h = g_dyn_plutosvg.plutosvg_document_get_height(m_document); int render_w = physical.width(); int render_h = physical.height(); @@ -139,8 +140,8 @@ void SVGWidget::rasterize() .a = static_cast(color.alphaF()), }; - plutovg_surface_t* const surface = - plutosvg_document_render_to_surface(m_document, nullptr, render_w, render_h, ¤t_color, nullptr, nullptr); + plutovg_surface_t* const surface = g_dyn_plutosvg.plutosvg_document_render_to_surface( + m_document, nullptr, render_w, render_h, ¤t_color, nullptr, nullptr); if (!surface) { qCritical() << "PlutoSVGWidget: render failed for" << m_resource_path; @@ -148,9 +149,10 @@ void SVGWidget::rasterize() } // plutovg surfaces are premultiplied ARGB (native-endian) == QImage::Format_ARGB32_Premultiplied. - const QImage img(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface), - plutovg_surface_get_height(surface), plutovg_surface_get_stride(surface), - QImage::Format_ARGB32_Premultiplied, CleanupSurface, surface); + const QImage img(g_dyn_plutosvg.plutovg_surface_get_data(surface), g_dyn_plutosvg.plutovg_surface_get_width(surface), + g_dyn_plutosvg.plutovg_surface_get_height(surface), + g_dyn_plutosvg.plutovg_surface_get_stride(surface), QImage::Format_ARGB32_Premultiplied, + CleanupSurface, surface); m_pixmap = QPixmap::fromImage(img); m_pixmap.setDevicePixelRatio(dpr); @@ -197,6 +199,9 @@ void SVGWidget::changeEvent(QEvent* event) QPixmap SVGWidget::renderSVGToPixmap(const QString& resource_path, const QSize& size, qreal device_pixel_ratio, const QColor& color) { + if (!g_dyn_plutosvg.Open()) + return {}; + DynamicHeapArray svg_data; QFile file(resource_path); if (!file.open(QFile::ReadOnly) || !QtUtils::ReadFileToByteArray(&file, svg_data)) @@ -206,7 +211,7 @@ QPixmap SVGWidget::renderSVGToPixmap(const QString& resource_path, const QSize& } // plutosvg borrows the raw pointer; svg_data must outlive m_document. - plutosvg_document* const document = plutosvg_document_load_from_data( + plutosvg_document* const document = g_dyn_plutosvg.plutosvg_document_load_from_data( reinterpret_cast(svg_data.data()), static_cast(svg_data.size()), -1.0f, -1.0f, nullptr, nullptr); if (!document) { @@ -216,8 +221,8 @@ QPixmap SVGWidget::renderSVGToPixmap(const QString& resource_path, const QSize& // Determine SVG intrinsic size and compute a uniform scale that fits inside physical, // preserving aspect ratio. - const float svg_w = plutosvg_document_get_width(document); - const float svg_h = plutosvg_document_get_height(document); + const float svg_w = g_dyn_plutosvg.plutosvg_document_get_width(document); + const float svg_h = g_dyn_plutosvg.plutosvg_document_get_height(document); const QSize physical = QtUtils::ApplyDevicePixelRatioToSize(size, device_pixel_ratio); int render_w = physical.width(); @@ -237,22 +242,23 @@ QPixmap SVGWidget::renderSVGToPixmap(const QString& resource_path, const QSize& // Swap in a palette colour here if tinting is ever desired. const plutovg_color_t current_color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f}; - plutovg_surface_t* const surface = - plutosvg_document_render_to_surface(document, nullptr, render_w, render_h, ¤t_color, nullptr, nullptr); + plutovg_surface_t* const surface = g_dyn_plutosvg.plutosvg_document_render_to_surface( + document, nullptr, render_w, render_h, ¤t_color, nullptr, nullptr); if (!surface) { qCritical() << "PlutoSVGWidget: render failed for" << resource_path; - plutosvg_document_destroy(document); + g_dyn_plutosvg.plutosvg_document_destroy(document); return {}; } // plutovg surfaces are premultiplied ARGB (native-endian) == QImage::Format_ARGB32_Premultiplied. - const QImage img(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface), - plutovg_surface_get_height(surface), plutovg_surface_get_stride(surface), - QImage::Format_ARGB32_Premultiplied, CleanupSurface, surface); + const QImage img(g_dyn_plutosvg.plutovg_surface_get_data(surface), g_dyn_plutosvg.plutovg_surface_get_width(surface), + g_dyn_plutosvg.plutovg_surface_get_height(surface), + g_dyn_plutosvg.plutovg_surface_get_stride(surface), QImage::Format_ARGB32_Premultiplied, + CleanupSurface, surface); QPixmap pm = QPixmap::fromImage(img); pm.setDevicePixelRatio(device_pixel_ratio); - plutosvg_document_destroy(document); + g_dyn_plutosvg.plutosvg_document_destroy(document); return pm; } diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 12feb5c7a..810f6d77f 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(util dyn_shaderc.h dyn_spirv_cross.h dyn_libpng.h + dyn_libs.cpp dyn_sdl.h dyn_sqlite.h elf_file.cpp @@ -203,9 +204,16 @@ if(ENABLE_VULKAN) endif() # shaderc/spirv-cross is loaded dynamically to reduce module loads on startup. +get_target_property(FREETYPE_INCLUDE_DIRS freetype INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(PLUTOSVG_INCLUDE_DIRS plutosvg::plutosvg INTERFACE_INCLUDE_DIRECTORIES) get_target_property(SHADERC_INCLUDE_DIR Shaderc::shaderc_shared INTERFACE_INCLUDE_DIRECTORIES) get_target_property(SPIRV_CROSS_INCLUDE_DIR spirv-cross-c-shared INTERFACE_INCLUDE_DIRECTORIES) -target_include_directories(util PUBLIC ${SHADERC_INCLUDE_DIR} ${SPIRV_CROSS_INCLUDE_DIR}) +target_include_directories(util PUBLIC + ${FREETYPE_INCLUDE_DIRS} + ${PLUTOSVG_INCLUDE_DIRS} + ${SHADERC_INCLUDE_DIR} + ${SPIRV_CROSS_INCLUDE_DIR} +) if(NOT ANDROID) target_sources(util PRIVATE @@ -283,9 +291,6 @@ function(add_util_resources target) # Copy dynamically-loaded libraries into the bundle. add_runtime_libraries(${target} spirv-cross-c-shared Shaderc::shaderc_shared SQLite3::sqlite3-shared SDL3::SDL3 - libjpeg-turbo::jpeg PNG::png_shared WebP::webp WebP::sharpyuv WebP::webpdecoder - WebP::webpdemux WebP::libwebpmux) - if(APPLE) - add_runtime_libraries(${target} harfbuzz::harfbuzz) - endif() + freetype harfbuzz::harfbuzz plutosvg::plutosvg libjpeg-turbo::jpeg PNG::png_shared WebP::webp + WebP::sharpyuv WebP::webpdecoder WebP::webpdemux WebP::libwebpmux) endfunction() diff --git a/src/util/dyn_libs.cpp b/src/util/dyn_libs.cpp new file mode 100644 index 000000000..d4c790b19 --- /dev/null +++ b/src/util/dyn_libs.cpp @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "dyn_freetype.h" +#include "dyn_plutosvg.h" + +#include "common/dynamic_library.h" +#include "common/error.h" +#include "common/log.h" + +LOG_CHANNEL(DynamicLibrary); + +static_assert(std::is_trivially_copyable_v && std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v && std::is_standard_layout_v); + +// Because of course friggin linux is different... +#ifdef _WIN32 +static constexpr int FREETYPE_MAJOR_VERSION = -1; +static constexpr int PLUTOSVG_MAJOR_VERSION = -1; +#else +static constexpr int FREETYPE_MAJOR_VERSION = 6; +static constexpr int PLUTOSVG_MAJOR_VERSION = 0; +#endif + +namespace { +struct Locals +{ + // Dynamic libraries + DynamicLibrary freetype_library; + std::once_flag freetype_init_flag; + DynamicLibrary plutosvg_library; + std::once_flag plutosvg_init_flag; +}; +} // namespace + +DynFreeType g_dyn_freetype; +DynPlutoSvg g_dyn_plutosvg; +static Locals s_locals; + +static bool LoadDynLib(const char* libname, int major_version, DynamicLibrary& dynlib, std::once_flag& once_flag, + std::span symbols, Error* const error) +{ + std::call_once(once_flag, [&libname, &major_version, &dynlib, &symbols, error]() { + Error lerror; + DynamicLibrary lib; + if (!lib.Open(DynamicLibrary::GetVersionedFilename(libname, major_version).c_str(), &lerror) || + !lib.ResolveSymbols(symbols.data(), symbols.size(), &lerror)) + { + ERROR_LOG("Failed to load {}: {}", libname, lerror.GetDescription()); + Error::SetStringFmt(error, "Failed to load {}: {}", libname, lerror.GetDescription()); + return; + } + + dynlib = std::move(lib); + }); + + return dynlib.IsOpen(); +} + +static const DynamicLibrary::SymbolTable s_freetype_symbols[] = { +#define RESOLVE_SYMBOL(F) {#F, (void**)&g_dyn_freetype.F}, + DYN_FREETYPE_FUNCTIONS(RESOLVE_SYMBOL) +#undef RESOLVE_SYMBOL +}; + +bool DynFreeType::Open(Error* const error) +{ + if (s_locals.freetype_library.IsOpen()) [[likely]] + return true; + + return LoadDynLib("freetype", FREETYPE_MAJOR_VERSION, s_locals.freetype_library, s_locals.freetype_init_flag, + s_freetype_symbols, error); +} + +static const DynamicLibrary::SymbolTable s_plutosvg_symbols[] = { +#define RESOLVE_SYMBOL(F) {#F, (void**)&g_dyn_plutosvg.F}, + DYN_PLUTOSVG_FUNCTIONS(RESOLVE_SYMBOL) +#undef RESOLVE_SYMBOL +}; + +bool DynPlutoSvg::Open(Error* const error) +{ + if (s_locals.plutosvg_library.IsOpen()) [[likely]] + return true; + + return LoadDynLib("plutosvg", PLUTOSVG_MAJOR_VERSION, s_locals.plutosvg_library, s_locals.plutosvg_init_flag, + s_plutosvg_symbols, error); +} diff --git a/src/util/image.cpp b/src/util/image.cpp index a9374adc1..9befb5751 100644 --- a/src/util/image.cpp +++ b/src/util/image.cpp @@ -20,6 +20,7 @@ #include "common/string_util.h" #include +#include #include #include #include @@ -611,10 +612,14 @@ bool Image::RasterizeSVG(const std::span data, u32 width, u32 height, return false; } + if (!g_dyn_plutosvg.Open(error)) [[unlikely]] + return false; + std::unique_ptr doc( - plutosvg_document_load_from_data(reinterpret_cast(data.data()), static_cast(data.size_bytes()), - static_cast(width), static_cast(height), nullptr, nullptr), - plutosvg_document_destroy); + g_dyn_plutosvg.plutosvg_document_load_from_data(reinterpret_cast(data.data()), + static_cast(data.size_bytes()), static_cast(width), + static_cast(height), nullptr, nullptr), + g_dyn_plutosvg.plutosvg_document_destroy); if (!doc) { Error::SetStringView(error, "plutosvg_document_load_from_data() failed"); @@ -629,8 +634,8 @@ bool Image::RasterizeSVG(const std::span data, u32 width, u32 height, if (maintain_aspect_ratio) { - const float doc_width = plutosvg_document_get_width(doc.get()); - const float doc_height = plutosvg_document_get_height(doc.get()); + const float doc_width = g_dyn_plutosvg.plutosvg_document_get_width(doc.get()); + const float doc_height = g_dyn_plutosvg.plutosvg_document_get_height(doc.get()); if (doc_width > 0.0f && doc_height > 0.0f) { const float fwidth = static_cast(width); @@ -660,9 +665,9 @@ bool Image::RasterizeSVG(const std::span data, u32 width, u32 height, } std::unique_ptr bitmap( - plutosvg_document_render_to_surface(doc.get(), nullptr, static_cast(width), static_cast(height), - ¤t_color, nullptr, nullptr), - plutovg_surface_destroy); + g_dyn_plutosvg.plutosvg_document_render_to_surface(doc.get(), nullptr, static_cast(width), + static_cast(height), ¤t_color, nullptr, nullptr), + g_dyn_plutosvg.plutovg_surface_destroy); if (!bitmap) { Error::SetStringView(error, "plutosvg_document_render_to_surface() failed"); @@ -683,7 +688,8 @@ bool Image::RasterizeSVG(const std::span data, u32 width, u32 height, // lunasvg works in BGRA, swap to RGBA SwapBGRAToRGBA(m_pixels.get() + (canvas_offset_y * m_pitch) + (canvas_offset_x * sizeof(u32)), m_pitch, - plutovg_surface_get_data(bitmap.get()), plutovg_surface_get_stride(bitmap.get()), width, height); + g_dyn_plutosvg.plutovg_surface_get_data(bitmap.get()), + g_dyn_plutosvg.plutovg_surface_get_stride(bitmap.get()), width, height); return true; } diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp index 5a855def4..a6b1ebb00 100644 --- a/src/util/imgui_manager.cpp +++ b/src/util/imgui_manager.cpp @@ -28,12 +28,14 @@ #include "common/threading.h" #include "common/timer.h" -#include "IconsEmoji.h" -#include "IconsFontAwesome.h" -#include "fmt/format.h" -#include "imgui.h" -#include "imgui_freetype.h" -#include "imgui_internal.h" +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -366,6 +368,10 @@ void ImGuiManager::UpdateFixedFont() bool ImGuiManager::Initialize(Error* error) { + // Requires both freetype and plutosvg. + if (!g_dyn_freetype.Open(error) || !g_dyn_plutosvg.Open(error)) [[unlikely]] + return false; + s_state.text_font_index = GetTextFontIndex(); s_state.fixed_font_index = GetFixedFontIndex(); diff --git a/src/util/util.props b/src/util/util.props index fe7683605..b09da5040 100644 --- a/src/util/util.props +++ b/src/util/util.props @@ -25,7 +25,7 @@ %(AdditionalIncludeDirectories);$(DepsIncludeDir)spirv_cross - %(AdditionalDependencies);freetype.lib;plutosvg.lib;soundtouch.lib;zip.lib;zlib.lib;zstd.lib + %(AdditionalDependencies);soundtouch.lib;zip.lib;zlib.lib;zstd.lib diff --git a/src/util/util.vcxproj b/src/util/util.vcxproj index de143c867..dda5b39f3 100644 --- a/src/util/util.vcxproj +++ b/src/util/util.vcxproj @@ -122,6 +122,7 @@ + @@ -278,7 +279,7 @@ - %(AdditionalIncludeDirectories);$(SolutionDir)dep\reshadefx\include;$(SolutionDir)dep\lzma\include + %(AdditionalIncludeDirectories);$(SolutionDir)dep\reshadefx\include;$(SolutionDir)dep\lzma\include;$(DepsIncludeDir)freetype2 $(IntDir)/%(RelativeDir)/ Use pch.h diff --git a/src/util/util.vcxproj.filters b/src/util/util.vcxproj.filters index 5faad9dee..27f615e6f 100644 --- a/src/util/util.vcxproj.filters +++ b/src/util/util.vcxproj.filters @@ -175,6 +175,7 @@ +