From 69f2da2423f33c06bf6963cfdc47fd2433d1c574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Mon, 31 Mar 2025 21:27:40 +0200 Subject: [PATCH] Text: new RendererGL class. This is a replacement for the existing AbstractRenderer and Renderer2D / Renderer3D classes, with no STL or other shittiness like excessive allocations, and with a much better feature set. Deprecation of the old APIs is going to happen next. Compared to the old implementation it doesn't make use of the complicated buffer mapping because -- unlike in 2012 -- such behavior was since deemed questionable as the driver (or whichever translation layer like ANGLE or Zink or Apple's GL-over-Metal) may still make a copy anyway and doing so prevents buffer orphaning. So it's right now just plain setData() / setSubData() calls. *If* it becomes some sort of a bottleneck (which I doubt), I may reconsider, or add something else like double buffering. --- src/Magnum/Text/CMakeLists.txt | 6 +- .../Text/Implementation/rendererState.h | 3 +- src/Magnum/Text/Renderer.cpp | 5 +- src/Magnum/Text/Renderer.h | 11 +- src/Magnum/Text/RendererGL.cpp | 306 +++++++ src/Magnum/Text/RendererGL.h | 245 ++++++ src/Magnum/Text/Test/CMakeLists.txt | 26 +- src/Magnum/Text/Test/RendererGLTest.cpp | 768 +++++++++++++++++- src/Magnum/Text/Test/RendererGL_Test.cpp | 74 ++ src/Magnum/Text/Test/render-nothing.tga | Bin 0 -> 20 bytes src/Magnum/Text/Test/render.tga | Bin 0 -> 56 bytes src/Magnum/Text/Text.h | 1 + 12 files changed, 1437 insertions(+), 8 deletions(-) create mode 100644 src/Magnum/Text/RendererGL.cpp create mode 100644 src/Magnum/Text/RendererGL.h create mode 100644 src/Magnum/Text/Test/RendererGL_Test.cpp create mode 100644 src/Magnum/Text/Test/render-nothing.tga create mode 100644 src/Magnum/Text/Test/render.tga diff --git a/src/Magnum/Text/CMakeLists.txt b/src/Magnum/Text/CMakeLists.txt index 37914ae94..a45429636 100644 --- a/src/Magnum/Text/CMakeLists.txt +++ b/src/Magnum/Text/CMakeLists.txt @@ -66,10 +66,12 @@ set(MagnumText_PRIVATE_HEADERS if(MAGNUM_TARGET_GL) list(APPEND MagnumText_GracefulAssert_SRCS DistanceFieldGlyphCacheGL.cpp - GlyphCacheGL.cpp) + GlyphCacheGL.cpp + RendererGL.cpp) list(APPEND MagnumText_HEADERS DistanceFieldGlyphCacheGL.h - GlyphCacheGL.h) + GlyphCacheGL.h + RendererGL.h) if(MAGNUM_BUILD_DEPRECATED) list(APPEND MagnumText_HEADERS DistanceFieldGlyphCache.h diff --git a/src/Magnum/Text/Implementation/rendererState.h b/src/Magnum/Text/Implementation/rendererState.h index ff04d5e5e..23b17f1e7 100644 --- a/src/Magnum/Text/Implementation/rendererState.h +++ b/src/Magnum/Text/Implementation/rendererState.h @@ -150,7 +150,8 @@ struct Renderer::State: RendererCore::AllocatorState { namespace Implementation { -/* Not used in the state structs above but needed by Renderer */ +/* Not used in the state structs above but needed by both Renderer and + RendererGL */ struct Vertex { Vector2 position; Vector2 textureCoordinates; diff --git a/src/Magnum/Text/Renderer.cpp b/src/Magnum/Text/Renderer.cpp index 8c533284e..3840b4098 100644 --- a/src/Magnum/Text/Renderer.cpp +++ b/src/Magnum/Text/Renderer.cpp @@ -930,6 +930,8 @@ Renderer::State::State(const AbstractGlyphCache& glyphCache, void(*glyphAllocato Renderer::Renderer(const AbstractGlyphCache& glyphCache, void(*glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&), void* glyphAllocatorState, void(*runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* runAllocatorState, void(*indexAllocator)(void*, UnsignedInt, Containers::ArrayView&), void* indexAllocatorState, void(*vertexAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* vertexAllocatorState, RendererFlags flags): RendererCore{Containers::pointer(glyphCache, glyphAllocator, glyphAllocatorState, runAllocator, runAllocatorState, indexAllocator, indexAllocatorState, vertexAllocator, vertexAllocatorState, flags)} {} +Renderer::Renderer(Containers::Pointer&& state): RendererCore{Utility::move(state)} {} + Renderer::Renderer(Renderer&&) noexcept = default; Renderer::~Renderer() = default; @@ -937,7 +939,8 @@ Renderer::~Renderer() = default; Renderer& Renderer::operator=(Renderer&&) noexcept = default; RendererFlags Renderer::flags() const { - return RendererFlags{UnsignedByte(_state->flags)}; + /* Subclasses inherit and add their own flags, mask them away */ + return RendererFlags{UnsignedByte(_state->flags)} & RendererFlags{0x1}; } namespace { diff --git a/src/Magnum/Text/Renderer.h b/src/Magnum/Text/Renderer.h index 21b60eb92..d694e61a2 100644 --- a/src/Magnum/Text/Renderer.h +++ b/src/Magnum/Text/Renderer.h @@ -68,8 +68,9 @@ enum class RendererCoreFlag: UnsignedByte { */ GlyphClusters = 1 << 0, - /* Additions to this enum have to be propagated to RendererFlag and the - mask in RendererCore::flag() */ + /* Additions to this enum have to be propagated to RendererFlag, + RendererGLFlag and the masks in RendererCore::flags() and + Renderer::flags() */ }; /** @@ -651,6 +652,9 @@ enum class RendererFlag: UnsignedByte { * memory so this flag includes both clusters and positions. */ GlyphPositionsClusters = Int(RendererCoreFlag::GlyphClusters) + + /* Additions to this enum have to be propagated to RendererGLFlag and the + mask in Renderer::flags() */ }; /** @@ -1062,6 +1066,9 @@ class MAGNUM_TEXT_EXPORT Renderer: public RendererCore { #endif struct State; + /* Delegated to by RendererGL constructors */ + explicit MAGNUM_TEXT_LOCAL Renderer(Containers::Pointer&& state); + private: /* While the allocators get just size to grow by, these functions get the total count */ diff --git a/src/Magnum/Text/RendererGL.cpp b/src/Magnum/Text/RendererGL.cpp new file mode 100644 index 000000000..f1c4b5bce --- /dev/null +++ b/src/Magnum/Text/RendererGL.cpp @@ -0,0 +1,306 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, 2024, 2025 + Vladimír Vondruš + + 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. +*/ + +#include "RendererGL.h" + +#include +#include + +#include "Magnum/Shaders/GenericGL.h" /* no link-time dependency here */ +#include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/Implementation/rendererState.h" + +/* Somehow on GCC 4.8 to 7 the {} passed as a default argument for + ArrayView causes "error: elements of array 'const class + Magnum::Text::FeatureRange [0]' have incomplete type". GCC 9 is fine, no + idea about version 8, but including the definition for it as well to be + safe. Similar problem happens with MSVC STL, where the initializer_list is + implemented as a (begin, end) pair and size() is a difference of those two + pointers. Which needs to know the type size to calculate the actual element + count. */ +#if (defined(CORRADE_TARGET_GCC) && __GNUC__ <= 8) || defined(CORRADE_TARGET_DINKUMWARE) +#include "Magnum/Text/Feature.h" +#endif + +namespace Magnum { namespace Text { + +Debug& operator<<(Debug& debug, const RendererGLFlag value) { + debug << "Text::RendererGLFlag" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case RendererGLFlag::v: return debug << "::" #v; + _c(GlyphPositionsClusters) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << Debug::hex << UnsignedByte(value) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const RendererGLFlags value) { + return Containers::enumSetDebugOutput(debug, value, "Text::RendererGLFlags{}", { + RendererGLFlag::GlyphPositionsClusters + }); +} + +struct RendererGL::State: Renderer::State { + explicit State(const AbstractGlyphCache& glyphCache, RendererGLFlags flags); + + GL::Buffer indices{GL::Buffer::TargetHint::ElementArray}, + vertices{GL::Buffer::TargetHint::Array}; + GL::Mesh mesh; + + /* Because querying GL buffer size is not possible on all platforms and it + may be slow, track the size here. It's used to know whether the buffer + should be reuploaded as a whole or can be partially updated, updated in + both reserve() and render(). */ + UnsignedInt bufferGlyphCapacity = 0; +}; + +RendererGL::State::State(const AbstractGlyphCache& glyphCache, RendererGLFlags flags): Renderer::State{glyphCache, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, RendererFlags{UnsignedByte(flags)}} { + #ifdef MAGNUM_TARGET_GLES2 + CORRADE_ASSERT(glyphCache.size().z() == 1, + "Text::RendererGL: array glyph caches are not supported in OpenGL ES 2.0 and WebGL 1 builds", ); + #endif + + /* As documented in RendererGL::setIndexType(), use of 8-bit indices is + discouraged on contemporary GPUs */ + indexType = minIndexType = MeshIndexType::UnsignedShort; + + /* Set up the mesh with the initial index type and zero primitives to draw. + The count gets updated on each renderer(), index buffer properties each + time the index type changes. */ + mesh.setIndexBuffer(indices, 0, indexType) + .setCount(0); + #ifndef MAGNUM_TARGET_GLES2 + if(glyphCache.size().z() != 1) { + mesh.addVertexBuffer(vertices, 0, + Shaders::GenericGL2D::Position{}, + Shaders::GenericGL2D::TextureArrayCoordinates{}); + } else + #endif + { + mesh.addVertexBuffer(vertices, 0, + Shaders::GenericGL2D::Position{}, + Shaders::GenericGL2D::TextureCoordinates{}); + } +} + +RendererGL::RendererGL(const AbstractGlyphCache& glyphCache, RendererGLFlags flags): Renderer{Containers::pointer(glyphCache, flags)} {} + +RendererGL::RendererGL(RendererGL&&) noexcept = default; + +RendererGL::~RendererGL() = default; + +RendererGL& RendererGL::operator=(RendererGL&&) noexcept = default; + +RendererGLFlags RendererGL::flags() const { + return RendererGLFlags{UnsignedByte(_state->flags)}; +} + +GL::Mesh& RendererGL::mesh() { + return static_cast(*_state).mesh; +} + +const GL::Mesh& RendererGL::mesh() const { + return static_cast(*_state).mesh; +} + +RendererGL& RendererGL::setIndexType(MeshIndexType atLeast) { + State& state = static_cast(*_state); + + Renderer::setIndexType(atLeast); + + /* Upload indices anew if the type is different from before. In this case + it's also most likely that the size is bigger than before, so do it as + a setData() call instead of having a specialized setSubData() code path + if the total size shrinks. + + Besides the type, the capacity should not change compared to when the + buffer was last updated in reserve() or render(). (Which only holds for + builtin allocators, but RendererGL so far allows only builtin allocators + so that's fine. It however does *not* hold for `state.indexData`, as + that can stay larger if the index type becomes smaller, so verifying + against `state.glyphPositions` instead.) */ + CORRADE_INTERNAL_ASSERT(state.bufferGlyphCapacity == state.glyphPositions.size()); + if(GL::meshIndexType(state.indexType) != state.mesh.indexType()) { + state.indices.setData(state.indexData); + state.mesh.setIndexBuffer(state.indices, 0, state.indexType); + } + + return *this; +} + +RendererGL& RendererGL::clear() { + Renderer::clear(); + static_cast(*_state).mesh.setCount(0); + return *this; +} + +RendererGL& RendererGL::reset() { + Renderer::reset(); + static_cast(*_state).mesh.setCount(0); + return *this; +} + +RendererGL& RendererGL::reserve(const UnsignedInt glyphCapacity, const UnsignedInt runCapacity) { + State& state = static_cast(*_state); + + Renderer::reserve(glyphCapacity, runCapacity); + + /* Upload indices anew if the capacity is bigger than before */ + if(state.bufferGlyphCapacity < glyphCapacity) { + state.indices.setData(state.indexData); + /* Update the mesh index buffer reference if the type changed */ + if(GL::meshIndexType(state.indexType) != state.mesh.indexType()) + state.mesh.setIndexBuffer(state.indices, 0, state.indexType); + + /* If the capacity isn't bigger, the index type shouldn't have changed + either and so no upload needs to be done. It can change only if the new + capacity is too larger to fit the type used, or in a setIndexType() + call, but there we handle the reupload directly. */ + } else CORRADE_INTERNAL_ASSERT(GL::meshIndexType(state.indexType) == state.mesh.indexType()); + + /* Resize the vertex buffer and reupload its contents if the capacity is + bigger than before */ + if(state.bufferGlyphCapacity < glyphCapacity) { + const UnsignedInt glyphSize = 4*( + #ifndef MAGNUM_TARGET_GLES2 + state.glyphCache.size().z() != 1 ? + sizeof(Implementation::VertexArray) : + #endif + sizeof(Implementation::Vertex)); + + /* The assumption in this case is that the capacity is bigger than the + actually rendered glyph count, otherwise we'd have it all resized + and uploaded in render() already. Thus we have to do a bigger + setData() allocation first and then upload just a portion with + setSubData(). */ + CORRADE_INTERNAL_ASSERT(glyphCapacity > state.glyphCount); + state.vertices + .setData({nullptr, glyphCapacity*glyphSize}) + .setSubData(0, state.vertexData.prefix(state.glyphCount*glyphSize)); + } + + /* Remember the currently used capacity if it grew. It can happen that + reserve() is called with a smaller capacity, or with just runCapacity + being larger, so this shouldn't reset that and cause needless reupload + next time. */ + state.bufferGlyphCapacity = Math::max(state.bufferGlyphCapacity, glyphCapacity); + + return *this; +} + +Containers::Pair RendererGL::render() { + State& state = static_cast(*_state); + + const Containers::Pair out = Renderer::render(); + + /* Upload indices anew if the glyph count is bigger than before */ + if(state.bufferGlyphCapacity < state.glyphCount) { + state.indices.setData(state.indexData); + /* Update the mesh index buffer reference if the type changed */ + if(GL::meshIndexType(state.indexType) != state.mesh.indexType()) + state.mesh.setIndexBuffer(state.indices, 0, state.indexType); + + /* If the glyph count isn't bigger, the index type shouldn't have changed + either. Same reasoning as in reserve() above. */ + } else CORRADE_INTERNAL_ASSERT(GL::meshIndexType(state.indexType) == state.mesh.indexType()); + + /* Upload vertices fully anew if the glyph count is bigger than before */ + const UnsignedInt glyphSize = 4*( + #ifndef MAGNUM_TARGET_GLES2 + state.glyphCache.size().z() != 1 ? + sizeof(Implementation::VertexArray) : + #endif + sizeof(Implementation::Vertex)); + if(state.bufferGlyphCapacity < state.glyphCount) { + /* Unlike in render(), it's just setData() alone, with the assumption + that the render() caused the capacity to grow to fit exactly all + glyphs, and so we upload everything. (Which only holds for builtin + vertex allocators, but RendererGL so far allows only builtin + allocators so that's fine.) */ + CORRADE_INTERNAL_ASSERT( + state.vertexPositions.size() == state.glyphCount*4 && + state.vertexTextureCoordinates.size() == state.glyphCount*4); + state.vertices.setData(state.vertexData.prefix(state.glyphCount*glyphSize)); + + /* Otherwise upload just what was rendered new */ + } else { + const Range1Dui glyphRange = glyphsForRuns(out.second()); + state.vertices.setSubData(glyphRange.min()*glyphSize, state.vertexData.slice(glyphRange.min()*glyphSize, glyphRange.max()*glyphSize)); + } + + /* Remember the currently used capacity if it grew */ + state.bufferGlyphCapacity = Math::max(state.bufferGlyphCapacity, state.glyphCount); + + /* Set the mesh index count to exactly what was rendered in total */ + state.mesh.setCount(state.glyphCount*6); + + return out; +} + +RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const Containers::ArrayView features) { + return static_cast(Renderer::add(shaper, size, text, begin, end, features)); +} + +RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end) { + return static_cast(Renderer::add(shaper, size, text, begin, end)); +} + +RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const std::initializer_list features) { + return static_cast(Renderer::add(shaper, size, text, begin, end, features)); +} + +RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const Containers::ArrayView features) { + return static_cast(Renderer::add(shaper, size, text, features)); +} + +RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text) { + return static_cast(Renderer::add(shaper, size, text)); +} + +RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const std::initializer_list features) { + return static_cast(Renderer::add(shaper, size, text, features)); +} + +Containers::Pair RendererGL::render(AbstractShaper& shaper, const Float size, const Containers::StringView text, const Containers::ArrayView features) { + /* Compared to Renderer::render() this calls our render() instead of + Renderer::render() */ + add(shaper, size, text, features); + return render(); +} + +Containers::Pair RendererGL::render(AbstractShaper& shaper, const Float size, const Containers::StringView text) { + return render(shaper, size, text, {}); +} + +Containers::Pair RendererGL::render(AbstractShaper& shaper, const Float size, const Containers::StringView text, const std::initializer_list features) { + return render(shaper, size, text, Containers::arrayView(features)); +} + +}} diff --git a/src/Magnum/Text/RendererGL.h b/src/Magnum/Text/RendererGL.h new file mode 100644 index 000000000..8bbcd420b --- /dev/null +++ b/src/Magnum/Text/RendererGL.h @@ -0,0 +1,245 @@ +#ifndef Magnum_Text_RendererGL_h +#define Magnum_Text_RendererGL_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, 2024, 2025 + Vladimír Vondruš + + 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. +*/ + +/** @file + * @brief Class @ref Magnum::Text::RendererGL, enum @ref Magnum::Text::RendererGLFlag, enum set @ref Magnum::Text::RendererGLFlags + * @m_since_latest + */ + +#include "Magnum/configure.h" + +#ifdef MAGNUM_TARGET_GL +#include "Magnum/Text/Renderer.h" + +namespace Magnum { namespace Text { + +/** +@brief OpenGL text renderer flag +@m_since_latest + +A superset of @ref RendererFlag. + +@note This enum is available only if Magnum is compiled with + @ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features + for more information. + +@see @ref RendererGLFlags, @ref RendererGL +*/ +/* Currently is the same as RendererFlag, but is made to a dedicated type to + not cause a breaking change once GL-specific flags are introduced, such as + buffer mapping */ +enum class RendererGLFlag: UnsignedByte { + /** @copydoc RendererFlag::GlyphPositionsClusters */ + GlyphPositionsClusters = UnsignedByte(RendererFlag::GlyphPositionsClusters) +}; + +/** +@debugoperatorenum{RendererGLFlag} +@m_since_latest + +@note This function is available only if Magnum is compiled with + @ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features + for more information. +*/ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, RendererGLFlag value); + +/** +@brief OpenGL text renderer flags +@m_since_latest + +@note This enum set is available only if Magnum is compiled with + @ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features + for more information. + +@see @ref RendererGL +*/ +typedef Containers::EnumSet RendererGLFlags; + +CORRADE_ENUMSET_OPERATORS(RendererGLFlags) + +/** +@debugoperatorenum{RendererGLFlags} +@m_since_latest + +@note This function is available only if Magnum is compiled with + @ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features + for more information. +*/ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, RendererGLFlags value); + +/** +@brief OpenGL text renderer +@m_since_latest + +@note This class is available only if Magnum is compiled with + @ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features + for more information. +*/ +class MAGNUM_TEXT_EXPORT RendererGL: public Renderer { + public: + /** + * @brief Construct + * @param glyphCache Glyph cache to use + * @param flags Opt-in feature flags + * + * Unlike with the @ref Renderer base, the OpenGL implementation needs + * to have a complete control over memory layout and allocation and + * thus it isn't possible to supply custom allocators. If you want the + * control, use @ref Renderer with custom index and vertex allocators + * and fill a @ref GL::Mesh instance with the data manually. + */ + explicit RendererGL(const AbstractGlyphCache& glyphCache, RendererGLFlags flags = {}); + + /** + * @brief Construct without creating the internal state and the OpenGL objects + * + * The constructed instance is equivalent to moved-from state, i.e. no + * APIs can be safely called on the object. Useful in cases where you + * will overwrite the instance later anyway. Move another object over + * it to make it useful. + * + * This function can be safely used for constructing (and later + * destructing) objects even without any OpenGL context being active. + * However note that this is a low-level and a potentially dangerous + * API, see the documentation of @ref NoCreate for alternatives. + */ + explicit RendererGL(NoCreateT) noexcept: Renderer{NoCreate} {} + + /** @brief Copying is not allowed */ + RendererGL(RendererGL&) = delete; + + /** + * @brief Move constructor + * + * Performs a destructive move, i.e. the original object isn't usable + * afterwards anymore. + */ + RendererGL(RendererGL&&) noexcept; + + ~RendererGL(); + + /** @brief Copying is not allowed */ + RendererGL& operator=(RendererGL&) = delete; + + /** @brief Move assignment */ + RendererGL& operator=(RendererGL&&) noexcept; + + /** @brief Flags */ + RendererGLFlags flags() const; + + /** @brief Mesh containing the rendered index and vertex data */ + GL::Mesh& mesh(); + const GL::Mesh& mesh() const; /**< @overload */ + + /** + * @brief Set index type + * @return Reference to self (for method chaining) + * + * Calls @ref Renderer::setIndexType() and updates @ref mesh() with the + * rendered index data, if different from before. Compared to + * @ref Renderer, the default index type is + * @ref MeshIndexType::UnsignedShort, not + * @ref MeshIndexType::UnsignedByte, as use of 8-bit indices is + * discouraged on contemporary GPUs. + */ + RendererGL& setIndexType(MeshIndexType atLeast); + + /** + * @brief Reserve capacity for given glyph count + * @return Reference to self (for method chaining) + * + * Calls @ref Renderer::reserve() and updates @ref mesh() with the + * rendered index data, if different from before. + */ + RendererGL& reserve(UnsignedInt glyphCapacity, UnsignedInt runCapacity); + + /** + * @brief Clear rendered glyphs, runs and vertices + * @return Reference to self (for method chaining) + * + * Calls @ref Renderer::clear() and additionally also sets @ref mesh() + * index count to @cpp 0 @ce. + */ + RendererGL& clear(); + + /** + * @brief Reset internal renderer state + * @return Reference to self (for method chaining) + * + * Calls @ref Renderer::reset(), and additionally also sets @ref mesh() + * index count to @cpp 0 @ce. + */ + RendererGL& reset(); + + /** + * @brief Wrap up rendering of all text added so far + * + * Calls @ref Renderer::render(), updates @ref mesh() with the newly + * rendered vertex data and potentially updates also the index data, if + * different from before. + */ + Containers::Pair render(); + + /* Overloads to remove a WTF factor from method chaining order, and to + ensure our render() is called instead of Render::render() */ + #ifndef DOXYGEN_GENERATING_OUTPUT + RendererGL& setCursor(const Vector2& cursor) { + return static_cast(Renderer::setCursor(cursor)); + } + RendererGL& setAlignment(Alignment alignment) { + return static_cast(Renderer::setAlignment(alignment)); + } + RendererGL& setLineAdvance(Float advance) { + return static_cast(Renderer::setLineAdvance(advance)); + } + RendererGL& setLayoutDirection(LayoutDirection direction) { + return static_cast(Renderer::setLayoutDirection(direction)); + } + + RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features); + RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end); + RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, std::initializer_list features); + RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView features); + RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text); + RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, std::initializer_list features); + + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView features); + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text); + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text, std::initializer_list features); + #endif + + private: + struct State; +}; + +}} +#else +#error this header is available only in the OpenGL build +#endif + +#endif diff --git a/src/Magnum/Text/Test/CMakeLists.txt b/src/Magnum/Text/Test/CMakeLists.txt index 621b6b4a1..76783389b 100644 --- a/src/Magnum/Text/Test/CMakeLists.txt +++ b/src/Magnum/Text/Test/CMakeLists.txt @@ -87,6 +87,7 @@ corrade_add_test(TextScriptTest ScriptTest.cpp LIBRARIES MagnumTextTestLib) if(MAGNUM_TARGET_GL) corrade_add_test(TextGlyphCacheGL_Test GlyphCacheGL_Test.cpp LIBRARIES MagnumText) corrade_add_test(TextDistanceFieldGlyphCacheGL_Test DistanceFieldGlyphCacheGL_Test.cpp LIBRARIES MagnumText) + corrade_add_test(TextRendererGL_Test RendererGL_Test.cpp LIBRARIES MagnumText) if(MAGNUM_BUILD_GL_TESTS) corrade_add_test(TextDistanceFieldGlyphCacheGLTest DistanceFieldGlyphCacheGLTest.cpp @@ -120,9 +121,32 @@ if(MAGNUM_TARGET_GL) MagnumTextTestLib MagnumOpenGLTester MagnumDebugTools) + corrade_add_test(TextRendererGLTest RendererGLTest.cpp LIBRARIES + MagnumDebugTools + MagnumShaders MagnumTextTestLib - MagnumOpenGLTester) + MagnumOpenGLTester + FILES + render.tga + render-nothing.tga) + target_include_directories(TextRendererGLTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) + if(MAGNUM_BUILD_PLUGINS_STATIC) + if(MAGNUM_WITH_ANYIMAGEIMPORTER) + target_link_libraries(TextRendererGLTest PRIVATE AnyImageImporter) + endif() + if(MAGNUM_WITH_TGAIMPORTER) + target_link_libraries(TextRendererGLTest PRIVATE TgaImporter) + endif() + else() + # So the plugins get properly built when building the test + if(MAGNUM_WITH_ANYIMAGEIMPORTER) + add_dependencies(TextRendererGLTest AnyImageImporter) + endif() + if(MAGNUM_WITH_TGAIMPORTER) + add_dependencies(TextRendererGLTest TgaImporter) + endif() + endif() endif() endif() diff --git a/src/Magnum/Text/Test/RendererGLTest.cpp b/src/Magnum/Text/Test/RendererGLTest.cpp index 71dea772c..27cf0c7a5 100644 --- a/src/Magnum/Text/Test/RendererGLTest.cpp +++ b/src/Magnum/Text/Test/RendererGLTest.cpp @@ -27,33 +27,799 @@ #include #include #include +#include #include +#include #include +#include #include +#include +#include "Magnum/Image.h" +#include "Magnum/ImageView.h" +#include "Magnum/Mesh.h" #include "Magnum/PixelFormat.h" +#include "Magnum/Math/Color.h" +#include "Magnum/Math/Matrix3.h" +#include "Magnum/DebugTools/CompareImage.h" #include "Magnum/GL/Context.h" #include "Magnum/GL/Extensions.h" +#include "Magnum/GL/Framebuffer.h" #include "Magnum/GL/OpenGLTester.h" +#include "Magnum/GL/Renderbuffer.h" +#include "Magnum/GL/RenderbufferFormat.h" +#include "Magnum/Shaders/VectorGL.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractShaper.h" #include "Magnum/Text/GlyphCacheGL.h" -#include "Magnum/Text/Renderer.h" +#include "Magnum/Text/RendererGL.h" +#include "Magnum/Trade/AbstractImporter.h" + +#include "configure.h" namespace Magnum { namespace Text { namespace Test { namespace { struct RendererGLTest: GL::OpenGLTester { explicit RendererGLTest(); + void construct(); + #ifdef MAGNUM_TARGET_GLES2 + void constructArrayGlyphCacheNotSupported(); + #endif + void constructCopy(); + void constructMove(); + + /* Compared to Renderer & RendererCore, RendererGL doesn't expose any extra + getters / setters for a properties() test */ + + void renderSetup(); + void renderTeardown(); + void renderClearReset(); + void renderIndexTypeChanged(); + void renderMesh(); void renderMeshIndexType(); void mutableText(); + + private: + PluginManager::Manager _manager{"nonexistent"}; + + GL::Renderbuffer _color{NoCreate}; + GL::Framebuffer _framebuffer{NoCreate}; +}; + +using namespace Containers::Literals; +using namespace Math::Literals; + +const struct { + const char* name; + Containers::Optional indexType; + Int glyphCacheArraySize; + RendererGLFlags flags; + MeshIndexType expectedIndexType; +} ConstructData[]{ + {"", + {}, 1, {}, MeshIndexType::UnsignedShort}, + {"UnsignedByte indices", + MeshIndexType::UnsignedByte, 1, {}, MeshIndexType::UnsignedByte}, + {"UnsignedInt indices", + MeshIndexType::UnsignedInt, 1, {}, MeshIndexType::UnsignedInt}, + {"glyph positions and clusters", + {}, 1, RendererGLFlag::GlyphPositionsClusters, MeshIndexType::UnsignedShort}, + #ifndef MAGNUM_TARGET_GLES2 + {"array glyph cache", + {}, 5, {}, MeshIndexType::UnsignedShort}, + {"array glyph cache, glyph positions and clusters", + {}, 5, RendererGLFlag::GlyphPositionsClusters, MeshIndexType::UnsignedShort}, + #endif +}; + +const struct { + const char* name; + Int glyphCacheArraySize; + RendererGLFlags flags; + UnsignedInt reserveBefore, reserveAfter; + Containers::Optional indexTypeBefore, indexTypeAfter; + bool clear, reset; + MeshIndexType expectedIndexType; +} RenderClearResetData[]{ + {"", + 1, {}, 0, 0, {}, {}, false, false, + MeshIndexType::UnsignedShort}, + {"glyph positions and clusters", + 1, RendererGLFlag::GlyphPositionsClusters, 0, 0, {}, {}, false, false, MeshIndexType::UnsignedShort}, + #ifndef MAGNUM_TARGET_GLES2 + {"array glyph cache", + 5, {}, 0, 0, {}, {}, false, false, + MeshIndexType::UnsignedShort}, + {"array glyph cache, glyph positions and clusters", + 5, RendererGLFlag::GlyphPositionsClusters, 0, 0, {}, {}, false, false, + MeshIndexType::UnsignedShort}, + #endif + /* These test just index buffer generation, so no cache- or glyph-related + variants */ + {"UnsignedByte indices", + 1, {}, 0, 0, MeshIndexType::UnsignedByte, {}, false, false, + MeshIndexType::UnsignedByte}, + {"explicit default UnsignedShort indices", + 1, {}, 0, 0, MeshIndexType::UnsignedShort, {}, false, false, + MeshIndexType::UnsignedShort}, + {"UnsignedInt indices", + 1, {}, 0, 0, MeshIndexType::UnsignedInt, {}, false, false, + MeshIndexType::UnsignedInt}, + {"reserve exactly upfront", + 1, {}, 5, 0, {}, {}, false, false, + MeshIndexType::UnsignedShort}, + {"reserve partially upfront", + 1, {}, 3, 0, {}, {}, false, false, + MeshIndexType::UnsignedShort}, + {"reserve more upfront", + 1, {}, 16385, 0, {}, {}, false, false, + MeshIndexType::UnsignedInt}, + {"reserve again after render with the same", + 1, {}, 0, 5, {}, {}, false, false, + MeshIndexType::UnsignedShort}, + {"reserve again after render with less", + 1, {}, 0, 3, {}, {}, false, false, + MeshIndexType::UnsignedShort}, + {"reserve again after render with more", + /* Reserve a bigger size to ensure it doesn't get aliased with the old + memory, preserving the original contents by accident and hiding a + potential bug where it doesn't get correctly reuploaded */ + 1, {}, 0, 1024*1024, {}, {}, false, false, + MeshIndexType::UnsignedInt}, + {"reserve all upfront and then change indices to UnsignedByte", + 1, {}, 5, 0, MeshIndexType::UnsignedByte, {}, false, false, + MeshIndexType::UnsignedByte}, + {"reserve all upfront and then explicitly use default UnsignedShort indices", + 1, {}, 5, 0, MeshIndexType::UnsignedShort, {}, false, false, + MeshIndexType::UnsignedShort}, + {"reserve all upfront and then change indices to UnsignedInt", + 1, {}, 5, 0, MeshIndexType::UnsignedInt, {}, false, false, + MeshIndexType::UnsignedInt}, + {"change indices to UnsignedByte after render", + 1, {}, 0, 0, {}, MeshIndexType::UnsignedByte, false, false, + MeshIndexType::UnsignedByte}, + {"explicitly set default UnsignedShort after render", + 1, {}, 0, 0, {}, MeshIndexType::UnsignedShort, false, false, + MeshIndexType::UnsignedShort}, + {"change indices to UnsignedInt after render", + 1, {}, 0, 0, {}, MeshIndexType::UnsignedInt, false, false, + MeshIndexType::UnsignedInt}, + {"clear and rerender", + 1, {}, 0, 0, {}, {}, true, false, + MeshIndexType::UnsignedShort}, + {"clear and rerender, UnsignedInt indices", + 1, {}, 0, 0, MeshIndexType::UnsignedInt, {}, true, false, + MeshIndexType::UnsignedInt}, + {"reset and rerender", + 1, {}, 0, 0, {}, {}, true, true, + MeshIndexType::UnsignedShort}, }; RendererGLTest::RendererGLTest() { + addInstancedTests({&RendererGLTest::construct}, + Containers::arraySize(ConstructData)); + + #ifdef MAGNUM_TARGET_GLES2 + addTests({&RendererGLTest::constructArrayGlyphCacheNotSupported}); + #endif + + addTests({&RendererGLTest::constructCopy, + &RendererGLTest::constructMove}); + + addInstancedTests({&RendererGLTest::renderClearReset}, + Containers::arraySize(RenderClearResetData), + &RendererGLTest::renderSetup, + &RendererGLTest::renderTeardown); + + addTests({&RendererGLTest::renderIndexTypeChanged}, + &RendererGLTest::renderSetup, + &RendererGLTest::renderTeardown); + addTests({&RendererGLTest::renderMesh, &RendererGLTest::renderMeshIndexType, &RendererGLTest::mutableText}); + + /* Load the plugins directly from the build tree. Otherwise they're either + static and already loaded or not present in the build tree */ + #ifdef ANYIMAGEIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(ANYIMAGEIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + #ifdef TGAIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(TGAIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif +} + +void RendererGLTest::construct() { + auto&& data = ConstructData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, data.glyphCacheArraySize}}; + + RendererGL renderer{glyphCache, data.flags}; + if(data.indexType) + renderer.setIndexType(*data.indexType); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + CORRADE_COMPARE(renderer.flags(), data.flags); + CORRADE_COMPARE(renderer.indexType(), data.expectedIndexType); + CORRADE_COMPARE(renderer.mesh().count(), 0); + CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(data.expectedIndexType)); +} + +#ifdef MAGNUM_TARGET_GLES2 +void RendererGLTest::constructArrayGlyphCacheNotSupported() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, 5}}; + + Containers::String out; + Error redirectError{&out}; + RendererGL{glyphCache}; + CORRADE_COMPARE(out, "Text::RendererGL: array glyph caches are not supported in OpenGL ES 2.0 and WebGL 1 builds\n"); +} +#endif + +void RendererGLTest::constructCopy() { + CORRADE_VERIFY(!std::is_copy_constructible{}); + CORRADE_VERIFY(!std::is_copy_assignable{}); +} + +void RendererGLTest::constructMove() { + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}, + anotherGlyphCache{PixelFormat::RGBA8Unorm, {4, 4}}; + + /* Verify that both the Renderer and the RendererGL state is + transferred */ + RendererGL a{glyphCache, RendererGLFlags{0x80}}; + UnsignedInt meshId = a.mesh().id(); + a.setIndexType(MeshIndexType::UnsignedInt); + + RendererGL b = Utility::move(a); + CORRADE_COMPARE(&b.glyphCache(), &glyphCache); + CORRADE_COMPARE(b.flags(), RendererGLFlags{0x80}); + CORRADE_COMPARE(b.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE(b.mesh().id(), meshId); + + RendererGL c{anotherGlyphCache}; + c = Utility::move(b); + CORRADE_COMPARE(&c.glyphCache(), &glyphCache); + CORRADE_COMPARE(c.flags(), RendererGLFlags{0x80}); + CORRADE_COMPARE(c.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE(c.mesh().id(), meshId); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + +constexpr Vector2i RenderSize{8, 8}; + +void RendererGLTest::renderSetup() { + /* Pick a color that's directly representable on RGBA4 as well to reduce + artifacts */ + GL::Renderer::setClearColor(0x111111_rgbf); + GL::Renderer::enable(GL::Renderer::Feature::FaceCulling); + + _color = GL::Renderbuffer{}; + _color.setStorage( + #if !defined(MAGNUM_TARGET_GLES2) || !defined(MAGNUM_TARGET_WEBGL) + GL::RenderbufferFormat::RGBA8, + #else + GL::RenderbufferFormat::RGBA4, + #endif + RenderSize); + _framebuffer = GL::Framebuffer{{{}, RenderSize}}; + _framebuffer + .attachRenderbuffer(GL::Framebuffer::ColorAttachment{0}, _color) + .clear(GL::FramebufferClear::Color) + .bind(); +} + +void RendererGLTest::renderTeardown() { + _framebuffer = GL::Framebuffer{NoCreate}; + _color = GL::Renderbuffer{NoCreate}; +} + +void RendererGLTest::renderClearReset() { + auto&& data = RenderClearResetData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Override the default padding to zero to make testing easier, also use + nearest neighbor filtering for predictable output */ + GlyphCacheGL cache{NoCreate}; + #ifndef MAGNUM_TARGET_GLES2 + GlyphCacheArrayGL cacheArray{NoCreate}; + if(data.glyphCacheArraySize != 1) { + cacheArray = GlyphCacheArrayGL{PixelFormat::R8Unorm, {2, 2, data.glyphCacheArraySize}, {}}; + cacheArray.texture() + .setMinificationFilter(SamplerFilter::Nearest) + .setMagnificationFilter(SamplerFilter::Nearest); + } else + #endif + { + cache = GlyphCacheGL{PixelFormat::R8Unorm, {2, 2}, {}}; + cache.texture() + .setMinificationFilter(SamplerFilter::Nearest) + .setMagnificationFilter(SamplerFilter::Nearest); + } + /* For type-independent access below */ + AbstractGlyphCache& glyphCache = + #ifndef MAGNUM_TARGET_GLES2 + data.glyphCacheArraySize != 1 ? static_cast(cacheArray) : + #endif + cache; + + Shaders::VectorGL2D shader{Shaders::VectorGL2D::Configuration{} + #ifndef MAGNUM_TARGET_GLES2 + .setFlags(data.glyphCacheArraySize != 1 ? + Shaders::VectorGL2D::Flag::TextureArrays : + Shaders::VectorGL2D::Flags{}) + #endif + }; + shader.setTransformationProjectionMatrix(Matrix3::projection(Vector2{RenderSize})); + #ifndef MAGNUM_TARGET_GLES2 + if(data.glyphCacheArraySize != 1) { + shader.bindVectorTexture(cacheArray.texture()); + } else + #endif + { + shader.bindVectorTexture(cache.texture()); + } + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float) override { + _opened = true; + /* The size is used to scale advances, ascent & descent is used to + align the block. Line height is used for multi-line text which + we don't test here, glyph count is overriden in addFont() + below. */ + return {1.0f, 2.0f, -1.0f, 10000.0f, 0}; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + + private: + bool _opened = false; + } font; + font.openFile("", 1.0f); + + UnsignedInt fontId = glyphCache.addFont(4, &font); + /* Shuffled order to not have their IDs match the clusters, other than that + the simplest possible contents to make it easy to verify that the data + get correctly uploaded. All corner cases are verified in RendererTest + already. */ + glyphCache.addGlyph(fontId, 3, {}, /* c, white square */ + data.glyphCacheArraySize/2, + Range2Di::fromSize({1, 0}, {1, 1})); + glyphCache.addGlyph(fontId, 1, {}, /* a / d, light gray square */ + data.glyphCacheArraySize - 1, + Range2Di::fromSize({1, 1}, {1, 1})); + glyphCache.addGlyph(fontId, 2, {}, /* b / e, dark gray rect */ + 0, + Range2Di::fromSize({0, 0}, {1, 2})); + { + const Containers::StridedArrayView3D pixels = glyphCache.image().pixels(); + pixels[data.glyphCacheArraySize/2][0][1] = 0xff; + pixels[0][0][0] = 0x33; + pixels[0][1][0] = 0x33; + pixels[data.glyphCacheArraySize - 1][1][1] = 0x99; + } + glyphCache.flushImage({{}, glyphCache.size()}); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt begin, UnsignedInt end, Containers::ArrayView) override { + return end - begin; + } + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + for(UnsignedInt i = 0; i != ids.size(); ++i) + ids[i] = i + 1; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + /* Just the simplest possible, rigorously tested in RendererTest + already */ + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + advances[i] = {2.0f, 0.0f}; + offsets[i] = {0.0f, i == 2 ? 1.0f : 0.0f}; + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D& clusters) const override { + /* Just to verify that the clusters get populated with meaningful + data */ + for(UnsignedInt i = 0; i != clusters.size(); ++i) + clusters[i] = 10 + i*3; + } + } shaper{font}; + + RendererGL renderer{glyphCache, data.flags}; + + /* Rendering with nothing inside should result in nothing */ + shader.draw(renderer.mesh()); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + if(!(_manager.loadState("AnyImageImporter") & PluginManager::LoadState::Loaded) || + !(_manager.loadState("TgaImporter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("AnyImageImporter / TgaImporter plugins not found."); + + CORRADE_COMPARE_WITH( + /* Use just one channel, the others are always the same */ + _framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels().slice(&Color4ub::r), + Utility::Path::join(TEXT_TEST_DIR, "render-nothing.tga"), + (DebugTools::CompareImageToFile{_manager, 0.0f, 0.0f})); + + /* This uploads indices if called */ + if(data.reserveBefore) { + renderer.reserve(data.reserveBefore, 0); + MAGNUM_VERIFY_NO_GL_ERROR(); + CORRADE_COMPARE(renderer.mesh().count(), 0); + } + + /* This may reupload indices if reserve() was called */ + if(data.indexTypeBefore) { + renderer.setIndexType(*data.indexTypeBefore); + MAGNUM_VERIFY_NO_GL_ERROR(); + CORRADE_COMPARE(renderer.indexType(), *data.indexTypeBefore); + CORRADE_COMPARE(renderer.mesh().count(), 0); + CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(*data.indexTypeBefore)); + } + + for(Int i = 0, iMax = 1 + (data.reset || data.clear); i != iMax; ++i) { + if(data.clear) + CORRADE_ITERATION("after clear"); + else if(data.reset) + CORRADE_ITERATION("after reset"); + + /* This uploads indices if reserve() wasn't called */ + renderer + .setAlignment(Alignment::LineLeft) + .setCursor({-3.0f, 1.0f}) + .render(shaper, 1.0f, "abc"); + + /* This uploads indices if reserve() wasn't called or was too little */ + renderer + .setAlignment(Alignment::LineRight) + .setCursor({5.0f, -3.0f}) + .render(shaper, 2.0f, "de"); + + /* This may reupload indices if called */ + if(data.indexTypeAfter) { + renderer.setIndexType(*data.indexTypeAfter); + MAGNUM_VERIFY_NO_GL_ERROR(); + CORRADE_COMPARE(renderer.indexType(), *data.indexTypeAfter); + CORRADE_COMPARE(renderer.mesh().count(), 5*6); + CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(*data.indexTypeAfter)); + } + + /* This may reupload indices and vertices if called */ + if(data.reserveAfter) { + renderer.reserve(data.reserveAfter, 0); + MAGNUM_VERIFY_NO_GL_ERROR(); + CORRADE_COMPARE(renderer.mesh().count(), 5*6); + } + + MAGNUM_VERIFY_NO_GL_ERROR(); + CORRADE_COMPARE(renderer.indexType(), data.expectedIndexType); + CORRADE_COMPARE(renderer.mesh().count(), 5*6); + CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(data.expectedIndexType)); + + /* If glyph positions and clusters are enabled, verify they got filled as + well. Again just to be sure that the operation is done at all, + thoroughly tested in RendererTest already. */ + if(data.flags & RendererGLFlag::GlyphPositionsClusters) { + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + {-3.0f, 1.0f}, /* a */ + {-1.0f, 1.0f}, /* b */ + { 1.0f, 2.0f}, /* c */ + + {-3.0f, -3.0f}, /* d */ + { 1.0f, -3.0f}, /* e */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({ + 10u, 13u, 16u, + 10u, 13u + }), TestSuite::Compare::Container); + } + + /* Verify the index and vertex data are generated as expected */ + if(data.expectedIndexType == MeshIndexType::UnsignedByte) + CORRADE_COMPARE_AS(renderer.indices(), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, + 16, 17, 18, 18, 17, 19 + }), TestSuite::Compare::Container); + else if(data.expectedIndexType == MeshIndexType::UnsignedShort) + CORRADE_COMPARE_AS(renderer.indices(), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, + 16, 17, 18, 18, 17, 19 + }), TestSuite::Compare::Container); + else if(data.expectedIndexType == MeshIndexType::UnsignedInt) + CORRADE_COMPARE_AS(renderer.indices(), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, + 16, 17, 18, 18, 17, 19 + }), TestSuite::Compare::Container); + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + + CORRADE_COMPARE_AS(renderer.vertexPositions(), Containers::arrayView({ + {-3.0f, 1.0f}, /* a */ + {-2.0f, 1.0f}, + {-3.0f, 2.0f}, + {-2.0f, 2.0f}, + + {-1.0f, 1.0f}, /* b, rect */ + { 0.0f, 1.0f}, + {-1.0f, 3.0f}, + { 0.0f, 3.0f}, + + { 1.0f, 2.0f}, /* c */ + { 2.0f, 2.0f}, + { 1.0f, 3.0f}, + { 2.0f, 3.0f}, + + {-3.0f, -3.0f}, /* d */ + {-1.0f, -3.0f}, + {-3.0f, -1.0f}, + {-1.0f, -1.0f}, + + { 1.0f, -3.0f}, /* e, rect */ + { 3.0f, -3.0f}, + { 1.0f, 1.0f}, + { 3.0f, 1.0f}, + }), TestSuite::Compare::Container); + + Vector3 expectedTextureCoordinates[]{ + {0.5f, 0.5f, Float(data.glyphCacheArraySize - 1)}, /* a */ + {1.0f, 0.5f, Float(data.glyphCacheArraySize - 1)}, + {0.5f, 1.0f, Float(data.glyphCacheArraySize - 1)}, + {1.0f, 1.0f, Float(data.glyphCacheArraySize - 1)}, + + {0.0f, 0.0f, 0.0f}, /* b, rect */ + {0.5f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, 1.0f, 0.0f}, + + {0.5f, 0.0f, Float(data.glyphCacheArraySize/2)}, /* c */ + {1.0f, 0.0f, Float(data.glyphCacheArraySize/2)}, + {0.5f, 0.5f, Float(data.glyphCacheArraySize/2)}, + {1.0f, 0.5f, Float(data.glyphCacheArraySize/2)}, + + {0.5f, 0.5f, Float(data.glyphCacheArraySize - 1)}, /* d; same as a */ + {1.0f, 0.5f, Float(data.glyphCacheArraySize - 1)}, + {0.5f, 1.0f, Float(data.glyphCacheArraySize - 1)}, + {1.0f, 1.0f, Float(data.glyphCacheArraySize - 1)}, + + {0.0f, 0.0f, 0.0f}, /* e, rect; same as b */ + {0.5f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.5f, 1.0f, 0.0f}, + }; + #ifndef MAGNUM_TARGET_GLES2 + if(data.glyphCacheArraySize != 1) { + CORRADE_COMPARE_AS(renderer.vertexTextureArrayCoordinates(), + Containers::arrayView(expectedTextureCoordinates), + TestSuite::Compare::Container); + } else + #endif + { + CORRADE_COMPARE_AS(renderer.vertexTextureCoordinates(), + Containers::stridedArrayView(expectedTextureCoordinates).slice(&Vector3::xy), + TestSuite::Compare::Container); + } + + /* Draw the generated mesh */ + _framebuffer.clear(GL::FramebufferClear::Color); + shader.draw(renderer.mesh()); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + /* light gray square, dark gray rect, white square on top left + large light gray square, large dark gray rect on bottom right */ + CORRADE_COMPARE_WITH( + /* Use just one channel, the others are always the same */ + _framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels().slice(&Color4ub::r), + Utility::Path::join(TEXT_TEST_DIR, "render.tga"), + (DebugTools::CompareImageToFile{_manager, 0.0f, 0.0f})); + + /* If resetting or clearing, there's another iteration of all above. + Verify that it calls correct parent reset or clear by checking + whether the cursor stays as before or not. */ + if(data.reset || data.clear) { + if(data.reset) { + renderer.reset(); + CORRADE_COMPARE(renderer.mesh().count(), 0); + CORRADE_COMPARE(renderer.cursor(), Vector2{}); + } else { + renderer.clear(); + CORRADE_COMPARE(renderer.cursor(), (Vector2{5.0f, -3.0f})); + } + + /* The index type should stay even after clear / reset */ + CORRADE_COMPARE(renderer.indexType(), data.expectedIndexType); + CORRADE_COMPARE(renderer.mesh().count(), 0); + CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(data.expectedIndexType)); + + /* Rendering after a reset or clear should result in nothing + again */ + _framebuffer.clear(GL::FramebufferClear::Color); + shader.draw(renderer.mesh()); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + CORRADE_COMPARE_WITH( + /* Use just one channel, the others are always the same */ + _framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels().slice(&Color4ub::r), + Utility::Path::join(TEXT_TEST_DIR, "render-nothing.tga"), + (DebugTools::CompareImageToFile{_manager, 0.0f, 0.0f})); + } + } + + /* Clearing twice in a row should not result in anything different */ + if(data.reset || data.clear) { + if(data.reset) { + renderer.reset(); + CORRADE_COMPARE(renderer.mesh().count(), 0); + CORRADE_COMPARE(renderer.cursor(), Vector2{}); + } else { + renderer.clear(); + CORRADE_COMPARE(renderer.cursor(), (Vector2{5.0f, -3.0f})); + } + + /* The index type should stay even after clear / reset */ + CORRADE_COMPARE(renderer.indexType(), data.expectedIndexType); + CORRADE_COMPARE(renderer.mesh().count(), 0); + CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(data.expectedIndexType)); + } +} + +void RendererGLTest::renderIndexTypeChanged() { + /* Verifies that an index type change happening inside render() due to + there being too many glyphs is correctly propagated to the GL mesh. A + trimmed-down version of renderClearReset() that verifies just the image + output, because that's the only place where it can be detected. */ + + GlyphCacheGL glyphCache{PixelFormat::R8Unorm, {2, 2}, {}}; + glyphCache.texture() + .setMinificationFilter(SamplerFilter::Nearest) + .setMagnificationFilter(SamplerFilter::Nearest); + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float) override { + _opened = true; + /* Compared to renderClearReset(), the line height is 0 so we can + render the 256 glyph prefix on the same spot without having to + adjust the cursor to place the next line correctly */ + return {1.0f, 2.0f, -1.0f, 0.0f, 0}; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + + private: + bool _opened = false; + } font; + font.openFile("", 1.0f); + + /* We have many glyphs from the initial prefix, only the first three + resolve to a valid one */ + UnsignedInt fontId = glyphCache.addFont(260, &font); + glyphCache.addGlyph(fontId, 3, {}, + Range2Di::fromSize({1, 0}, {1, 1})); + glyphCache.addGlyph(fontId, 1, {}, + Range2Di::fromSize({1, 1}, {1, 1})); + glyphCache.addGlyph(fontId, 2, {}, + Range2Di::fromSize({0, 0}, {1, 2})); + { + const Containers::StridedArrayView2D pixels = glyphCache.image().pixels()[0]; + pixels[0][1] = 0xff; + pixels[0][0] = 0x33; + pixels[1][0] = 0x33; + pixels[1][1] = 0x99; + } + glyphCache.flushImage({{}, glyphCache.size()}); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt begin, UnsignedInt end, Containers::ArrayView) override { + return end - begin; + } + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + for(UnsignedInt i = 0; i != ids.size(); ++i) + ids[i] = i + 1; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + advances[i] = {2.0f, 0.0f}; + offsets[i] = {0.0f, i == 2 ? 1.0f : 0.0f}; + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} + } shaper{font}; + + RendererGL renderer{glyphCache}; + /* Set a small index type to only have to render 256 glyphs to make it + change, not 16k */ + renderer.setIndexType(MeshIndexType::UnsignedByte); + + /* 16*16 glyphs. Index type doesn't change yet, only after render(). */ + renderer + .setAlignment(Alignment::LineLeft) + .setCursor({-3.0f, 1.0f}) + .add(shaper, 1.0f, "0123456789abcdef"_s*16); + CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedByte); + CORRADE_COMPARE(renderer.mesh().indexType(), GL::MeshIndexType::UnsignedByte); + + /* This should then cause the index type change, and the GL mesh should + adapt to it */ + renderer.render(shaper, 1.0f, "\nabc"); + CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(renderer.mesh().indexType(), GL::MeshIndexType::UnsignedShort); + + /* Just to match the image made in renderClearReset(), nothing else. There + should be 256 + 5 glyphs in total. */ + renderer + .setAlignment(Alignment::LineRight) + .setCursor({5.0f, -3.0f}) + .render(shaper, 2.0f, "de"); + CORRADE_COMPARE(renderer.glyphCount(), 256 + 5); + + /* Draw just the suffix from the mesh, not everything */ + renderer.mesh() + .setIndexOffset(256*6) + .setCount(5*6); + + Shaders::VectorGL2D shader; + shader.setTransformationProjectionMatrix(Matrix3::projection(Vector2{RenderSize})) + .bindVectorTexture(glyphCache.texture()) + .draw(renderer.mesh()); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + CORRADE_COMPARE_WITH( + /* Use just one channel, the others are always the same */ + _framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels().slice(&Color4ub::r), + Utility::Path::join(TEXT_TEST_DIR, "render.tga"), + (DebugTools::CompareImageToFile{_manager, 0.0f, 0.0f})); } struct TestShaper: AbstractShaper { diff --git a/src/Magnum/Text/Test/RendererGL_Test.cpp b/src/Magnum/Text/Test/RendererGL_Test.cpp new file mode 100644 index 000000000..3b47294f2 --- /dev/null +++ b/src/Magnum/Text/Test/RendererGL_Test.cpp @@ -0,0 +1,74 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, 2024, 2025 + Vladimír Vondruš + + 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. +*/ + +#include +#include + +#include "Magnum/Text/RendererGL.h" + +namespace Magnum { namespace Text { namespace Test { namespace { + +struct RendererGL_Test: TestSuite::Tester { + explicit RendererGL_Test(); + + void debugFlag(); + void debugFlags(); + + void constructNoCreate(); +}; + +RendererGL_Test::RendererGL_Test() { + addTests({&RendererGL_Test::debugFlag, + &RendererGL_Test::debugFlags, + + &RendererGL_Test::constructNoCreate}); +} + +void RendererGL_Test::debugFlag() { + Containers::String out; + Debug{&out} << RendererGLFlag::GlyphPositionsClusters << RendererGLFlag(0xca); + CORRADE_COMPARE(out, "Text::RendererGLFlag::GlyphPositionsClusters Text::RendererGLFlag(0xca)\n"); +} + +void RendererGL_Test::debugFlags() { + Containers::String out; + Debug{&out} << (RendererGLFlag::GlyphPositionsClusters|RendererGLFlag(0xf0)) << RendererGLFlags{}; + CORRADE_COMPARE(out, "Text::RendererGLFlag::GlyphPositionsClusters|Text::RendererGLFlag(0xf0) Text::RendererGLFlags{}\n"); +} + +void RendererGL_Test::constructNoCreate() { + RendererGL renderer{NoCreate}; + + /* Shouldn't crash */ + CORRADE_VERIFY(true); + + /* Implicit construction is not allowed */ + CORRADE_VERIFY(!std::is_convertible::value); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Text::Test::RendererGL_Test) diff --git a/src/Magnum/Text/Test/render-nothing.tga b/src/Magnum/Text/Test/render-nothing.tga new file mode 100644 index 0000000000000000000000000000000000000000..abc15474cea2c74b47c1d3d0921693510a230bfb GIT binary patch literal 20 ScmZQz;AVgU4h9h1F9-kv`2h3) literal 0 HcmV?d00001 diff --git a/src/Magnum/Text/Test/render.tga b/src/Magnum/Text/Test/render.tga new file mode 100644 index 0000000000000000000000000000000000000000..18daa0ded15271863828b3f7a91a17cca80830a0 GIT binary patch literal 56 tcmZQz;AVgU4h9hH5Nw>;DA;HW#9+D=$Ohp?L8h64#%+R3#)AJl1px=}42l2% literal 0 HcmV?d00001 diff --git a/src/Magnum/Text/Text.h b/src/Magnum/Text/Text.h index c7dbe3c0c..c5e9d7cae 100644 --- a/src/Magnum/Text/Text.h +++ b/src/Magnum/Text/Text.h @@ -73,6 +73,7 @@ class AbstractRenderer; template class BasicRenderer; typedef BasicRenderer<2> Renderer2D; typedef BasicRenderer<3> Renderer3D; +class RendererGL; #endif }}