From 21817cb7fa0823c68b1dd7d1604125ae71a8aba8 Mon Sep 17 00:00:00 2001 From: Erfan Ahmadi Date: Wed, 29 Jul 2026 12:37:07 +0400 Subject: [PATCH 1/3] removed textrendering extension and msdfgen from 3rdparty --- .gitmodules | 4 - 3rdparty/CMakeLists.txt | 18 - 3rdparty/msdfgen | 1 - include/nbl/ext/TextRendering/TextRendering.h | 293 -------- include/nbl/ext/TextRendering/stb_rect_pack.h | 623 ------------------ src/nbl/ext/CMakeLists.txt | 5 - src/nbl/ext/TextRendering/CMakeLists.txt | 29 +- 7 files changed, 1 insertion(+), 972 deletions(-) delete mode 160000 3rdparty/msdfgen delete mode 100644 include/nbl/ext/TextRendering/TextRendering.h delete mode 100644 include/nbl/ext/TextRendering/stb_rect_pack.h diff --git a/.gitmodules b/.gitmodules index 6ef5432f80..f96f66ad0d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -65,10 +65,6 @@ [submodule "examples_tests"] path = examples_tests url = git@github.com:Devsh-Graphics-Programming/Nabla-Examples-and-Tests.git -[submodule "3rdparty/msdfgen"] - path = 3rdparty/msdfgen - url = git@github.com:Chlumsky/msdfgen.git - branch = master [submodule "3rdparty/dxc/dxc"] path = 3rdparty/dxc/dxc url = git@github.com:Devsh-Graphics-Programming/DirectXShaderCompiler.git diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 68e821dfdf..6e7e9e9970 100755 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -39,24 +39,6 @@ add_subdirectory(simdjson simdjson EXCLUDE_FROM_ALL) # freetype2 set(FT_WITH_HARFBUZZ OFF) add_subdirectory(freetype2 freetype2 EXCLUDE_FROM_ALL) -add_library(Freetype::Freetype ALIAS freetype) # alias target for MSDFGEN not trying to find freetype system-wide but rather to use the logical target available in our build tree - -# msdfgen -set(MSDFGEN_USE_VCPKG OFF) -set(MSDFGEN_USE_SKIA OFF) -set(MSDFGEN_CORE_ONLY OFF) # if turned on then MSDFGEN will be built without dependencies like freetype or skia -set(MSDFGEN_INSTALL OFF) -set(MSDFGEN_BUILD_STANDALONE OFF) -set(MSDFGEN_USE_CPP11 ON) -set(MSDFGEN_DISABLE_SVG ON) -set(MSDFGEN_DISABLE_PNG ON) -add_subdirectory(msdfgen msdfgen EXCLUDE_FROM_ALL) - -if(MSDFGEN_CORE_ONLY) - message(FATAL_ERROR "Internal error, MSDF must be used with extension target as well!") -else() - set(NBL_MSDFGEN_TARGETS msdfgen-ext msdfgen-core) -endif() set_property(TARGET simdjson PROPERTY CXX_STANDARD 17) diff --git a/3rdparty/msdfgen b/3rdparty/msdfgen deleted file mode 160000 index 755c45b444..0000000000 --- a/3rdparty/msdfgen +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 755c45b4445baa12bfead90fa7d4d2259f2a14c4 diff --git a/include/nbl/ext/TextRendering/TextRendering.h b/include/nbl/ext/TextRendering/TextRendering.h deleted file mode 100644 index e9888ecced..0000000000 --- a/include/nbl/ext/TextRendering/TextRendering.h +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h - -#ifndef _NBL_EXT_TEXT_RENDERING_H_INCLUDED_ -#define _NBL_EXT_TEXT_RENDERING_H_INCLUDED_ - -#include "nabla.h" - -#include -#include -#include -#include -#include -#include FT_FREETYPE_H -#include FT_OUTLINE_H -#include FT_TRUETYPE_TABLES_H -#include FT_SFNT_NAMES_H -#include FT_MULTIPLE_MASTERS_H - -using namespace nbl; -using namespace nbl::core; -using namespace nbl::asset; -using namespace nbl::video; -using namespace nbl::hlsl; - -namespace nbl -{ -namespace ext -{ -namespace TextRendering -{ - -class TextRenderer : public nbl::core::IReferenceCounted -{ -public: - - static constexpr asset::E_FORMAT MSDFTextureFormat = asset::E_FORMAT::EF_R8G8B8A8_SNORM; - - // Takes the CPUBuffer containing the image data in SNORM format and an offset into it - void generateShapeMSDF( - ICPUBuffer* bufferToFill, - size_t* bufferOffset, - msdfgen::Shape glyph, - float32_t msdfPixelRange, - uint32_t2 msdfExtents, - float32_t2 scale, - float32_t2 translate); - - TextRenderer() - { - auto error = FT_Init_FreeType(&m_ftLibrary); - assert(!error); - } - - ~TextRenderer() - { - auto error = FT_Done_FreeType(m_ftLibrary); - assert(!error); - } - - // TODO: Remove these here, it's only used for customized tests such as building shapes for hatches - const FT_Library& getFreetypeLibrary() const { return m_ftLibrary; } - FT_Library& getFreetypeLibrary() { return m_ftLibrary; } - -protected: - friend class FontFace; - FT_Library m_ftLibrary; -}; - -class FontFace : public nbl::core::IReferenceCounted -{ -public: - - // Face Global Metrics/Settings - struct Metrics - { - // This value is the vertical distance between two consecutive baselines, expressed in font units. It is always positive. - float64_t height; - // The distance from the baseline to the highest or upper grid coordinate used to place an outline point. It is a positive value. - float64_t ascent; - // The distance from the baseline to the lowest grid coordinate used to place an outline point. this is almost always a negative value. - float64_t descent; - }; - - struct GlyphMetrics - { - // Offset that should be applied to the current baseline after this glyph is placed - float64_t2 advance; - // Offset of the glyph's top left from the current baseline start, horizontal refers to horizonral LTR or RTL Languages - float64_t2 horizontalBearing; - // Size of the glyph in the text line - float64_t2 size; - }; - - static core::smart_refctd_ptr create(core::smart_refctd_ptr&& textRenderer, const std::string& path, int faceIdx = 0) - { - FT_Face face; - FT_Error res = FT_New_Face(textRenderer->m_ftLibrary, path.c_str(), faceIdx, &face); - if (res != 0) - return nullptr; - - return core::smart_refctd_ptr(new FontFace(std::move(textRenderer), face, path), core::dont_grab); - } - - ~FontFace() - { - auto error = FT_Done_Face(m_ftFace); - assert(!error); - } - - static constexpr uint32_t InvalidGlyphIndex = ~0u; - - uint32_t getGlyphIndex(wchar_t unicode) - { - if (m_ftFace == nullptr) - { - assert(false); - return InvalidGlyphIndex; - } - return FT_Get_Char_Index(m_ftFace, unicode); - } - - Metrics getMetrics() const; - - GlyphMetrics getGlyphMetrics(uint32_t glyphId); - - // returns the cpu buffer for the generated MSDF texture with "TextRenderer::MSDFTextureFormat" format - // it will place the glyph in the center of msdfExtents considering the margin of msdfPixelRange - // preserves aspect ratio of the glyph corresponding to metrics of the "glyphId" - // use the `getUV` to address the glyph in your texture correctly. - core::smart_refctd_ptr generateGlyphMSDF(uint32_t baseMSDFPixelRange, uint32_t glyphId, uint32_t2 textureExtents, uint32_t mipLevels); - - // transforms uv in glyph space to uv in the actual texture - float32_t2 getUV(float32_t2 uv, float32_t2 glyphSize, uint32_t2 textureExtents, uint32_t msdfPixelRange); - - core::blake3_hash_t getHash() { return m_hash; } - - // TODO: make these protected, it's only used for customized tests such as building shapes for hatches - FT_GlyphSlot getGlyphSlot(uint32_t glyphId) - { - auto error = FT_Load_Glyph(m_ftFace, glyphId, FT_LOAD_NO_SCALE); - assert(!error); - return m_ftFace->glyph; - } - - FT_Face getFreetypeFace() { return m_ftFace; } - - msdfgen::Shape generateGlyphShape(uint32_t glyphId); - - // Functions to Set and Query Masters: - - bool hasMultipleMasters() const - { - return FT_HAS_MULTIPLE_MASTERS(m_ftFace); - } - - uint32_t getMastersCount() const - { - uint32_t numMasters = 0u; - if (hasMultipleMasters()) - { - FT_MM_Var* multipleMasterData; - FT_Error res = FT_Get_MM_Var(m_ftFace, &multipleMasterData); - if (res == 0) - { - numMasters = multipleMasterData->num_namedstyles; - FT_Done_MM_Var(m_textRenderer->m_ftLibrary, multipleMasterData); - } - } - return numMasters; - } - - bool setMasterIndex(uint32_t index) - { - FT_Error res = FT_Set_Named_Instance(m_ftFace, index); - if (res == 0) - { - calculateFaceHash(); - return true; - } - return false; - } - - // bits [0,15] are for faceIndex given to create function / constructor - // bits [16,30] are for masterIndex given to `setMasterIndex` if it was ever called successfully - // bit 31 is always 0 - // See FreeType documentation: https://freetype.org/freetype2/docs/reference/ft2-face_creation.html#ft_face - int32_t getFaceIndex() const - { - return static_cast(m_ftFace->face_index); - } - - // Each face may have multiple masters. to change face index you need to recreate your face and use `int faceIdx` param - uint32_t getNumFaces() const { return m_ftFace->num_faces; } - - // Names: - const char* getFontFamilyName() const { return m_ftFace->family_name; } - const char* getStyleName() const { return m_ftFace->style_name; } - -protected: - - void calculateFaceHash() - { - core::blake3_hasher hasher; - hasher.update(&m_ftFace->face_index, sizeof(FT_Long)); - hasher.update(&m_pathHash, sizeof(core::blake3_hash_t)); - m_hash = static_cast(hasher); - } - - FontFace(core::smart_refctd_ptr&& textRenderer, FT_Face face, const std::string& path) - { - m_textRenderer = std::move(textRenderer); - m_ftFace = face; - - // calculate hash path and store it - core::blake3_hasher hasher; - hasher.update(path.data(), path.size()); - m_pathHash = static_cast(hasher); - // calculate final hash with faceIdx - calculateFaceHash(); - } - - core::smart_refctd_ptr m_textRenderer; - FT_Face m_ftFace; - core::blake3_hash_t m_pathHash; - core::blake3_hash_t m_hash; -}; - -// Helper class for building an msdfgen shape from a glyph -// The shape can be built like a canvas drawing API (move to, line to, -// and by adding quadratic & cubic segments) -class GlyphShapeBuilder { -public: - GlyphShapeBuilder(msdfgen::Shape& createShape) : shape(createShape) {} - - // Start a new line from here - void moveTo(const float64_t2 to) - { - if (!(currentContour && currentContour->edges.empty())) - currentContour = &shape.addContour(); - lastPosition = to; - } - - // Continue the last line started with moveTo (could also use the last - // position from a lineTo) - void lineTo(const float64_t2 to) - { - if (to != lastPosition) { - currentContour->addEdge(msdfgen::EdgeHolder(msdfPoint(lastPosition), msdfPoint(to))); - lastPosition = to; - } - } - - // Continue the last moveTo or lineTo with a quadratic bezier: - // [last position, control, end] - void quadratic(const float64_t2 control, const float64_t2 to) - { - currentContour->addEdge(msdfgen::EdgeHolder(msdfPoint(lastPosition), msdfPoint(control), msdfPoint(to))); - lastPosition = to; - } - - // Continue the last moveTo or lineTo with a cubic bezier: - // [last position, control1, control2, end] - void cubic(const float64_t2 control1, const float64_t2 control2, const float64_t2 to) - { - currentContour->addEdge(msdfgen::EdgeHolder(msdfPoint(lastPosition), msdfPoint(control1), msdfPoint(control2), msdfPoint(to))); - lastPosition = to; - } - - void finish() - { - if (!shape.contours.empty() && shape.contours.back().edges.empty()) - shape.contours.pop_back(); - } -private: - msdfgen::Point2 msdfPoint(const float64_t2 point) - { - return msdfgen::Point2(point.x, point.y); - } - - // Shape that is currently being created - msdfgen::Shape& shape; - // Set with move to and line to - float64_t2 lastPosition = float64_t2(0.0); - // Current contour, used for adding edges - msdfgen::Contour* currentContour = nullptr; -}; - -} -} -} -#endif \ No newline at end of file diff --git a/include/nbl/ext/TextRendering/stb_rect_pack.h b/include/nbl/ext/TextRendering/stb_rect_pack.h deleted file mode 100644 index ab24c488a1..0000000000 --- a/include/nbl/ext/TextRendering/stb_rect_pack.h +++ /dev/null @@ -1,623 +0,0 @@ -// stb_rect_pack.h - v1.01 - public domain - rectangle packing -// Sean Barrett 2014 -// -// Useful for e.g. packing rectangular textures into an atlas. -// Does not do rotation. -// -// Before #including, -// -// #define STB_RECT_PACK_IMPLEMENTATION -// -// in the file that you want to have the implementation. -// -// Not necessarily the awesomest packing method, but better than -// the totally naive one in stb_truetype (which is primarily what -// this is meant to replace). -// -// Has only had a few tests run, may have issues. -// -// More docs to come. -// -// No memory allocations; uses qsort() and assert() from stdlib. -// Can override those by defining STBRP_SORT and STBRP_ASSERT. -// -// This library currently uses the Skyline Bottom-Left algorithm. -// -// Please note: better rectangle packers are welcome! Please -// implement them to the same API, but with a different init -// function. -// -// Credits -// -// Library -// Sean Barrett -// Minor features -// Martins Mozeiko -// github:IntellectualKitty -// -// Bugfixes / warning fixes -// Jeremy Jaussaud -// Fabian Giesen -// -// Version history: -// -// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section -// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles -// 0.99 (2019-02-07) warning fixes -// 0.11 (2017-03-03) return packing success/fail result -// 0.10 (2016-10-25) remove cast-away-const to avoid warnings -// 0.09 (2016-08-27) fix compiler warnings -// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) -// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) -// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort -// 0.05: added STBRP_ASSERT to allow replacing assert -// 0.04: fixed minor bug in STBRP_LARGE_RECTS support -// 0.01: initial release -// -// LICENSE -// -// See end of file for license information. - -////////////////////////////////////////////////////////////////////////////// -// -// INCLUDE SECTION -// - -#ifndef STB_INCLUDE_STB_RECT_PACK_H -#define STB_INCLUDE_STB_RECT_PACK_H - -#define STB_RECT_PACK_VERSION 1 - -#ifdef STBRP_STATIC -#define STBRP_DEF static -#else -#define STBRP_DEF extern -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct stbrp_context stbrp_context; -typedef struct stbrp_node stbrp_node; -typedef struct stbrp_rect stbrp_rect; - -typedef int stbrp_coord; - -#define STBRP__MAXVAL 0x7fffffff -// Mostly for internal use, but this is the maximum supported coordinate value. - -STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); -// Assign packed locations to rectangles. The rectangles are of type -// 'stbrp_rect' defined below, stored in the array 'rects', and there -// are 'num_rects' many of them. -// -// Rectangles which are successfully packed have the 'was_packed' flag -// set to a non-zero value and 'x' and 'y' store the minimum location -// on each axis (i.e. bottom-left in cartesian coordinates, top-left -// if you imagine y increasing downwards). Rectangles which do not fit -// have the 'was_packed' flag set to 0. -// -// You should not try to access the 'rects' array from another thread -// while this function is running, as the function temporarily reorders -// the array while it executes. -// -// To pack into another rectangle, you need to call stbrp_init_target -// again. To continue packing into the same rectangle, you can call -// this function again. Calling this multiple times with multiple rect -// arrays will probably produce worse packing results than calling it -// a single time with the full rectangle array, but the option is -// available. -// -// The function returns 1 if all of the rectangles were successfully -// packed and 0 otherwise. - -struct stbrp_rect -{ - // reserved for your use: - int id; - - // input: - stbrp_coord w, h; - - // output: - stbrp_coord x, y; - int was_packed; // non-zero if valid packing - -}; // 16 bytes, nominally - - -STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); -// Initialize a rectangle packer to: -// pack a rectangle that is 'width' by 'height' in dimensions -// using temporary storage provided by the array 'nodes', which is 'num_nodes' long -// -// You must call this function every time you start packing into a new target. -// -// There is no "shutdown" function. The 'nodes' memory must stay valid for -// the following stbrp_pack_rects() call (or calls), but can be freed after -// the call (or calls) finish. -// -// Note: to guarantee best results, either: -// 1. make sure 'num_nodes' >= 'width' -// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' -// -// If you don't do either of the above things, widths will be quantized to multiples -// of small integers to guarantee the algorithm doesn't run out of temporary storage. -// -// If you do #2, then the non-quantized algorithm will be used, but the algorithm -// may run out of temporary storage and be unable to pack some rectangles. - -STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); -// Optionally call this function after init but before doing any packing to -// change the handling of the out-of-temp-memory scenario, described above. -// If you call init again, this will be reset to the default (false). - - -STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); -// Optionally select which packing heuristic the library should use. Different -// heuristics will produce better/worse results for different data sets. -// If you call init again, this will be reset to the default. - -enum -{ - STBRP_HEURISTIC_Skyline_default=0, - STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, - STBRP_HEURISTIC_Skyline_BF_sortHeight -}; - - -////////////////////////////////////////////////////////////////////////////// -// -// the details of the following structures don't matter to you, but they must -// be visible so you can handle the memory allocations for them - -struct stbrp_node -{ - stbrp_coord x,y; - stbrp_node *next; -}; - -struct stbrp_context -{ - int width; - int height; - int align; - int init_mode; - int heuristic; - int num_nodes; - stbrp_node *active_head; - stbrp_node *free_head; - stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' -}; - -#ifdef __cplusplus -} -#endif - -#endif - -////////////////////////////////////////////////////////////////////////////// -// -// IMPLEMENTATION SECTION -// - -#ifdef STB_RECT_PACK_IMPLEMENTATION -#ifndef STBRP_SORT -#include -#define STBRP_SORT qsort -#endif - -#ifndef STBRP_ASSERT -#include -#define STBRP_ASSERT assert -#endif - -#ifdef _MSC_VER -#define STBRP__NOTUSED(v) (void)(v) -#define STBRP__CDECL __cdecl -#else -#define STBRP__NOTUSED(v) (void)sizeof(v) -#define STBRP__CDECL -#endif - -enum -{ - STBRP__INIT_skyline = 1 -}; - -STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) -{ - switch (context->init_mode) { - case STBRP__INIT_skyline: - STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); - context->heuristic = heuristic; - break; - default: - STBRP_ASSERT(0); - } -} - -STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) -{ - if (allow_out_of_mem) - // if it's ok to run out of memory, then don't bother aligning them; - // this gives better packing, but may fail due to OOM (even though - // the rectangles easily fit). @TODO a smarter approach would be to only - // quantize once we've hit OOM, then we could get rid of this parameter. - context->align = 1; - else { - // if it's not ok to run out of memory, then quantize the widths - // so that num_nodes is always enough nodes. - // - // I.e. num_nodes * align >= width - // align >= width / num_nodes - // align = ceil(width/num_nodes) - - context->align = (context->width + context->num_nodes-1) / context->num_nodes; - } -} - -STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) -{ - int i; - - for (i=0; i < num_nodes-1; ++i) - nodes[i].next = &nodes[i+1]; - nodes[i].next = NULL; - context->init_mode = STBRP__INIT_skyline; - context->heuristic = STBRP_HEURISTIC_Skyline_default; - context->free_head = &nodes[0]; - context->active_head = &context->extra[0]; - context->width = width; - context->height = height; - context->num_nodes = num_nodes; - stbrp_setup_allow_out_of_mem(context, 0); - - // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) - context->extra[0].x = 0; - context->extra[0].y = 0; - context->extra[0].next = &context->extra[1]; - context->extra[1].x = (stbrp_coord) width; - context->extra[1].y = (1<<30); - context->extra[1].next = NULL; -} - -// find minimum y position if it starts at x1 -static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste) -{ - stbrp_node *node = first; - int x1 = x0 + width; - int min_y, visited_width, waste_area; - - STBRP__NOTUSED(c); - - STBRP_ASSERT(first->x <= x0); - - #if 0 - // skip in case we're past the node - while (node->next->x <= x0) - ++node; - #else - STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency - #endif - - STBRP_ASSERT(node->x <= x0); - - min_y = 0; - waste_area = 0; - visited_width = 0; - while (node->x < x1) { - if (node->y > min_y) { - // raise min_y higher. - // we've accounted for all waste up to min_y, - // but we'll now add more waste for everything we've visted - waste_area += visited_width * (node->y - min_y); - min_y = node->y; - // the first time through, visited_width might be reduced - if (node->x < x0) - visited_width += node->next->x - x0; - else - visited_width += node->next->x - node->x; - } else { - // add waste area - int under_width = node->next->x - node->x; - if (under_width + visited_width > width) - under_width = width - visited_width; - waste_area += under_width * (min_y - node->y); - visited_width += under_width; - } - node = node->next; - } - - *pwaste = waste_area; - return min_y; -} - -typedef struct -{ - int x,y; - stbrp_node **prev_link; -} stbrp__findresult; - -static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) -{ - int best_waste = (1<<30), best_x, best_y = (1 << 30); - stbrp__findresult fr; - stbrp_node **prev, *node, *tail, **best = NULL; - - // align to multiple of c->align - width = (width + c->align - 1); - width -= width % c->align; - STBRP_ASSERT(width % c->align == 0); - - // if it can't possibly fit, bail immediately - if (width > c->width || height > c->height) { - fr.prev_link = NULL; - fr.x = fr.y = 0; - return fr; - } - - node = c->active_head; - prev = &c->active_head; - while (node->x + width <= c->width) { - int y,waste; - y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); - if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL - // bottom left - if (y < best_y) { - best_y = y; - best = prev; - } - } else { - // best-fit - if (y + height <= c->height) { - // can only use it if it first vertically - if (y < best_y || (y == best_y && waste < best_waste)) { - best_y = y; - best_waste = waste; - best = prev; - } - } - } - prev = &node->next; - node = node->next; - } - - best_x = (best == NULL) ? 0 : (*best)->x; - - // if doing best-fit (BF), we also have to try aligning right edge to each node position - // - // e.g, if fitting - // - // ____________________ - // |____________________| - // - // into - // - // | | - // | ____________| - // |____________| - // - // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned - // - // This makes BF take about 2x the time - - if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { - tail = c->active_head; - node = c->active_head; - prev = &c->active_head; - // find first node that's admissible - while (tail->x < width) - tail = tail->next; - while (tail) { - int xpos = tail->x - width; - int y,waste; - STBRP_ASSERT(xpos >= 0); - // find the left position that matches this - while (node->next->x <= xpos) { - prev = &node->next; - node = node->next; - } - STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); - y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); - if (y + height <= c->height) { - if (y <= best_y) { - if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { - best_x = xpos; - STBRP_ASSERT(y <= best_y); - best_y = y; - best_waste = waste; - best = prev; - } - } - } - tail = tail->next; - } - } - - fr.prev_link = best; - fr.x = best_x; - fr.y = best_y; - return fr; -} - -static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) -{ - // find best position according to heuristic - stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); - stbrp_node *node, *cur; - - // bail if: - // 1. it failed - // 2. the best node doesn't fit (we don't always check this) - // 3. we're out of memory - if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { - res.prev_link = NULL; - return res; - } - - // on success, create new node - node = context->free_head; - node->x = (stbrp_coord) res.x; - node->y = (stbrp_coord) (res.y + height); - - context->free_head = node->next; - - // insert the new node into the right starting point, and - // let 'cur' point to the remaining nodes needing to be - // stiched back in - - cur = *res.prev_link; - if (cur->x < res.x) { - // preserve the existing one, so start testing with the next one - stbrp_node *next = cur->next; - cur->next = node; - cur = next; - } else { - *res.prev_link = node; - } - - // from here, traverse cur and free the nodes, until we get to one - // that shouldn't be freed - while (cur->next && cur->next->x <= res.x + width) { - stbrp_node *next = cur->next; - // move the current node to the free list - cur->next = context->free_head; - context->free_head = cur; - cur = next; - } - - // stitch the list back in - node->next = cur; - - if (cur->x < res.x + width) - cur->x = (stbrp_coord) (res.x + width); - -#ifdef _DEBUG - cur = context->active_head; - while (cur->x < context->width) { - STBRP_ASSERT(cur->x < cur->next->x); - cur = cur->next; - } - STBRP_ASSERT(cur->next == NULL); - - { - int count=0; - cur = context->active_head; - while (cur) { - cur = cur->next; - ++count; - } - cur = context->free_head; - while (cur) { - cur = cur->next; - ++count; - } - STBRP_ASSERT(count == context->num_nodes+2); - } -#endif - - return res; -} - -static int STBRP__CDECL rect_height_compare(const void *a, const void *b) -{ - const stbrp_rect *p = (const stbrp_rect *) a; - const stbrp_rect *q = (const stbrp_rect *) b; - if (p->h > q->h) - return -1; - if (p->h < q->h) - return 1; - return (p->w > q->w) ? -1 : (p->w < q->w); -} - -static int STBRP__CDECL rect_original_order(const void *a, const void *b) -{ - const stbrp_rect *p = (const stbrp_rect *) a; - const stbrp_rect *q = (const stbrp_rect *) b; - return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); -} - -STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) -{ - int i, all_rects_packed = 1; - - // we use the 'was_packed' field internally to allow sorting/unsorting - for (i=0; i < num_rects; ++i) { - rects[i].was_packed = i; - } - - // sort according to heuristic - STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); - - for (i=0; i < num_rects; ++i) { - if (rects[i].w == 0 || rects[i].h == 0) { - rects[i].x = rects[i].y = 0; // empty rect needs no space - } else { - stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); - if (fr.prev_link) { - rects[i].x = (stbrp_coord) fr.x; - rects[i].y = (stbrp_coord) fr.y; - } else { - rects[i].x = rects[i].y = STBRP__MAXVAL; - } - } - } - - // unsort - STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); - - // set was_packed flags and all_rects_packed status - for (i=0; i < num_rects; ++i) { - rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); - if (!rects[i].was_packed) - all_rects_packed = 0; - } - - // return the all_rects_packed status - return all_rects_packed; -} -#endif - -/* ------------------------------------------------------------------------------- -This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------- -ALTERNATIVE A - MIT License -Copyright (c) 2017 Sean Barrett -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ------------------------------------------------------------------------------- -ALTERNATIVE B - Public Domain (www.unlicense.org) -This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -software, either in source code form or as a compiled binary, for any purpose, -commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors of this -software dedicate any and all copyright interest in the software to the public -domain. We make this dedication for the benefit of the public at large and to -the detriment of our heirs and successors. We intend this dedication to be an -overt act of relinquishment in perpetuity of all present and future rights to -this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------- -*/ \ No newline at end of file diff --git a/src/nbl/ext/CMakeLists.txt b/src/nbl/ext/CMakeLists.txt index 264cfc7c2d..7d669216ce 100644 --- a/src/nbl/ext/CMakeLists.txt +++ b/src/nbl/ext/CMakeLists.txt @@ -5,7 +5,6 @@ start_tracking_variables_for_propagation_to_parent() # TODO: all of those options bellow should be defined here -option(NBL_BUILD_TEXT_RENDERING "Enable Nabla Text Rendering extension building and integration?" OFF) # TODO: also all variables bellow should be killed from build system since we have logical # targets which properties (like include search directories or outputs) can be queried @@ -58,10 +57,6 @@ if (NBL_BUILD_IMGUI) ) endif() -if(NBL_BUILD_TEXT_RENDERING) - add_subdirectory(TextRendering) -endif() - if(NBL_BUILD_DEBUG_DRAW) add_subdirectory(DebugDraw) set(NBL_EXT_DEBUG_DRAW_INCLUDE_DIRS diff --git a/src/nbl/ext/TextRendering/CMakeLists.txt b/src/nbl/ext/TextRendering/CMakeLists.txt index 2d2ced5705..cd0d629322 100644 --- a/src/nbl/ext/TextRendering/CMakeLists.txt +++ b/src/nbl/ext/TextRendering/CMakeLists.txt @@ -23,31 +23,4 @@ nbl_create_ext_library_project( "${NBL_EXT_TEXT_RENDERING_EXTERNAL_INCLUDE}" "" "" -) - -# Freetype -add_dependencies(${LIB_NAME} freetype) -if(NBL_STATIC_BUILD) - target_link_libraries(${LIB_NAME} INTERFACE freetype) -else() - target_link_libraries(${LIB_NAME} PUBLIC freetype) -endif() -target_include_directories(${LIB_NAME} PUBLIC $) - -# msdfgen -add_dependencies(${LIB_NAME} ${NBL_MSDFGEN_TARGETS}) -if(NBL_STATIC_BUILD) - target_link_libraries(${LIB_NAME} INTERFACE ${NBL_MSDFGEN_TARGETS}) -else() - target_link_libraries(${LIB_NAME} PUBLIC ${NBL_MSDFGEN_TARGETS}) -endif() - -foreach(NBL_TARGET IN LISTS NBL_MSDFGEN_TARGETS) - target_include_directories(${LIB_NAME} PUBLIC $) -endforeach() - -# TODO: once the extension is used + some shaders then we can make use of NSC to precompile to -# SPIRV as builtin resources library linked to the extension like IMGUI ext does, then the -# extension would benefit from constexpr pointers to precompiled SPIRV shaders - -set(NBL_EXT_TEXT_RENDERING_TARGET ${LIB_NAME} CACHE INTERNAL "Nabla's Text Rendering logical target name") \ No newline at end of file +) \ No newline at end of file From a4a3158129bdb9392ad99f011c2aae651e84f310 Mon Sep 17 00:00:00 2001 From: Erfan Ahmadi Date: Wed, 29 Jul 2026 12:40:03 +0400 Subject: [PATCH 2/3] remove leftover src files for outdated text rendering extension --- examples_tests | 2 +- src/nbl/ext/TextRendering/CMakeLists.txt | 26 --- src/nbl/ext/TextRendering/TextRendering.cpp | 247 -------------------- 3 files changed, 1 insertion(+), 274 deletions(-) delete mode 100644 src/nbl/ext/TextRendering/CMakeLists.txt delete mode 100644 src/nbl/ext/TextRendering/TextRendering.cpp diff --git a/examples_tests b/examples_tests index 7f2461e328..5f52c496a8 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 7f2461e32825c6544a10cff473b33999e9e1c06c +Subproject commit 5f52c496a8907974fda21bcc264f08c2ba85e330 diff --git a/src/nbl/ext/TextRendering/CMakeLists.txt b/src/nbl/ext/TextRendering/CMakeLists.txt deleted file mode 100644 index cd0d629322..0000000000 --- a/src/nbl/ext/TextRendering/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -include(${NBL_ROOT_PATH}/cmake/common.cmake) - -set(NBL_EXT_INTERNAL_INCLUDE_DIR "${NBL_ROOT_PATH}/include") - -set(NBL_EXT_TEXT_RENDERING_H - # extra headers goes there - # eg. ${NBL_EXT_INTERNAL_INCLUDE_DIR}/something.hpp -) - -set(NBL_EXT_TEXT_RENDERING_SRC - TextRendering.cpp -) - -set(NBL_EXT_TEXT_RENDERING_EXTERNAL_INCLUDE - "${NBL_ROOT_PATH}/3rdparty" - "${NBL_EXT_INTERNAL_INCLUDE_DIR}" -) - -nbl_create_ext_library_project( - TEXT_RENDERING - "${NBL_EXT_TEXT_RENDERING_H}" - "${NBL_EXT_TEXT_RENDERING_SRC}" - "${NBL_EXT_TEXT_RENDERING_EXTERNAL_INCLUDE}" - "" - "" -) \ No newline at end of file diff --git a/src/nbl/ext/TextRendering/TextRendering.cpp b/src/nbl/ext/TextRendering/TextRendering.cpp deleted file mode 100644 index e45ff381bc..0000000000 --- a/src/nbl/ext/TextRendering/TextRendering.cpp +++ /dev/null @@ -1,247 +0,0 @@ -using namespace nbl; -using namespace nbl::core; -using namespace nbl::asset; -using namespace nbl::video; - -#include "nabla.h" -#include - -// TODO sticking to using this library? -// #define STB_RECT_PACK_IMPLEMENTATION -//#include - -namespace nbl -{ -namespace ext -{ -namespace TextRendering -{ - -void TextRenderer::generateShapeMSDF( - ICPUBuffer* bufferToFill, - size_t* bufferOffset, - msdfgen::Shape glyph, - float32_t msdfPixelRange, - uint32_t2 msdfExtents, - float32_t2 scale, - float32_t2 translate) -{ - uint32_t glyphW = msdfExtents.x; - uint32_t glyphH = msdfExtents.y; - - size_t& offset = *bufferOffset; - size_t bufferSize = glyphW * glyphH * sizeof(int8_t) * 4; - assert(bufferToFill->getSize() >= bufferSize + offset); - - int8_t* data = reinterpret_cast(bufferToFill->getPointer()); - - auto floatToSNORM8 = [](const float fl) -> int8_t - { - // we need to invert values because msdfgen assigns positive values for shape interior which is the exact opposite of our convention - return -1 * (int8_t)(std::clamp(fl * 2.0f - 1.0f, -1.0f, 1.0f) * 127.f); - }; - - msdfgen::edgeColoringSimple(glyph, 3.0); - - auto shapeBounds = glyph.getBounds(); - - msdfgen::Bitmap msdfMap(msdfExtents.x, msdfExtents.y); - - float32_t pxRange = msdfPixelRange / (hlsl::min(scale.x, scale.y)); - msdfgen::generateMTSDF(msdfMap, glyph, pxRange, { scale.x, scale.y }, { translate.x, translate.y }); - - for (int y = 0; y < msdfExtents.x; ++y) - { - for (int x = 0; x < msdfExtents.y; ++x) - { - auto pixel = msdfMap(x, msdfExtents.y - 1 - y); - data[offset + (x + y * msdfExtents.x) * 4 + 0] = floatToSNORM8(pixel[0]); - data[offset + (x + y * msdfExtents.x) * 4 + 1] = floatToSNORM8(pixel[1]); - data[offset + (x + y * msdfExtents.x) * 4 + 2] = floatToSNORM8(pixel[2]); - data[offset + (x + y * msdfExtents.x) * 4 + 3] = floatToSNORM8(pixel[3]); - } - } - - offset += bufferSize; -} - -constexpr double FreeTypeFontScaling = 1.0 / 64.0; - -FontFace::Metrics FontFace::getMetrics() const -{ - Metrics ret = {}; - ret.height = float64_t(m_ftFace->height) * FreeTypeFontScaling; - ret.ascent = float64_t(m_ftFace->ascender) * FreeTypeFontScaling; - ret.descent = float64_t(m_ftFace->descender) * FreeTypeFontScaling; - return ret; -} - -FontFace::GlyphMetrics FontFace::getGlyphMetrics(uint32_t glyphId) -{ - auto slot = getGlyphSlot(glyphId); - - return { - .advance = float64_t2(slot->advance.x, 0.0) * FreeTypeFontScaling, - .horizontalBearing = float64_t2(slot->metrics.horiBearingX, slot->metrics.horiBearingY) * FreeTypeFontScaling, - .size = float64_t2(slot->metrics.width, slot->metrics.height) * FreeTypeFontScaling, - }; -} - -core::smart_refctd_ptr FontFace::generateGlyphMSDF(uint32_t baseMSDFPixelRange, uint32_t glyphId, uint32_t2 textureExtents, uint32_t mipLevels) -{ - ICPUImage::SCreationParams imgParams; - { - imgParams.flags = static_cast(0u); // no flags - imgParams.type = ICPUImage::ET_2D; - imgParams.format = TextRenderer::MSDFTextureFormat; - imgParams.extent = { textureExtents.x, textureExtents.y, 1 }; - imgParams.mipLevels = mipLevels; - imgParams.arrayLayers = 1u; - imgParams.samples = ICPUImage::ESCF_1_BIT; - } - - uint32_t bufferSize = 0u; - for (uint32_t i = 0; i < mipLevels; i++) - { - uint32_t mipW = textureExtents.x / (1 << i); - uint32_t mipH = textureExtents.y / (1 << i); - bufferSize += mipW * mipH * sizeof(uint8_t) * 4; - } - - auto image = ICPUImage::create(std::move(imgParams)); - - ICPUBuffer::SCreationParams bparams; - bparams.size = bufferSize; - - auto buffer = ICPUBuffer::create(std::move(bparams)); - auto regions = core::make_refctd_dynamic_array>(mipLevels); - - size_t bufferOffset = 0ull; - for (uint32_t i = 0; i < mipLevels; i++) - { - // we need to generate a msdfgen per mip map, because the msdf generate call consumes the shape - // and we can't deep clone it - auto shape = generateGlyphShape(glyphId); - - uint32_t mipW = textureExtents.x / (1 << i); - uint32_t mipH = textureExtents.y / (1 << i); - - auto& region = regions->begin()[i]; - region.bufferOffset = bufferOffset; - region.bufferRowLength = mipW; - region.bufferImageHeight = mipH; - region.imageSubresource.aspectMask = asset::IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - region.imageSubresource.mipLevel = i; - region.imageSubresource.baseArrayLayer = 0u; - region.imageSubresource.layerCount = 1u; - region.imageOffset = { 0u,0u,0u }; - region.imageExtent = { mipW, mipH, 1u }; - - if (shape.contours.empty()) - { - _NBL_DEBUG_BREAK_IF(true); // glyph id has no contours in it's shape for this font - return nullptr; - } - - auto shapeBounds = shape.getBounds(); - - float32_t2 mipExtents = float32_t2(float(mipW), float(mipH)); - float32_t mipPixelRange = (float32_t)baseMSDFPixelRange / pow(2.0, double(i)); // TODO: Pixel range should be float - - float32_t2 frameSize = float32_t2( - (shapeBounds.r - shapeBounds.l), - (shapeBounds.t - shapeBounds.b) - ); - - const float32_t2 margin = float32_t2(mipPixelRange * 2); - const float32_t2 nonUniformScale = (mipExtents - margin) / frameSize; - const float32_t uniformScale = core::min(nonUniformScale.x, nonUniformScale.y); - - // Center before: ((shapeBounds.l + shapeBounds.r) * 0.5, (shapeBounds.t + shapeBounds.b) * 0.5) - // Center after: msdfExtents / 2.0 - // Transformation implementation: Center after = (Center before + Translation) * Scale - // Plugging in the values and solving for translate yields: - // Translate = (msdfExtents / (2 * scale)) - ((shapeBounds.l + shapeBounds.r) * 0.5, (shapeBounds.t + shapeBounds.b) * 0.5) - const float32_t2 shapeSpaceCenter = float32_t2(shapeBounds.l + shapeBounds.r, shapeBounds.t + shapeBounds.b) * float32_t2(0.5); - const float32_t2 translate = mipExtents / (float32_t2(2.0) * uniformScale) - shapeSpaceCenter; - - // We are using `baseMSDFPixelRange`, because we still need larger range for smaller mips when aa feather is relatively large. - // WARNING: HWTrilinear filtering will not give correct results. - //because now the baseMSDFPixelRange is being used for all mips as it's wrong to mix/lerp values that have different scales (pixel ranges) - m_textRenderer->generateShapeMSDF(buffer.get(), &bufferOffset, shape, baseMSDFPixelRange, mipExtents, float32_t2(uniformScale, uniformScale), translate); - } - assert(bufferOffset <= buffer->getCreationParams().size); - image->setBufferAndRegions(std::move(buffer), std::move(regions)); - - return image; -} - -float32_t2 FontFace::getUV(float32_t2 uv, float32_t2 glyphSize, uint32_t2 textureExtents, uint32_t msdfPixelRange) -{ - // NOTE[Erfan]: I don't know if the calculations here are the best way to do it, but it was the first solution that came to mind to transform glyph uv to actual texture uv - const float32_t2 margin = float32_t2(msdfPixelRange * 2); - const float32_t2 nonUniformScale = (float32_t2(textureExtents) - margin) / glyphSize; - const float32_t uniformScale = core::min(nonUniformScale.x, nonUniformScale.y); - - // after finding the scale we solve this equation to get translate: - // uniformScale * V + translate = P ---> where V is in [0, GlyphSize] and P is in [0, TextureSize] - const float32_t2 translate = (float32_t2(textureExtents) / 2.0f) - (uniformScale * glyphSize / 2.0f); - - // transform uv of glyph into position in actual texture. - const float32_t2 placeInTexture = (uniformScale * (uv * glyphSize) + translate); - // divide by textureExtents to get uv - return placeInTexture / float32_t2(textureExtents); -} - -float64_t2 ftPoint2(const FT_Vector& vector) { - return float64_t2(FreeTypeFontScaling * vector.x, FreeTypeFontScaling * vector.y); -} - -int ftMoveToMSDF(const FT_Vector* to, void* user) { - GlyphShapeBuilder* context = reinterpret_cast(user); - context->moveTo(ftPoint2(*to)); - return 0; -} - -int ftLineToMSDF(const FT_Vector* to, void* user) { - GlyphShapeBuilder* context = reinterpret_cast(user); - context->lineTo(ftPoint2(*to)); - return 0; -} - -int ftConicToMSDF(const FT_Vector* control, const FT_Vector* to, void* user) { - GlyphShapeBuilder* context = reinterpret_cast(user); - context->quadratic(ftPoint2(*control), ftPoint2(*to)); - return 0; -} - -int ftCubicToMSDF(const FT_Vector* control1, const FT_Vector* control2, const FT_Vector* to, void* user) { - GlyphShapeBuilder* context = reinterpret_cast(user); - context->cubic(ftPoint2(*control1), ftPoint2(*control2), ftPoint2(*to)); - return 0; -} - -msdfgen::Shape FontFace::generateGlyphShape(uint32_t glyphId) -{ - auto slot = getGlyphSlot(glyphId); - - msdfgen::Shape shape; - nbl::ext::TextRendering::GlyphShapeBuilder builder(shape); - FT_Outline_Funcs ftFunctions; - ftFunctions.move_to = &ftMoveToMSDF; - ftFunctions.line_to = &ftLineToMSDF; - ftFunctions.conic_to = &ftConicToMSDF; - ftFunctions.cubic_to = &ftCubicToMSDF; - ftFunctions.shift = 0; - ftFunctions.delta = 0; - FT_Error error = FT_Outline_Decompose(&m_ftFace->glyph->outline, &ftFunctions, &builder); - if (error) - return msdfgen::Shape(); - - builder.finish(); - return shape; -} - -} -} -} \ No newline at end of file From 544d2bc74b3fcef9786b92fdba2b8ae8064f21d4 Mon Sep 17 00:00:00 2001 From: Erfan Ahmadi Date: Wed, 29 Jul 2026 13:36:39 +0400 Subject: [PATCH 3/3] update examples_tests --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 5f52c496a8..8515f32fc4 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 5f52c496a8907974fda21bcc264f08c2ba85e330 +Subproject commit 8515f32fc4bb630270dbe3bca83a2835fd2006e2