From 84809128b10c35cb8067ba78c650ae438c28e795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 7 Nov 2024 22:15:38 +0100 Subject: [PATCH] Text: new RendererCore class. A higher-level stateful wrapper around the low-level utilities, capable of rendering text formed from multiple lines and multiple fonts. Will be used as a backend for a new Renderer / RendererGL implementation. No usage docs yet, those will come once the other two classes are made as well. --- src/Magnum/Text/CMakeLists.txt | 3 +- .../Text/Implementation/rendererState.h | 123 + src/Magnum/Text/Renderer.cpp | 723 +++- src/Magnum/Text/Renderer.h | 569 ++- src/Magnum/Text/Test/RendererTest.cpp | 3320 +++++++++++++++++ src/Magnum/Text/Text.h | 2 + 6 files changed, 4735 insertions(+), 5 deletions(-) create mode 100644 src/Magnum/Text/Implementation/rendererState.h diff --git a/src/Magnum/Text/CMakeLists.txt b/src/Magnum/Text/CMakeLists.txt index 60d876663..37914ae94 100644 --- a/src/Magnum/Text/CMakeLists.txt +++ b/src/Magnum/Text/CMakeLists.txt @@ -60,7 +60,8 @@ set(MagnumText_HEADERS visibility.h) set(MagnumText_PRIVATE_HEADERS - Implementation/printFourCC.h) + Implementation/printFourCC.h + Implementation/rendererState.h) if(MAGNUM_TARGET_GL) list(APPEND MagnumText_GracefulAssert_SRCS diff --git a/src/Magnum/Text/Implementation/rendererState.h b/src/Magnum/Text/Implementation/rendererState.h new file mode 100644 index 000000000..e831ee7d4 --- /dev/null +++ b/src/Magnum/Text/Implementation/rendererState.h @@ -0,0 +1,123 @@ +#ifndef Magnum_Text_Implementation_rendererState_h +#define Magnum_Text_Implementation_rendererState_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. +*/ + +#include "Magnum/Text/Renderer.h" + +#include +#include +#include + +#include "Magnum/Math/Range.h" +#include "Magnum/Text/Alignment.h" +#include "Magnum/Text/Direction.h" + +namespace Magnum { namespace Text { + +/* Is inherited by RendererCore::AllocatorState to avoid extra allocations */ +struct RendererCore::State { + /* Gets called by RendererCore only if both allocators are specified by the + user. If not, AllocatorState is constructed instead. */ + explicit State(const AbstractGlyphCache& glyphCache, void(*glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&), void* glyphAllocatorState, void(*const runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* runAllocatorState, RendererCoreFlags flags): glyphCache(glyphCache), glyphAllocator{glyphAllocator}, glyphAllocatorState{glyphAllocatorState}, runAllocator{runAllocator}, runAllocatorState{runAllocatorState}, flags{flags} { + CORRADE_INTERNAL_DEBUG_ASSERT(glyphAllocator && runAllocator); + } + + /* AllocatorState, Renderer::State etc. inherit from this with the instance + deleted through the base pointer */ + virtual ~State() = default; + + const AbstractGlyphCache& glyphCache; + void(*const glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&); + void* const glyphAllocatorState; + void(*const runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&); + void* const runAllocatorState; + const RendererCoreFlags flags; + + /* These are controllable by various setters. Keep these values in sync + with code in reset(). */ + Alignment alignment = Alignment::MiddleCenter; + LayoutDirection layoutDirection = LayoutDirection::HorizontalTopToBottom; + /* 1 byte free */ + Vector2 cursor; + Float lineAdvance = 0.0f; + + /* Capacity is the array size. The "rendering" value is glyphs from the + add() calls since the last render() or clear(), i.e. ones that aren't + fully aligned and such yet. */ + UnsignedInt glyphCount = 0, renderingGlyphCount = 0; + Containers::StridedArrayView1D glyphPositions; + Containers::StridedArrayView1D glyphIds; + /* Non-empty only if RendererFlag::GlyphClusters is set */ + Containers::StridedArrayView1D glyphClusters; + Containers::StridedArrayView1D glyphAdvances; + + /* Capacity is the array size. The "rendering" value is again runs from the + add() calls since the last render() or clear(). */ + UnsignedInt runCount = 0, renderingRunCount = 0; + Containers::StridedArrayView1D runScales; + Containers::StridedArrayView1D runEnds; + + /* Rendering state */ + bool rendering = false; + /* 1 byte free */ + Containers::Optional resolvedAlignment; + /* Both are a zero vector initially, the first track the current line start + and the second position within the current line. The actual `cursor` is + added to all glyph positions only at the end. */ + Vector2 renderingLineStart, renderingLineCursor; + /* On add(), if zero, is set to lineAdvance (if non-zero) or (scaled) line + advance of the first used font */ + /** @todo might want to vary line advance per line based on the actual + metrics of used fonts (it looks uglily uneven though!), would need to + keep track of max descent, max ascent and max line gap of fonts used on + previous and next line and calculate the actual line advance from + prev line max descent + max of prev and current line line gap + max of + current line ascent, and then shift the whole like by that once it's + done instead of advancing directly the cursor before */ + Vector2 renderingLineAdvance; + /* Everything until runCount is a block that needs to be aligned */ + UnsignedInt blockRunBegin = 0; + Range2D blockRectangle; + /* Everything until runCount is a line that needs to be aligned */ + UnsignedInt lineGlyphBegin = 0; + Range2D lineRectangle; +}; + +/* Instantiated only if the builtin allocators are used. See their contents for + the types being stored here. */ +struct RendererCore::AllocatorState: RendererCore::State { + /* Defined in Renderer.cpp because it needs access to default allocator + implementations */ + explicit AllocatorState(const AbstractGlyphCache& glyphCache, void(*glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&), void* glyphAllocatorState, void(*const runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* runAllocatorState, RendererCoreFlags flags); + + Containers::Array glyphData; + Containers::Array runData; +}; + +}} + +#endif diff --git a/src/Magnum/Text/Renderer.cpp b/src/Magnum/Text/Renderer.cpp index bca3afeba..0308ea523 100644 --- a/src/Magnum/Text/Renderer.cpp +++ b/src/Magnum/Text/Renderer.cpp @@ -26,22 +26,39 @@ #include "Renderer.h" -#include +#include +#include #include +#include +#include #include #include "Magnum/Math/Functions.h" +#include "Magnum/Math/FunctionsBatch.h" #include "Magnum/Math/Range.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/Text/AbstractShaper.h" #include "Magnum/Text/Alignment.h" #include "Magnum/Text/Direction.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 #ifdef MAGNUM_TARGET_GL #include #include #include -#include #include /** @todo remove once Renderer is STL-free */ #include /** @todo remove once Renderer is STL-free */ @@ -50,11 +67,711 @@ #include "Magnum/GL/Extensions.h" #include "Magnum/GL/Mesh.h" #include "Magnum/Shaders/GenericGL.h" -#include "Magnum/Text/AbstractShaper.h" #endif namespace Magnum { namespace Text { +Debug& operator<<(Debug& debug, const RendererCoreFlag value) { + debug << "Text::RendererCoreFlag" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case RendererCoreFlag::v: return debug << "::" #v; + _c(GlyphClusters) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << Debug::hex << UnsignedByte(value) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const RendererCoreFlags value) { + return Containers::enumSetDebugOutput(debug, value, "Text::RendererCoreFlags{}", { + RendererCoreFlag::GlyphClusters + }); +} + +namespace { + +struct Glyph { + Vector2 position; + UnsignedInt id; +}; + +struct GlyphCluster { + Vector2 position; + UnsignedInt id; + UnsignedInt cluster; +}; + +auto defaultGlyphAllocatorFor(const RendererCoreFlags flags) -> void(*)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&) { + if(flags >= RendererCoreFlag::GlyphClusters) + return [](void* const state, const UnsignedInt glyphCount, Containers::StridedArrayView1D& glyphPositions, Containers::StridedArrayView1D& glyphIds, Containers::StridedArrayView1D* const glyphClusters, Containers::StridedArrayView1D& glyphAdvances) { + Containers::Array& data = *static_cast*>(state); + /* The array may not be fully used yet, or it might have been reset + back to empty. Append only if the desired capacity is more than + what's there. */ + const std::size_t existingSize = glyphPositions.size(); + const std::size_t desiredByteSize = (existingSize + glyphCount)*sizeof(GlyphCluster); + if(desiredByteSize > data.size()) { + /* Using arrayAppend() as it reallocates with a growth + strategy, arrayResize() would take the size literally */ + arrayAppend(data, NoInit, desiredByteSize - data.size()); + } + /* The new capacity is the actual array size, not just + `desiredByteSize`. If the array got enlarged by exactly the + requested `size`, it'll be the same as `desiredByteSize`. If the + array was larger, such as after a clear(), the capacity will + again use up all of it. */ + const Containers::StridedArrayView1D glyphs = Containers::arrayCast(data); + glyphPositions = glyphs.slice(&GlyphCluster::position); + glyphIds = glyphs.slice(&GlyphCluster::id); + *glyphClusters = glyphs.slice(&GlyphCluster::cluster); + /* As IDs and clusters are right after each other and have the same + size as a Vector2, we can abuse them to store advances. Those + are guaranteed to be always filled only once advances are no + longer needed, so that's fine -- but we need to ensure that we + point to the new memory, not to the existing, where it'd + overwrite existing IDs and clusters. */ + glyphAdvances = Containers::arrayCast(glyphs.slice(&GlyphCluster::id).exceptPrefix(existingSize)); + }; + else + return [](void* const state, const UnsignedInt glyphCount, Containers::StridedArrayView1D& glyphPositions, Containers::StridedArrayView1D& glyphIds, Containers::StridedArrayView1D*, Containers::StridedArrayView1D& glyphAdvances) { + Containers::Array& data = *static_cast*>(state); + /* The array may not be fully used yet, or it might have been reset + back to empty. Append only if the desired capacity is more than + what's there. Unlike above we don't have any place to alias + advances with, so append them at the end. */ + const std::size_t desiredByteSize = glyphPositions.size()*sizeof(Glyph) + glyphCount*(sizeof(Glyph) + sizeof(Vector2)); + if(desiredByteSize > data.size()) { + /* Using arrayAppend() as it reallocates with a growth + strategy, arrayResize() would take the size literally */ + arrayAppend(data, NoInit, desiredByteSize - data.size()); + } + /* Calculate the new capacity from the actual array size. Compared + to the above, we need to make sure the unused space at the end + is correctly divided between the Glyph and the Vector2 for + advances. */ + const std::size_t newCapacity = glyphPositions.size() + (data.size() - glyphPositions.size()*sizeof(Glyph))/(sizeof(Glyph) + sizeof(Vector2)); + const std::size_t newSize = newCapacity - glyphPositions.size(); + + const Containers::StridedArrayView1D glyphs = Containers::arrayCast(data.prefix(newCapacity*sizeof(Glyph))); + glyphPositions = glyphs.slice(&Glyph::position); + glyphIds = glyphs.slice(&Glyph::id); + /* Don't take just the suffix for advances as the size may not be + divisible by sizeof(Vector2), especially after clear() */ + glyphAdvances = Containers::arrayCast(data.sliceSize(newCapacity*sizeof(Glyph), newSize*sizeof(Vector2))); + }; +} + +struct TextRun { + Float scale; + UnsignedInt end; +}; + +void defaultRunAllocator(void* const state, const UnsignedInt runCount, Containers::StridedArrayView1D& runScales, Containers::StridedArrayView1D& runEnds) { + Containers::Array& data = *static_cast*>(state); + + const std::size_t newSize = runScales.size() + runCount; + const std::size_t desiredByteSize = newSize*sizeof(TextRun); + if(desiredByteSize > data.size()) { + /* Using arrayAppend() as it reallocates with a growth strategy, + arrayResize() would take the size literally */ + arrayAppend(data, NoInit, desiredByteSize - data.size()); + } + + const Containers::StridedArrayView1D runs = Containers::arrayCast(data); + runScales = runs.slice(&TextRun::scale); + runEnds = runs.slice(&TextRun::end); +} + +} + +RendererCore::AllocatorState::AllocatorState(const AbstractGlyphCache& glyphCache, void(*glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&), void* glyphAllocatorState, void(*const runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* runAllocatorState, RendererCoreFlags flags): State{glyphCache, + glyphAllocator ? glyphAllocator : defaultGlyphAllocatorFor(flags), + glyphAllocator ? glyphAllocatorState : &glyphData, + runAllocator ? runAllocator : defaultRunAllocator, + runAllocator ? runAllocatorState : &runData, flags} {} + +RendererCore::RendererCore(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, const RendererCoreFlags flags): _state{ + /* If either allocator is left at the default, create a state that includes + the data arrays for use by the internal allocators. If both are + user-specified, there's no need to have them as they're unused. */ + glyphAllocator && runAllocator ? + Containers::pointer(glyphCache, glyphAllocator, glyphAllocatorState, runAllocator, runAllocatorState, flags) : + Containers::pointer(glyphCache, glyphAllocator, glyphAllocatorState, runAllocator, runAllocatorState, flags)} {} + +RendererCore::RendererCore(NoCreateT) noexcept {} + +RendererCore::RendererCore(RendererCore&&) noexcept = default; + +RendererCore::~RendererCore() = default; + +RendererCore& RendererCore::operator=(RendererCore&&) noexcept = default; + +const AbstractGlyphCache& RendererCore::glyphCache() const { + return _state->glyphCache; +} + +RendererCoreFlags RendererCore::flags() const { + return _state->flags; +} + +UnsignedInt RendererCore::glyphCount() const { + return _state->glyphCount; +} + +UnsignedInt RendererCore::glyphCapacity() const { + return _state->glyphPositions.size(); +} + +UnsignedInt RendererCore::runCount() const { + return _state->runCount; +} + +UnsignedInt RendererCore::runCapacity() const { + return _state->runScales.size(); +} + +bool RendererCore::isRendering() const { + return _state->rendering; +} + +UnsignedInt RendererCore::renderingGlyphCount() const { + return _state->renderingGlyphCount; +} + +UnsignedInt RendererCore::renderingRunCount() const { + return _state->renderingRunCount; +} + +Vector2 RendererCore::cursor() const { + return _state->cursor; +} + +RendererCore& RendererCore::setCursor(const Vector2& cursor) { + State& state = *_state; + CORRADE_ASSERT(!state.rendering, + "Text::RendererCore::setCursor(): rendering in progress", *this); + state.cursor = cursor; + return *this; +} + +Alignment RendererCore::alignment() const { + return _state->alignment; +} + +RendererCore& RendererCore::setAlignment(const Alignment alignment) { + State& state = *_state; + CORRADE_ASSERT(!state.rendering, + "Text::RendererCore::setAlignment(): rendering in progress", *this); + state.alignment = alignment; + return *this; +} + +Float RendererCore::lineAdvance() const { + return _state->lineAdvance; +} + +RendererCore& RendererCore::setLineAdvance(const Float advance) { + State& state = *_state; + CORRADE_ASSERT(!state.rendering, + "Text::RendererCore::setLineAdvance(): rendering in progress", *this); + state.lineAdvance = advance; + return *this; +} + +LayoutDirection RendererCore::layoutDirection() const { + return _state->layoutDirection; +} + +RendererCore& RendererCore::setLayoutDirection(const LayoutDirection direction) { + State& state = *_state; + CORRADE_ASSERT(!state.rendering, + "Text::RendererCore::setLayoutDirection(): rendering in progress", *this); + CORRADE_ASSERT(direction == LayoutDirection::HorizontalTopToBottom, + "Text::RendererCore::setLayoutDirection(): only" << LayoutDirection::HorizontalTopToBottom << "is supported right now, got" << direction, *this); + state.layoutDirection = direction; + return *this; +} + +Containers::StridedArrayView1D RendererCore::glyphPositions() const { + const State& state = *_state; + return state.glyphPositions.prefix(state.glyphCount); +} + +Containers::StridedArrayView1D RendererCore::glyphIds() const { + const State& state = *_state; + return state.glyphIds.prefix(state.glyphCount); +} + +Containers::StridedArrayView1D RendererCore::glyphClusters() const { + const State& state = *_state; + CORRADE_ASSERT(state.flags & RendererCoreFlag::GlyphClusters, + "Text::RendererCore::glyphClusters(): glyph clusters not enabled", {}); + return state.glyphClusters.prefix(state.glyphCount); +} + +Containers::StridedArrayView1D RendererCore::runScales() const { + const State& state = *_state; + return state.runScales.prefix(state.runCount); +} + +Containers::StridedArrayView1D RendererCore::runEnds() const { + const State& state = *_state; + return state.runEnds.prefix(state.runCount); +} + +Range1Dui RendererCore::glyphsForRuns(const Range1Dui& runRange) const { + const State& state = *_state; + CORRADE_ASSERT(runRange.min() <= state.renderingRunCount && + runRange.max() <= state.renderingRunCount, + /* The Vector2ui is to avoid double {}s in the printed value */ + /** @todo fix the printer itself, maybe? */ + "Text::RendererCore::glyphsForRuns(): runs" << Debug::packed << (Vector2ui{runRange.min(), runRange.max()}) << "out of range for" << state.renderingRunCount << "runs", {}); + return {runRange.min() ? state.runEnds[runRange.min() - 1] : 0, + runRange.max() ? state.runEnds[runRange.max() - 1] : 0}; +} + +void RendererCore::allocateGlyphs( + #ifndef CORRADE_NO_ASSERT + const char* const messagePrefix, + #endif + const UnsignedInt totalGlyphCount) +{ + State& state = *_state; + + /* This function should only be called if we need more memory or from + clear() with everything empty */ + CORRADE_INTERNAL_DEBUG_ASSERT(totalGlyphCount > state.glyphPositions.size() || (state.glyphCount == 0 && state.renderingGlyphCount == 0 && totalGlyphCount == 0)); + + /* Sliced copies of the views for the allocator to update. As this is + called from add(), all glyph contents until `state.renderingGlyphCount` + should be preserved, not just `state.glyphCount`. */ + Containers::StridedArrayView1D glyphPositions = + state.glyphPositions.prefix(state.renderingGlyphCount); + Containers::StridedArrayView1D glyphIds = + state.glyphIds.prefix(state.renderingGlyphCount); + Containers::StridedArrayView1D glyphClusters = state.flags & RendererCoreFlag::GlyphClusters ? + state.glyphClusters.prefix(state.renderingGlyphCount) : nullptr; + /* Advances are just temporary and thus we don't need to preserve existing + contents. But the allocator may still want to know where it's coming + from so give it a non-null empty view if possible */ + Containers::StridedArrayView1D glyphAdvances = + state.glyphAdvances.prefix(0); + + /* While this function gets total glyph count, the allocator gets glyph + count to grow by instead */ + state.glyphAllocator(state.glyphAllocatorState, + totalGlyphCount - state.renderingGlyphCount, + glyphPositions, + glyphIds, + state.flags & RendererCoreFlag::GlyphClusters ? &glyphClusters : nullptr, + glyphAdvances); + /* Take the smallest size of all as the new capacity. Again the advances + don't preserve the previous contents so they're just the new size. Add + the existing glyph count to that instead of subtracting glyph count from + all others to avoid an underflow. */ + std::size_t minCapacity = Math::min({ + glyphPositions.size(), + glyphIds.size(), + state.renderingGlyphCount + glyphAdvances.size()}); + /* These assertions are present even for the builtin allocator but + shouldn't fire. If they do, the whole thing is broken, but it's better + to blow up with a nice message than with some strange OOB error later */ + if(state.flags & RendererCoreFlag::GlyphClusters) { + minCapacity = Math::min(minCapacity, glyphClusters.size()); + CORRADE_ASSERT(minCapacity >= totalGlyphCount, + messagePrefix << "expected allocated glyph positions, IDs and clusters to have at least" << totalGlyphCount << "elements and advances" << totalGlyphCount - state.renderingGlyphCount << "but got" << glyphPositions.size() << Debug::nospace << "," << glyphIds.size() << Debug::nospace << "," << glyphClusters.size() << "and" << glyphAdvances.size(), ); + } else { + CORRADE_ASSERT(minCapacity >= totalGlyphCount, + messagePrefix << "expected allocated glyph positions and IDs to have at least" << totalGlyphCount << "elements and advances" << totalGlyphCount - state.renderingGlyphCount << "but got" << glyphPositions.size() << Debug::nospace << "," << glyphIds.size() << "and" << glyphAdvances.size(), ); + } + + /* Keep just the minimal size for all, which is the new capacity */ + state.glyphPositions = glyphPositions.prefix(minCapacity); + state.glyphIds = glyphIds.prefix(minCapacity); + if(state.flags & RendererCoreFlag::GlyphClusters) + state.glyphClusters = glyphClusters.prefix(minCapacity); + /* Again the advances are just the size alone, not the full capacity */ + state.glyphAdvances = glyphAdvances.prefix(minCapacity - state.renderingGlyphCount); +} + +void RendererCore::allocateRuns( + #ifndef CORRADE_NO_ASSERT + const char* const messagePrefix, + #endif + const UnsignedInt totalRunCount) +{ + State& state = *_state; + + /* This function should only be called if we need more memory or from + clear() with everything empty */ + CORRADE_INTERNAL_DEBUG_ASSERT(totalRunCount > state.runScales.size() || (state.runCount == 0 && state.renderingRunCount == 0 && totalRunCount == 0)); + + /* Sliced copies of the views for the allocator to update. As this is + called from add(), all run contents until `state.renderingRunCount` + should be preserved, not just `state.runCount`. */ + Containers::StridedArrayView1D runScales = + state.runScales.prefix(state.renderingRunCount); + Containers::StridedArrayView1D runEnds = + state.runEnds.prefix(state.renderingRunCount); + + /* While this function gets total run count, the allocator gets run count + to grow by instead */ + state.runAllocator(state.runAllocatorState, + totalRunCount - state.renderingRunCount, + runScales, + runEnds); + /* Take the smallest size of all as the new capacity */ + const std::size_t minCapacity = Math::min( + runScales.size(), + runEnds.size()); + /* These assertions are present even for the builtin allocator but + shouldn't fire. If they do, the whole thing is broken, but it's better + to blow up with a nice message than with some strange OOB error later */ + CORRADE_ASSERT(minCapacity >= totalRunCount, + messagePrefix << "expected allocated run scales and ends to have at least" << totalRunCount << "elements but got" << runScales.size() << "and" << runEnds.size(), ); + + /* Keep just the minimal size for all, which is the new capacity */ + state.runScales = runScales.prefix(minCapacity); + state.runEnds = runEnds.prefix(minCapacity); +} + +RendererCore& RendererCore::reserve(const UnsignedInt glyphCapacity, const UnsignedInt runCapacity) { + State& state = *_state; + + if(state.glyphPositions.size() < glyphCapacity) + allocateGlyphs( + #ifndef CORRADE_NO_ASSERT + "Text::RendererCore::reserve():", + #endif + glyphCapacity); + if(state.runScales.size() < runCapacity) + allocateRuns( + #ifndef CORRADE_NO_ASSERT + "Text::RendererCore::reserve():", + #endif + runCapacity); + + return *this; +} + +RendererCore& RendererCore::clear() { + State& state = *_state; + + /* Reset the glyph / run count to 0 and call the allocators, requesting 0 + glyphs and runs as well. It may make use of that to refresh itself. */ + state.glyphCount = 0; + state.renderingGlyphCount = 0; + state.runCount = 0; + state.renderingRunCount = 0; + allocateGlyphs( + #ifndef CORRADE_NO_ASSERT + "", /* Asserts won't happen as returned sizes will be always >= 0 */ + #endif + 0); + allocateRuns( + #ifndef CORRADE_NO_ASSERT + "", /* Asserts won't happen as returned sizes will be always >= 0 */ + #endif + 0); + + /* All in-progress rendering, both for the block and for the line, should + be cleaned up */ + state.rendering = false; + state.resolvedAlignment = {}; + state.renderingLineStart = {}; + state.renderingLineCursor = {}; + state.renderingLineAdvance = {}; + state.blockRunBegin = {}; + state.blockRectangle = {}; + state.lineGlyphBegin = {}; + state.lineRectangle = {}; + + return *this; +} + +void RendererCore::resetInternal() { + State& state = *_state; + + /* Keep in sync with the initializers in the State struct */ + state.alignment = Alignment::MiddleCenter; + state.layoutDirection = LayoutDirection::HorizontalTopToBottom; + state.cursor = {}; + state.lineAdvance = {}; +} + +RendererCore& RendererCore::reset() { + clear(); + /* Reset also all other settable state to defaults */ + resetInternal(); + + return *this; +} + +void RendererCore::alignAndFinishLine() { + State& state = *_state; + CORRADE_INTERNAL_DEBUG_ASSERT(state.lineGlyphBegin != state.renderingGlyphCount && state.resolvedAlignment); + + const Range2D alignedLineRectangle = alignRenderedLine( + state.lineRectangle, + state.layoutDirection, + *state.resolvedAlignment, + state.glyphPositions.slice(state.lineGlyphBegin, state.renderingGlyphCount)); + + /* Extend the block rectangle with final line bounds */ + state.blockRectangle = Math::join(state.blockRectangle, alignedLineRectangle); + + /* New line starts after all existing glyphs and is empty */ + state.lineGlyphBegin = state.renderingGlyphCount; + state.lineRectangle = {}; +} + +RendererCore& RendererCore::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const Containers::ArrayView features) { + State& state = *_state; + + /* Mark as rendering in progress if not already */ + state.rendering = true; + + /* Query ID of shaper font in the cache for performing glyph ID mapping */ + const Containers::Optional glyphCacheFontId = state.glyphCache.findFont(shaper.font()); + CORRADE_ASSERT(glyphCacheFontId, + "Text::RendererCore::add(): shaper font not found among" << state.glyphCache.fontCount() << "fonts in associated glyph cache", *this); + + /* Scaling factor, line advance taken from the font if not specified + externally. Currently assuming just horizontal layout direction, so the + line advance is vertical. */ + /** @todo update once allowing other directions */ + const AbstractFont& font = shaper.font(); + const Float scale = size/font.size(); + CORRADE_INTERNAL_DEBUG_ASSERT(state.layoutDirection == LayoutDirection::HorizontalTopToBottom); + if(state.renderingLineAdvance == Vector2{}) { + if(state.lineAdvance != 0.0f) + state.renderingLineAdvance = Vector2::yAxis(-state.lineAdvance); + else + state.renderingLineAdvance = Vector2::yAxis(-font.lineHeight()*scale); + } + + Containers::StringView line = text.slice(begin, end); + while(line) { + const Containers::StringView lineEnd = line.findOr('\n', line.end()); + /* Comparing like this to avoid an unnecessary memcmp(). If we reach + the end of the input text, it's *not* an end of the line, because + the next add() call may continue with it. */ + const bool isEndOfLine = lineEnd.size() == 1 && lineEnd[0] == '\n'; + + /* If the line is not empty and produced some glyphs, render them */ + if(const UnsignedInt glyphCount = lineEnd.begin() != line.begin() ? shaper.shape(text, line.begin() - text.begin(), lineEnd.begin() - text.begin(), features) : 0) { + /* If we need to add more glyphs than what's in the capacity, + allocate more */ + if(state.glyphPositions.size() < state.renderingGlyphCount + glyphCount) { + allocateGlyphs( + #ifndef CORRADE_NO_ASSERT + "Text::RendererCore::add():", + #endif + state.renderingGlyphCount + glyphCount); + #ifdef CORRADE_GRACEFUL_ASSERT + /* For testing only -- if allocation failed, bail */ + if(state.glyphPositions.size() < state.renderingGlyphCount + glyphCount) + return *this; + #endif + } + + const Containers::StridedArrayView1D glyphOffsetsPositions = state.glyphPositions.sliceSize(state.renderingGlyphCount, glyphCount); + /* The glyph advance array may be aliasing IDs and clusters. Pick + only a suffix of the same size as the remaining capacity -- that + memory is guaranteed to be unused yet. */ + const std::size_t remainingCapacity = state.glyphPositions.size() - state.renderingGlyphCount; + const Containers::StridedArrayView1D glyphAdvances = state.glyphAdvances.sliceSize(state.glyphAdvances.size() - remainingCapacity, glyphCount); + shaper.glyphOffsetsAdvancesInto( + glyphOffsetsPositions, + glyphAdvances); + + /* Render line glyph positions, aliasing the offsets */ + const Range2D rectangle = renderLineGlyphPositionsInto( + shaper.font(), + size, + state.layoutDirection, + glyphOffsetsPositions, + glyphAdvances, + state.renderingLineCursor, + glyphOffsetsPositions); + + /* Retrieve the glyph IDs and clusters, convert the glyph IDs to + cache-global. Do it only after finalizing the positions so the + glyphAdvances array can alias the IDs. */ + const Containers::StridedArrayView1D glyphIds = state.glyphIds.sliceSize(state.renderingGlyphCount, glyphCount); + shaper.glyphIdsInto(glyphIds); + state.glyphCache.glyphIdsInto(*glyphCacheFontId, glyphIds, glyphIds); + if(state.flags & RendererCoreFlag::GlyphClusters) + shaper.glyphClustersInto(state.glyphClusters.sliceSize(state.renderingGlyphCount, glyphCount)); + + /* If we're aligning based on glyph bounds, calculate a rectangle + from scratch instead of using a rectangle based on advances and + font metrics. Join the resulting rectangle with one that's + maintained for the line so far. */ + state.lineRectangle = Math::join(state.lineRectangle, + (UnsignedByte(state.alignment) & Implementation::AlignmentGlyphBounds) ? + glyphQuadBounds(state.glyphCache, scale, glyphOffsetsPositions, glyphIds) : + rectangle); + + state.renderingGlyphCount += glyphCount; + } + + /* If the alignment isn't resolved yet and the shaper detected any + usable direction (or we're at the end of the line where we need + it), resolve it. If there's no usable direction detected yet, maybe + it will be next time. */ + if(!state.resolvedAlignment) { + /* In this case it may happen that we query direction on a shaper + for which shape() wasn't called yet, for example if shaping a + text starting with \n and the previous text shaping gave back + ShapeDirection::Unspecified as well. In such case it likely + returns ShapeDirection::Unspecified too. */ + const ShapeDirection shapeDirection = shaper.direction(); + if(shapeDirection != ShapeDirection::Unspecified || isEndOfLine) + state.resolvedAlignment = alignmentForDirection( + state.alignment, + state.layoutDirection, + shapeDirection); + } + + /* If a newline follows, wrap up the existing line. This can happen + independently of whether any glyphs were processed in this + iteration, as add() can be called with a string that starts with a + \n, for example. */ + if(isEndOfLine) { + /* If there are any glyphs on the current line, either added right + above or being there from the previous add() call, align them. + If alignment based on bounds is requested, calculate a special + rectangle for it. */ + if(state.lineGlyphBegin != state.renderingGlyphCount) + alignAndFinishLine(); + + /* Move the cursor for the next line */ + state.renderingLineStart += state.renderingLineAdvance; + state.renderingLineCursor = state.renderingLineStart; + } + + /* For the next iteration cut away everything that got processed, + including the \n */ + line = line.suffix(lineEnd.end()); + } + + /* Final alignment of the whole block happens in render() below */ + + /* Save the whole thing as a new run, if any glyphs were added at all. + Right now it's just a single run each time add() is called, but + eventually it might get split by lines or by shaping direction. */ + if((state.renderingRunCount ? state.runEnds[state.renderingRunCount - 1] : 0) < state.renderingGlyphCount) { + if(state.runScales.size() <= state.renderingRunCount) { + allocateRuns( + #ifndef CORRADE_NO_ASSERT + "Text::RendererCore::add():", + #endif + state.renderingRunCount + 1); + #ifdef CORRADE_GRACEFUL_ASSERT + /* For testing only -- if allocation failed, bail */ + if(state.runScales.size() <= state.renderingRunCount) + return *this; + #endif + } + state.runScales[state.renderingRunCount] = scale; + state.runEnds[state.renderingRunCount] = state.renderingGlyphCount; + ++state.renderingRunCount; + } + + return *this; +} + +RendererCore& RendererCore::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end) { + return add(shaper, size, text, begin, end, {}); +} + +RendererCore& RendererCore::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const std::initializer_list features) { + return add(shaper, size, text, begin, end, Containers::arrayView(features)); +} + +RendererCore& RendererCore::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const Containers::ArrayView features) { + return add(shaper, size, text, 0, text.size(), features); +} + +RendererCore& RendererCore::add(AbstractShaper& shaper, const Float size, const Containers::StringView text) { + return add(shaper, size, text, {}); +} + +RendererCore& RendererCore::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const std::initializer_list features) { + return add(shaper, size, text, Containers::arrayView(features)); +} + +Containers::Pair RendererCore::render() { + State& state = *_state; + + /* If the alignment still isn't resolved at this point, it means it either + stayed at ShapeDirection::Unspecified for all text added so far, or + there wasn't any text actually added. Go with whatever + alignmentForDirection() picks, then. Also, state.resolvedAlignment is + going to get reset right at the end, but let's just write there + temporarily, the logic is easier that way. */ + if(!state.resolvedAlignment) + state.resolvedAlignment = alignmentForDirection( + state.alignment, + state.layoutDirection, + ShapeDirection::Unspecified); + + /* Align the last unfinished line. In most cases there will be, unless the + last text passed to add() was ending with a \n. */ + if(state.lineGlyphBegin != state.renderingGlyphCount) + alignAndFinishLine(); + + /* Align the block. Now it's respecting the alignment relative to the + origin, move everything relative to the actual desired cursor. Could + probably do that in the alignRendered*() by passing a specially crafted + rect that's shifted by the cursor, but that'd become a testing nightmare + with vertical text or when per-line advance is implemented. So just do + that after. */ + const Containers::StridedArrayView1D blockGlyphPositions = state.glyphPositions.slice(state.blockRunBegin ? state.runEnds[state.blockRunBegin - 1] : 0, state.renderingRunCount ? state.runEnds[state.renderingRunCount - 1] : 0); + const Range2D alignedBlockRectangle = alignRenderedBlock( + state.blockRectangle, + state.layoutDirection, + *state.resolvedAlignment, + blockGlyphPositions); + for(Vector2& i: blockGlyphPositions) + i += state.cursor; + + /* Reset all block-related state, marking the renderer as not in progress + anymore. Line-related state should be reset after the alignLine() above + already. */ + const UnsignedInt blockRunBegin = state.blockRunBegin; + state.rendering = false; + state.resolvedAlignment = {}; + state.renderingLineStart = {}; + state.renderingLineCursor = {}; + state.renderingLineAdvance = {}; + CORRADE_INTERNAL_DEBUG_ASSERT(state.lineGlyphBegin == state.renderingGlyphCount && + state.lineRectangle == Range2D{}); + state.glyphCount = state.renderingGlyphCount; + state.runCount = state.renderingRunCount; + state.blockRunBegin = state.runCount; + state.blockRectangle = {}; + + return {alignedBlockRectangle.translated(state.cursor), {blockRunBegin, state.runCount}}; +} + +Containers::Pair RendererCore::render(AbstractShaper& shaper, const Float size, const Containers::StringView text, const Containers::ArrayView features) { + add(shaper, size, text, features); + return render(); +} + +Containers::Pair RendererCore::render(AbstractShaper& shaper, const Float size, const Containers::StringView text) { + return render(shaper, size, text, {}); +} + +Containers::Pair RendererCore::render(AbstractShaper& shaper, const Float size, const Containers::StringView text, const std::initializer_list features) { + return render(shaper, size, text, Containers::arrayView(features)); +} + Range2D renderLineGlyphPositionsInto(const AbstractFont& font, const Float size, const LayoutDirection direction, const Containers::StridedArrayView1D& glyphOffsets, const Containers::StridedArrayView1D& glyphAdvances, Vector2& cursor, const Containers::StridedArrayView1D& glyphPositions) { CORRADE_ASSERT(glyphAdvances.size() == glyphOffsets.size() && glyphPositions.size() == glyphOffsets.size(), diff --git a/src/Magnum/Text/Renderer.h b/src/Magnum/Text/Renderer.h index 1e95c3e5f..e10e62e84 100644 --- a/src/Magnum/Text/Renderer.h +++ b/src/Magnum/Text/Renderer.h @@ -27,9 +27,12 @@ */ /** @file - * @brief Class @ref Magnum::Text::AbstractRenderer, typedef @ref Magnum::Text::Renderer2D, @ref Magnum::Text::Renderer3D, function @ref Magnum::Text::renderLineGlyphPositionsInto(), @ref Magnum::Text::renderGlyphQuadsInto(), @ref Magnum::Text::glyphQuadBounds(), @ref Magnum::Text::alignRenderedLine(), @ref Magnum::Text::alignRenderedBlock(), @ref Magnum::Text::renderGlyphQuadIndicesInto(), @ref Magnum::Text::glyphRangeForBytes() + * @brief Class @ref Magnum::Text::RendererCore, @ref Magnum::Text::AbstractRenderer, typedef @ref Magnum::Text::Renderer2D, @ref Magnum::Text::Renderer3D, function @ref Magnum::Text::renderLineGlyphPositionsInto(), @ref Magnum::Text::renderGlyphQuadsInto(), @ref Magnum::Text::glyphQuadBounds(), @ref Magnum::Text::alignRenderedLine(), @ref Magnum::Text::alignRenderedBlock(), @ref Magnum::Text::renderGlyphQuadIndicesInto(), @ref Magnum::Text::glyphRangeForBytes() */ +#include +#include + #include "Magnum/Magnum.h" #include "Magnum/Text/Text.h" #include "Magnum/Text/visibility.h" @@ -52,6 +55,570 @@ namespace Magnum { namespace Text { +/** +@brief Text renderer core flag +@m_since_latest + +@see @ref RendererFlags, @ref Renderer +*/ +enum class RendererCoreFlag: UnsignedByte { + /** + * Populate glyph cluster info in @ref RendererCore::glyphClusters() for + * text selection and editing purposes. + */ + GlyphClusters = 1 << 0, +}; + +/** + * @debugoperatorenum{RendererCoreFlag} + * @m_since_latest + */ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, RendererCoreFlag value); + +/** +@brief Text renderer core flags +@m_since_latest + +@see @ref Renderer +*/ +typedef Containers::EnumSet RendererCoreFlags; + +CORRADE_ENUMSET_OPERATORS(RendererCoreFlags) + +/** + * @debugoperatorenum{RendererCoreFlags} + * @m_since_latest + */ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, RendererCoreFlags value); + +/** +@brief Text renderer core +@m_since_latest +*/ +class MAGNUM_TEXT_EXPORT RendererCore { + public: + /** + * @brief Construct + * @param glyphCache Glyph cache to use for glyph ID mapping + * @param flags Opt-in feature flags + * + * By default, the renderer allocates the memory for glyph and run data + * internally. Use the overload below to supply external allocators. + * @todoc the damn thing can't link to functions taking functions + */ + explicit RendererCore(const AbstractGlyphCache& glyphCache, RendererCoreFlags flags = {}): RendererCore{glyphCache, nullptr, nullptr, nullptr, nullptr, flags} {} + + /** + * @brief Construct with external allocators + * @param glyphCache Glyph cache to use for glyph ID mapping + * @param glyphAllocator Glyph allocator function or + * @cpp nullptr @ce + * @param glyphAllocatorState State pointer to pass to + * @p glyphAllocator + * @param runAllocator Run allocator function or @cpp nullptr @ce + * @param runAllocatorState State pointer to pass to @p runAllocator + * @param flags Opt-in feature flags + * + * The @p glyphAllocator gets called with desired @p glyphCount every + * time @ref glyphCount() reaches @ref glyphCapacity(). Size of + * passed-in @p glyphPositions, @p glyphIds and @p glyphClusters views + * matches @ref glyphCount(). The @p glyphAdvances view is a temporary + * storage with contents that don't need to be preserved on + * reallocation and is thus passed in empty. If the renderer wasn't + * constructed with @ref RendererCoreFlag::GlyphClusters, the + * @p glyphClusters is @cpp nullptr @ce to indicate it's not meant to + * be allocated. The allocator is expected to replace all passed views + * with new views that are larger by *at least* @p glyphCount, pointing + * to a reallocated memory with contents from the original view + * preserved. Initially @ref glyphCount() is @cpp 0 @ce and the views + * are all passed in empty, every subsequent time the views match a + * prefix of views previously returned by the allocator. To save + * memory, the renderer guarantees that @p glyphIds and + * @p glyphClusters are only filled once @p glyphAdvances were merged + * into @p glyphPositions. In other words, the @p glyphAdvances can + * alias a suffix of @p glyphIds and @p glyphClusters. + * + * The @p runAllocator gets called with desired @p runCount every time + * @ref runCount() reaches @ref runCapacity(). Size of passed-in + * @p runScales and @p runEnds views matches @ref runCount(). The + * allocator is expected to replace the views with new views that are + * larger by *at least* @p runCount, pointing to a reallocated memory + * with contents from the original views preserved. Initially + * @ref runCount() is @cpp 0 @ce and the views are passed in empty, + * every subsequent time the views match a prefix of views previously + * returned by the allocator. + * + * The renderer always requests only exactly the desired size and the + * growth strategy is up to the allocators themselves --- the returned + * views can be larger than requested and aren't all required to all + * have the same size. The minimum of size increases across all views + * is then treated as the new @ref glyphCapacity() / @ref runCapacity(). + * + * As a special case, when @ref clear() or @ref reset() is called, the + * allocators are called with empty views and @p glyphCount / + * @p runCount being @cpp 0 @ce. This is to allow the allocators to + * perform any needed reset as well. + * + * If @p glyphAllocator or @p runAllocator is @cpp nullptr @ce, + * @p glyphAllocatorState or @p runAllocatorState is ignored and + * default builtin allocator get used for either. Passing + * @cpp nullptr @ce for both is equivalent to calling the + * @ref RendererCore(const AbstractGlyphCache&, RendererCoreFlags) + * constructor. + */ + explicit RendererCore(const AbstractGlyphCache& glyphCache, void(*glyphAllocator)(void* state, UnsignedInt glyphCount, Containers::StridedArrayView1D& glyphPositions, Containers::StridedArrayView1D& glyphIds, Containers::StridedArrayView1D* glyphClusters, Containers::StridedArrayView1D& glyphAdvances), void* glyphAllocatorState, void(*runAllocator)(void* state, UnsignedInt runCount, Containers::StridedArrayView1D& runScales, Containers::StridedArrayView1D& runEnds), void* runAllocatorState, RendererCoreFlags flags = {}); + + /** + * @brief Construct without creating the internal state + * @m_since_latest + * + * 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. + * + * Note that this is a low-level and a potentially dangerous API, see + * the documentation of @ref NoCreate for alternatives. + */ + explicit RendererCore(NoCreateT) noexcept; + + /** @brief Copying is not allowed */ + RendererCore(RendererCore&) = delete; + + /** + * @brief Move constructor + * + * Performs a destructive move, i.e. the original object isn't usable + * afterwards anymore. + */ + RendererCore(RendererCore&&) noexcept; + + ~RendererCore(); + + /** @brief Copying is not allowed */ + RendererCore& operator=(RendererCore&) = delete; + + /** @brief Move assignment */ + RendererCore& operator=(RendererCore&&) noexcept; + + /** @brief Glyph cache associated with the renderer */ + const AbstractGlyphCache& glyphCache() const; + + /** @brief Flags */ + RendererCoreFlags flags() const; + + /** + * @brief Total count of rendered glyphs + * + * Does *not* include glyphs from the current in-progress rendering, if + * any, as their contents are not finalized yet. Use + * @ref renderingGlyphCount() to query the count including the + * in-progress glyphs. + * @see @ref isRendering(), @ref runCount() + */ + UnsignedInt glyphCount() const; + + /** + * @brief Glyph capacity + * + * @see @ref glyphCount(), @ref runCapacity(), @ref reserve() + */ + UnsignedInt glyphCapacity() const; + + /** + * @brief Total count of rendered runs + * + * Does *not* include runs from the current in-progress rendering, if + * any, as their contents are not finalized yet. Use + * @ref renderingRunCount() to query the count including the + * in-progress runs. + * @see @ref isRendering(), @ref glyphCount() + */ + UnsignedInt runCount() const; + + /** + * @brief Run capacity + * + * @see @ref runCount(), @ref glyphCapacity() @ref reserve() + */ + UnsignedInt runCapacity() const; + + /** + * @brief Whether text rendering is currently in progress + * + * Returns @cpp true @ce if there are any @ref add() calls not yet + * followed by a @ref render(), @cpp false @ce otherwise. If rendering + * is in progress, @ref setCursor(), @ref setAlignment() and + * @ref setLayoutDirection() cannot be called. The @ref glyphCount(), + * @ref runCount() and all data accessors don't include the + * yet-to-be-finalized contents. + */ + bool isRendering() const; + + /** + * @brief Total count of glyphs including current in-progress rendering + * + * Can be used for example to query which glyphs correspond to the last + * @ref add() call. If @ref isRendering() is @cpp false @ce, the + * returned value is the same as @ref glyphCount(). + */ + UnsignedInt renderingGlyphCount() const; + + /** + * @brief Total count of runs including current in-progress rendering + * + * Can be used for example to query which runs correspond to the last + * @ref add() call. If @ref isRendering() is @cpp false @ce, the + * returned value is the same as @ref runCount(). + */ + UnsignedInt renderingRunCount() const; + + /** + * @brief Cursor position + * + * Note that this does *not* return the current position at which an + * in-progress rendering is happening --- the way the glyphs get placed + * before they're aligned to their final position is internal to the + * implementation and querying such in-progress state would be of + * little use. + */ + Vector2 cursor() const; + + /** + * @brief Set cursor position for the next rendered text + * @return Reference to self (for method chaining) + * + * The next rendered text is placed according to specified @p cursor, + * @ref alignment() and @ref lineAdvance(). Expects that rendering is + * currently not in progress, meaning that the cursor can be only + * specified before rendering a particular piece of text. Initial value + * is @cpp {0.0f, 0.0f} @ce. + * @see @ref isRendering() + */ + RendererCore& setCursor(const Vector2& cursor); + + /** @brief Text alignment */ + Alignment alignment() const; + + /** + * @brief Set alignment for the next rendered text + * @return Reference to self (for method chaining) + * + * The next rendered text is placed according to specified + * @ref cursor(), @p alignment and @ref lineAdvance(). Expects that + * rendering is currently not in progress, meaning that the alignment + * can be only specified before rendering a particular piece of text. + * Initial value is @ref Alignment::MiddleCenter. + * @see @ref isRendering() + */ + RendererCore& setAlignment(Alignment alignment); + + /** @brief Line advance */ + Float lineAdvance() const; + + /** + * @brief Set line advance for the next rendered text + * @return Reference to self (for method chaining) + * + * The next rendered text is placed according to specified + * @ref cursor(), @ref alignment and @p advance. The advance value is + * used according to @ref layoutDirection() and in a coordinate system + * matching @ref AbstractFont::ascent() and + * @relativeref{AbstractFont,descent()}, so for example causes the next + * line to be shifted in a negative Y direction for + * @ref LayoutDirection::HorizontalTopToBottom. Expects that rendering + * is currently not in progress, meaning that the line advance can be + * only specified before rendering a particular piece of text. If set + * to @cpp 0.0f @ce, the line advance is picked metrics of the first + * font a corresponding size passed to @ref add(). + * @see @ref isRendering(), @ref AbstractFont::lineHeight() + */ + RendererCore& setLineAdvance(Float advance); + + /** @brief Layout direction */ + LayoutDirection layoutDirection() const; + + /** + * @brief Set layout direction + * @return Reference to self (for method chaining) + * + * Expects that rendering is currently not in progress. Currently + * expected to always be @ref LayoutDirection::HorizontalTopToBottom. + * Initial value is @ref LayoutDirection::HorizontalTopToBottom. + * @see @ref isRendering() + */ + RendererCore& setLayoutDirection(LayoutDirection direction); + + /** + * @brief Glyph positions + * + * The returned view has a size of @ref glyphCount(). Note that the + * contents are not guaranteed to be meaningful if custom glyph + * allocator is used, as the user code is free to perform subsequent + * operations on those. + */ + Containers::StridedArrayView1D glyphPositions() const; + + /** + * @brief Glyph IDs + * + * The returned view has a size of @ref glyphCount(). Note that the + * contents are not guaranteed to be meaningful if custom glyph + * allocator is used, as the user code is free to perform subsequent + * operations on those. + */ + Containers::StridedArrayView1D glyphIds() const; + + /** + * @brief Glyph cluster IDs + * + * Expects that the renderer was constructed with + * @ref RendererCoreFlag::GlyphClusters. The returned view has a size + * of @ref glyphCount(). Note that the contents are not guaranteed to + * be meaningful if custom glyph allocator is used, as the user code is + * free to perform subsequent operations on those. + */ + Containers::StridedArrayView1D glyphClusters() const; + + /** + * @brief Text run scales + * + * The returned view has a size of @ref runCount(). Note that the + * contents are not guaranteed to be meaningful if custom run allocator + * is used, as the user code is free to perform subsequent operations + * on those. + */ + Containers::StridedArrayView1D runScales() const; + + /** + * @brief Text run end glyph offsets + * + * The returned view has a size of @ref runCount(), the last value is + * equal to @ref glyphCount(), and the values index the + * @ref glyphPositions(), @ref glyphIds() and @ref glyphClusters() + * views. The first text run glyphs start at offset @cpp 0 @ce and end + * at @cpp runEnds()[0] @ce, the second text run glyphs start at offset + * @cpp runEnds()[0] @ce and end at @cpp runEnds()[1] @ce, etc. See + * also the @ref glyphsForRuns() function which provides a convenient + * way to get a range of glyphs corresponding to a range of runs + * without having to deal with edge cases. + * + * Note that the contents of the returned view are not guaranteed to be + * meaningful if custom run allocator is used, as the user code is free + * to perform subsequent operations on those. + */ + Containers::StridedArrayView1D runEnds() const; + + /** + * @brief Range of glyphs corresponding to a range of runs + * + * With @p runRange being for example the second value returned by + * @ref render(), returns a begin and end glyph offset for given run + * range, which can then be used to index the @ref glyphPositions(), + * @ref glyphIds() and @ref glyphClusters() views. Expects that both + * the min and max @p runRange value are less than or equal to + * @ref renderingRunCount(). + * + * Note that the returned value is not guaranteed to be meaningful if + * custom run allocator is used, as the user code is free to perform + * subsequent operations on the run data. + * @see @ref runEnds() + */ + Range1Dui glyphsForRuns(const Range1Dui& runRange) const; + + /** + * @brief Reserve capacity for given glyph and run count + * @return Reference to self (for method chaining) + * + * @see @ref glyphCapacity(), @ref glyphCount(), + * @ref runCapacity(), @ref runCount() + */ + RendererCore& reserve(UnsignedInt glyphCapacity, UnsignedInt runCapacity); + + /** + * @brief Clear rendered glyphs and runs + * @return Reference to self (for method chaining) + * + * The @ref glyphCount() and @ref runCount() becomes @cpp 0 @ce after + * this call and any in-progress rendering is discarded, making + * @ref isRendering() return @cpp false @ce. If custom glyph or run + * allocators are used, they get called with empty views and zero + * sizes. + * + * Depending on allocator used, @ref glyphCapacity() and + * @ref runCapacity() may stay non-zero. The @ref cursor(), + * @ref alignment(), @ref lineAdvance() and @ref layoutDirection() are + * left untouched, use @ref reset() to reset those to their default + * values as well. + */ + RendererCore& clear(); + + /** + * @brief Reset internal renderer state + * @return Reference to self (for method chaining) + * + * Calls @ref clear(), and additionally @ref cursor(), + * @ref alignment(), @ref lineAdvance() and @ref layoutDirection() are + * reset to their default values. Apart from @ref glyphCapacity() and + * @ref runCapacity(), which may stay non-zero depending on allocator + * used, the instance is equivalent to a default-constructed state. + */ + RendererCore& reset(); + + /** + * @brief Add to the currently rendered text + * @param shaper Shaper instance to render with + * @param size Font size + * @param text Text in UTF-8 + * @param begin Beginning byte in the input text + * @param end (One byte after) the end byte in the input text + * @param features Typographic features to apply for the whole text or + * its subranges + * @return Reference to self (for method chaining) + * + * Splits @p text into individual lines and shapes each with given + * @p shaper. Places and aligns the text according to @ref cursor(), + * @ref alignment() and @ref layoutDirection(), continuing from the + * state left after the previous @ref add(), if any. + * + * After calling this function, @ref isRendering() returns + * @cpp true @ce, @ref renderingGlyphCount() and @ref renderingRunCount() + * are updated with the count of newly added glyphs and runs, and + * @ref setCursor(), @ref setAlignment() or @ref setLayoutDirection() + * cannot be called anymore. Call @ref add() more times and wrap up + * with @ref render() to perform the final alignment and other steps + * necessary to finalize the rendering. If you only need to render the + * whole text at once, you can use + * @ref render(AbstractShaper&, Float, Containers::StringView, Containers::ArrayView) + * instead. + * + * The function assumes that the @p shaper has either appropriate + * script, language and shape direction set, or has them left at + * defaults in order let them be autodetected. In order to allow the + * implementation to perform shaping aware of surrounding context, such + * as picking correct glyphs for beginning, middle or end of a word or a + * paragraph, the individual @ref add() calls should ideally be made + * with the same @p text view and the slices defined by @p begin and + * @p end. Use the @ref add(AbstractShaper&, Float, Containers::StringView, Containers::ArrayView) + * overload to pass a string as a whole. + * + * The function uses @ref AbstractShaper::shape(), + * @ref renderLineGlyphPositionsInto(), @ref alignRenderedLine() and + * @ref glyphQuadBounds() internally, see their documentation for more + * information. + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + RendererCore& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features = {}); + #else + /* To not have to include ArrayView */ + RendererCore& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end); + RendererCore& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features); + #endif + + /** @overload */ + RendererCore& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, std::initializer_list features); + + /** + * @brief Add a whole string to the currently rendered text + * + * Equivalent to @ref add(AbstractShaper&, Float, Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) + * with @p begin set to @cpp 0 @ce and @p end to size of @p text. + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + RendererCore& add(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView features = {}); + #else + /* To not have to include ArrayView */ + RendererCore& add(AbstractShaper& shaper, Float size, Containers::StringView text); + RendererCore& add(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView features); + #endif + + /** @overload */ + RendererCore& add(AbstractShaper& shaper, Float size, Containers::StringView text, std::initializer_list features); + + /** + * @brief Wrap up rendering of all text added so far + * + * Performs a final alignment of the text block added by preceding + * @ref add() calls and wraps up the rendering. After calling this + * function, @ref isRendering() returns @cpp false @ce, + * @ref glyphCount() and @ref runCount() are updated with the count of + * all rendered glyphs and runs, and @ref setCursor(), + * @ref setAlignment() or @ref setLayoutDirection() can be called again + * for the next text to be rendered. + * + * The function returns a bounding box and a range of runs of the + * currently rendered text, the run range can then be used to index + * the @ref runScales() and @ref runEnds() views. Note that it's + * possible for the render to produce an empty range, such as when an + * empty text was passed or when it was just newlines. You can use + * @ref glyphsForRuns() to convert the returned run range to a begin + * and end glyph offset, which can be then used to index the + * @ref glyphPositions(), @ref glyphIds() and @ref glyphClusters() + * views. + * + * The rendered glyph range is not touched or used by the renderer in + * any way afterwards. If the renderer was created with custom + * allocators, the caller can thus perform further operations on the + * allocated data. + * + * Use @ref clear() or @ref reset() to discard all text rendered so + * far. The function uses @ref alignRenderedBlock() internally, see its + * documentation for more information. + */ + Containers::Pair render(); + + /** + * @brief Render a whole text at once + * + * A convenience shortcut for rendering a single piece of text that's + * equivalent to calling @ref add(AbstractShaper&, Float, Containers::StringView, Containers::ArrayView) + * followed by @ref render(). See their documentation for more + * information. + * + * After calling this function, @ref isRendering() returns + * @cpp false @ce. If this function is called while rendering is in + * progress, the glyphs rendered so far are included in the result as + * well. + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView features = {}); + #else + /* To not have to include ArrayView */ + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text); + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView features); + #endif + + /** @overload */ + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text, std::initializer_list features); + + #ifdef DOXYGEN_GENERATING_OUTPUT + private: + #else + protected: + #endif + struct State; + struct AllocatorState; + Containers::Pointer _state; + + /* Called by reset() */ + MAGNUM_TEXT_LOCAL void resetInternal(); + + private: + /* While the allocators get just size to grow by, these functions get + the total count */ + MAGNUM_TEXT_LOCAL void allocateGlyphs( + #ifndef CORRADE_NO_ASSERT + const char* messagePrefix, + #endif + UnsignedInt totalGlyphCount); + MAGNUM_TEXT_LOCAL void allocateRuns( + #ifndef CORRADE_NO_ASSERT + const char* messagePrefix, + #endif + UnsignedInt totalRunCount); + MAGNUM_TEXT_LOCAL void alignAndFinishLine(); +}; + /** @brief Render glyph positions for a (part of a) single line @param[in] font Font to query metrics from diff --git a/src/Magnum/Text/Test/RendererTest.cpp b/src/Magnum/Text/Test/RendererTest.cpp index be9b97ec5..210a8ffea 100644 --- a/src/Magnum/Text/Test/RendererTest.cpp +++ b/src/Magnum/Text/Test/RendererTest.cpp @@ -28,18 +28,24 @@ #include #include #include +#include +#include #include #include +#include #include #include #include +#include #include +#include #include "Magnum/PixelFormat.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" #include "Magnum/Text/AbstractShaper.h" #include "Magnum/Text/Direction.h" +#include "Magnum/Text/Feature.h" #include "Magnum/Text/Renderer.h" namespace Magnum { namespace Text { namespace Test { namespace { @@ -47,6 +53,9 @@ namespace Magnum { namespace Text { namespace Test { namespace { struct RendererTest: TestSuite::Tester { explicit RendererTest(); + /* Unlike the order in Renderer.h, the low-level utils are tested first + since the higher-level APIs rely on them */ + void lineGlyphPositions(); void lineGlyphPositionsAliasedViews(); void lineGlyphPositionsInvalidViewSizes(); @@ -76,6 +85,40 @@ struct RendererTest: TestSuite::Tester { void glyphRangeForBytes(); + void debugFlagCore(); + void debugFlagsCore(); + + void constructCore(); + void constructCoreAllocator(); + void constructCoreNoCreate(); + + void constructCopyCore(); + void constructMoveCore(); + + void propertiesCore(); + void propertiesCoreInvalid(); + void propertiesCoreRenderingInProgress(); + + void glyphsForRuns(); + void glyphsForRunsInvalid(); + + void allocateCore(); + void allocateCoreGlyphAllocator(); + void allocateCoreGlyphAllocatorInvalid(); + void allocateCoreRunAllocator(); + void allocateCoreRunAllocatorInvalid(); + + void addSingleLine(); + void addSingleLineAlign(); + void addMultipleLines(); + void addMultipleLinesAlign(); + void addFontNotFoundInCache(); + + void multipleBlocks(); + + void clearResetCore(); + void clearResetCoreAllocators(); + #ifdef MAGNUM_TARGET_GL void renderData(); @@ -149,6 +192,724 @@ const struct { }}, }; +const struct { + const char* name; + RendererCoreFlags flags; +} ConstructCoreData[]{ + {"", {}}, + {"with glyph clusters", RendererCoreFlag::GlyphClusters} +}; + +const struct { + const char* name; + void(*glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&); + void(*runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&); + RendererCoreFlags flags; +} ConstructCoreAllocatorData[]{ + {"no allocators", nullptr, nullptr, {}}, + {"no allocators, with glyph clusters", nullptr, nullptr, RendererCoreFlag::GlyphClusters}, + {"glyph allocator", [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, nullptr, {}}, + {"glyph allocator, with glyph clusters", [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, nullptr, RendererCoreFlag::GlyphClusters}, + {"run allocator", nullptr, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, {}}, + {"both allocators", [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, {}}, + {"both allocators, with glyph clusters", [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, RendererCoreFlag::GlyphClusters}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + RendererCoreFlags flagsCore; + UnsignedInt reserveGlyphs, reserveRuns, + secondReserveGlyphs, secondReserveRuns; + bool render, renderAddOnly, expectNoGlyphReallocation, expectNoRunReallocation; + UnsignedInt expectedGlyphCapacity, expectedRunCapacity; +} AllocateData[]{ + {"second reserve() same as first", + {}, 26, 3, 26, 3, false, false, true, true, 26, 3}, + {"second reserve() less glyphs than first", + {}, 26, 3, 23, 3, false, false, true, true, 26, 3}, + {"second reserve() less runs than first", + {}, 26, 3, 26, 1, false, false, true, true, 26, 3}, + {"second reserve() reallocates glyphs", + {}, 3, 3, 26, 3, false, false, false, true, 26, 3}, + {"second reserve() reallocates runs", + {}, 26, 1, 26, 3, false, false, true, false, 26, 3}, + {"render", + {}, 26, 3, 0, 0, true, false, true, true, 26, 3}, + {"render, second reserve() reallocates glyphs", + {}, 3, 3, 26, 3, true, false, false, true, 26, 3}, + {"render, second reserve() reallocates runs", + {}, 26, 1, 26, 3, true, false, true, false, 26, 3}, + {"render, second render() reallocates glyphs", + {}, 3, 3, 0, 0, true, false, false, true, 26, 3}, + {"render, second render() reallocates runs", + {}, 26, 1, 0, 0, true, false, true, false, 26, 3}, + {"render, second reserve() reallocates both, second render() also", + {}, 3, 1, 13, 2, true, false, false, false, 13, 2}, + {"render, second reserve() while in progress reallocates glyphs", + {}, 3, 3, 26, 3, true, true, false, true, 26, 3}, + {"render, second reserve() while in progress reallocates runs", + {}, 26, 1, 26, 3, true, true, true, false, 26, 3}, + {"render, second render() while in progress reallocates glyphs", + {}, 3, 3, 0, 0, true, true, false, true, 26, 3}, + {"render, second render() while in progress reallocates runs", + {}, 26, 1, 0, 0, true, true, true, false, 26, 3}, + {"render, second reserve() while in progress reallocates both, second render() also", + {}, 3, 1, 13, 2, true, true, false, false, 13, 2}, + /* The flag affects only glyph allocation, not runs, so their variants are + not tested below */ + {"with glyph (positions and) clusters, second reserve() same as first", + RendererCoreFlag::GlyphClusters, + 26, 3, 26, 3, false, false, true, true, 26, 3}, + {"with glyph (positions and) clusters, second reserve() less glyphs than first", + RendererCoreFlag::GlyphClusters, + 26, 3, 23, 3, false, false, true, true, 26, 3}, + {"with glyph (positions and) clusters, second reserve() reallocates glyphs", + RendererCoreFlag::GlyphClusters, + 3, 3, 26, 3, false, false, false, true, 26, 3}, + {"with glyph (positions and) clusters, render", + RendererCoreFlag::GlyphClusters, + 26, 3, 0, 0, true, false, true, true, 26, 3}, + {"with glyph (positions and) clusters, render, second render() reallocates glyphs", + RendererCoreFlag::GlyphClusters, + 3, 3, 0, 0, true, false, false, true, 26, 3}, + {"with glyph (positions and) clusters, render, second render() while in progress reallocates glyphs", + RendererCoreFlag::GlyphClusters, + 3, 3, 0, 0, true, true, false, true, 26, 3} +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + RendererCoreFlags flags; + UnsignedInt reserve, secondReserve; + bool render, renderAddOnly, expectNoReallocation; + UnsignedInt + positionSize, + idSize, + clusterSize, + advanceSize, + expectedCapacity; +} AllocateCoreGlyphAllocatorData[]{ + {"second reserve() same as first", + {}, 26, 26, false, false, true, + 0, 0, 0, 0, 26}, + {"second reserve() smaller than first", + {}, 26, 23, false, false, true, + 0, 0, 0, 0, 26}, + {"second reserve() reallocates, positions smallest", + {}, 3, 26, false, false, false, + 27, 30, 0, 28, 27}, + {"second reserve() reallocates, IDs smallest", + {}, 3, 26, false, false, false, + 29, 28, 0, 30, 28}, + {"second reserve() reallocates, advances smallest", + {}, 3, 26, false, false, false, + 31, 30, 0, 29, 29}, + {"render", + {}, 26, 26, true, false, true, + 0, 0, 0, 0, 26}, + {"render, second render() reallocates, positions smallest", + {}, 3, 26, true, false, false, + /* Size of advances excludes the already-rendered glyphs, same below */ + 27, 30, 0, 28 - 3, 27}, + {"render, second render() reallocates, IDs smallest", + {}, 3, 26, true, false, false, + 28, 27, 0, 30 - 3, 27}, + {"render, second render() reallocates, advances smallest", + {}, 3, 26, true, false, false, + 31, 32, 0, 30 - 3, 30}, + {"render, second render() while in progress reallocates", + {}, 3, 26, true, true, false, + 26, 26, 0, 26 - 3, 26}, + {"with glyph clusters, second reserve() same as first", + RendererCoreFlag::GlyphClusters, 26, 26, false, false, true, + 0, 0, 0, 0, 26}, + {"with glyph clusters, second reserve() reallocates, IDs smallest", + RendererCoreFlag::GlyphClusters, 3, 26, false, false, false, + 28, 27, 32, 30, 27}, + {"with glyph clusters, second reserve() reallocates, clusters smallest", + RendererCoreFlag::GlyphClusters, 3, 26, false, false, false, + 30, 28, 27, 32, 27}, + {"with glyph clusters, render", + RendererCoreFlag::GlyphClusters, 26, 26, true, false, true, + 0, 0, 0, 0, 26}, + {"with glyph clusters, second render() reallocates, IDs smallest", + RendererCoreFlag::GlyphClusters, 3, 26, true, false, false, + /* Size of advances excludes the already-rendered glyphs, same below */ + 28, 27, 32, 30 - 3, 27}, + {"with glyph clusters, second render() reallocates, clusters smallest", + RendererCoreFlag::GlyphClusters, 3, 26, true, false, false, + 30, 28, 27, 32 - 3, 27}, + {"with glyph clusters, second render() while in progress reallocates", + RendererCoreFlag::GlyphClusters, 3, 26, true, true, false, + 26, 26, 26, 26 - 3, 26} +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + RendererCoreFlags flags; + bool render; + std::size_t positionSize, idSize, clusterSize, advanceSize; + const char* expected; +} AllocateCoreGlyphAllocatorInvalidData[]{ + {"reserve, positions too small", + {}, false, 16, 17, 0, 8, + "Text::RendererCore::reserve(): expected allocated glyph positions and IDs to have at least 17 elements and advances 7 but got 16, 17 and 8\n"}, + {"render, positions too small", + {}, true, 16, 17, 0, 8, + "Text::RendererCore::add(): expected allocated glyph positions and IDs to have at least 17 elements and advances 7 but got 16, 17 and 8\n"}, + {"reserve, IDs too small", + {}, false, 20, 16, 0, 7, + "Text::RendererCore::reserve(): expected allocated glyph positions and IDs to have at least 17 elements and advances 7 but got 20, 16 and 7\n"}, + {"render, IDs too small", + {}, true, 20, 16, 0, 7, + "Text::RendererCore::add(): expected allocated glyph positions and IDs to have at least 17 elements and advances 7 but got 20, 16 and 7\n"}, + {"reserve, advances too small", + {}, false, 17, 20, 0, 6, + "Text::RendererCore::reserve(): expected allocated glyph positions and IDs to have at least 17 elements and advances 7 but got 17, 20 and 6\n"}, + {"reserve, advances too small", + {}, true, 17, 20, 0, 6, + "Text::RendererCore::add(): expected allocated glyph positions and IDs to have at least 17 elements and advances 7 but got 17, 20 and 6\n"}, + {"with glyph clusters, reserve, IDs too small", + RendererCoreFlag::GlyphClusters, false, 20, 16, 18, 7, + "Text::RendererCore::reserve(): expected allocated glyph positions, IDs and clusters to have at least 17 elements and advances 7 but got 20, 16, 18 and 7\n"}, + {"with glyph clusters, render, IDs too small", + RendererCoreFlag::GlyphClusters, true, 20, 16, 18, 7, + "Text::RendererCore::add(): expected allocated glyph positions, IDs and clusters to have at least 17 elements and advances 7 but got 20, 16, 18 and 7\n"}, + {"with glyph clusters, reserve, clusters too small", + RendererCoreFlag::GlyphClusters, false, 17, 20, 16, 9, + "Text::RendererCore::reserve(): expected allocated glyph positions, IDs and clusters to have at least 17 elements and advances 7 but got 17, 20, 16 and 9\n"}, + {"with glyph clusters, render, clusters too small", + RendererCoreFlag::GlyphClusters, true, 17, 20, 16, 9, + "Text::RendererCore::add(): expected allocated glyph positions, IDs and clusters to have at least 17 elements and advances 7 but got 17, 20, 16 and 9\n"}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + UnsignedInt reserve, secondReserve; + bool render, renderAddOnly, expectNoReallocation; + UnsignedInt + scaleSize, + endSize, + expectedCapacity; +} AllocateCoreRunAllocatorData[]{ + {"second reserve() same as first", + 5, 5, false, false, true, + 0, 0, 5}, + {"second reserve() smaller than first", + 5, 3, false, false, true, + 0, 0, 5}, + {"second reserve() reallocates, scales smallest", + 3, 5, false, false, false, + 7, 8, 7}, + {"second reserve() reallocates, ends smallest", + 3, 5, false, false, false, + 7, 6, 6}, + {"render", + 5, 5, true, false, true, + 0, 0, 5}, + {"render, second render() reallocates, scales smallest", + 3, 5, true, false, false, + 7, 8, 7}, + {"render, second render() reallocates, ends smallest", + 3, 5, true, false, false, + 7, 6, 6}, + {"render, second render() reallocates while in progress", + 3, 5, true, true, false, + 5, 5, 5}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + bool render; + std::size_t scaleSize, endSize; + const char* expected; +} AllocateCoreRunAllocatorInvalidData[]{ + {"reserve, scales too small", + false, 3, 5, + "Text::RendererCore::reserve(): expected allocated run scales and ends to have at least 5 elements but got 3 and 5\n"}, + {"render, scales too small", + true, 4, 5, + "Text::RendererCore::add(): expected allocated run scales and ends to have at least 5 elements but got 4 and 5\n"}, + {"reserve, ends too small", + false, 5, 3, + "Text::RendererCore::reserve(): expected allocated run scales and ends to have at least 5 elements but got 5 and 3\n"}, + {"render, ends too small", + true, 5, 4, + "Text::RendererCore::add(): expected allocated run scales and ends to have at least 5 elements but got 5 and 4\n"}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + /* Char begin, end, size multiplier */ + Containers::Array> items; + RendererCoreFlags flags; + Alignment alignment; + ShapeDirection shapeDirection; + UnsignedInt advertiseShapeDirectionAt; + bool direct; + Float expectedRectHeight; + Containers::Array> expectedRuns; + UnsignedInt expectedGlyphIds[10]; +} AddSingleLineData[]{ + {"all at once", {InPlaceInit, { + {3, 8, 1.0f}, + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + /* H h E e L l L l O o */ + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, direct render()", {InPlaceInit, { + {0, 5, 1.0f}, + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, true, 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, with glyph clusters", {InPlaceInit, { + {3, 8, 1.0f}, + }}, RendererCoreFlag::GlyphClusters, Alignment::LineRight, ShapeDirection{}, 0, false, 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, with glyph clusters, direct render()", {InPlaceInit, { + {0, 5, 1.0f}, + }}, RendererCoreFlag::GlyphClusters, Alignment::LineRight, ShapeDirection{}, 0, true, 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + /* Direction-based alignment resolve, should end up being LineRight in all + cases */ + {"all at once, top begin, RTL", {InPlaceInit, { + {3, 8, 1.0f}, + }}, {}, Alignment::LineBegin, ShapeDirection::RightToLeft, 3, false, 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, top end, LTR", {InPlaceInit, { + {3, 8, 1.0f}, + }}, {}, Alignment::LineEnd, ShapeDirection::LeftToRight, 3, false, 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, top end, unspecified", {InPlaceInit, { + {3, 8, 1.0f}, + }}, {}, Alignment::LineEnd, ShapeDirection::Unspecified, 0, false, 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + /* The direction should only affect Start / End alignment */ + {"all at once, top right, RTL", {InPlaceInit, { + {3, 8, 1.0f}, + }}, {}, Alignment::LineRight, ShapeDirection::RightToLeft, 3, false, 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + /* These verify that submission in parts doesn't cause problems */ + {"three parts", {InPlaceInit, { + {3, 5, 1.0f}, + {5, 7, 2.0f}, + {7, 8, 1.0f}, + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 24.0f, {InPlaceInit, { + {1.0f, 4}, + {1.0f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + {"three parts, with glyph clusters", {InPlaceInit, { + {3, 5, 1.0f}, + {5, 7, 2.0f}, + {7, 8, 1.0f}, + }}, RendererCoreFlag::GlyphClusters, Alignment::LineRight, ShapeDirection{}, 0, false, 24.0f, {InPlaceInit, { + {1.0f, 4}, + {1.0f, 8}, + {1.0f, 10} + }}, { + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + /* These verify that direction-based alignment resolve works no matter when + it happens on given line */ + {"three parts, top begin, RTL, detected at the begining", {InPlaceInit, { + {3, 5, 1.0f}, + {5, 7, 2.0f}, + {7, 8, 1.0f}, + }}, {}, Alignment::LineBegin, ShapeDirection::RightToLeft, 3, false, 24.0f, {InPlaceInit, { + {1.0f, 4}, + {1.0f, 8}, + {1.0f, 10}, + }}, { + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + {"three parts, top begin, RTL, detected at the end", {InPlaceInit, { + {3, 5, 1.0f}, + {5, 7, 2.0f}, + {7, 8, 1.0f}, + }}, {}, Alignment::LineBegin, ShapeDirection::RightToLeft, 7, false, 24.0f, {InPlaceInit, { + {1.0f, 4}, + {1.0f, 8}, + {1.0f, 10}, + }}, { + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + /* Empty parts shouldn't affect anything */ + {"empty parts", {InPlaceInit, { + {3, 3, 1.0f}, + {3, 6, 2.0f}, + {6, 6, 1.0f}, + {6, 8, 2.0f}, + {8, 8, 1.0f} + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 24.0f, {InPlaceInit, { + {1.0f, 6}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + second ------------------------------ */ + 12, 14, 10, 9, 13, 11, 13, 11, 15, 16 + }}, + /* These verify that scaling is correctly accounted for */ + {"first part with taller font", {InPlaceInit, { + {3, 5, 5.0f}, + {5, 7, 2.0f}, + {7, 8, 1.0f}, + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 120.0f, {InPlaceInit, { + {5.0f, 4}, + {1.0f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + {"all but last part with shorter font", {InPlaceInit, { + {3, 5, 0.5f}, + {5, 7, 1.0f}, + {7, 8, 0.75f}, + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 18.0f, {InPlaceInit, { + {0.5f, 4}, + {0.5f, 8}, + {0.75f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + /* Empty parts have their font metrics ignored */ + {"an empty part with taller font", {InPlaceInit, { + {3, 5, 1.0f}, + {5, 5, 10.0f}, + {5, 8, 1.0f}, + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 24.0f, {InPlaceInit, { + {1.0f, 4}, + {1.0f, 10}, + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, direct render(), with taller font", {InPlaceInit, { + {0, 5, 5.0f}, + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, true, 120.0f, {InPlaceInit, { + {5.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + Alignment alignment; + ShapeDirection shapeDirection; + Vector2 offset; +} AddSingleLineAlignData[]{ + /* The individual alignment values are tested in alignLine() and + alignBlock() already, here just making sure that the output makes sense + when everything is combined together, including shape direction */ + {"line left", + Alignment::LineLeft, ShapeDirection::Unspecified, + /* This is the default (0) value, thus should result in no shift */ + {}}, + {"top right", + Alignment::TopRight, ShapeDirection::Unspecified, + /* Advances were 1, 2, 3, so 6 in total, ascent is 4.5; scaled by + 0.5 */ + {-3.0f, -2.25f}}, + {"middle left, glyph bounds, integral", + Alignment::MiddleLeftGlyphBoundsIntegral, ShapeDirection::Unspecified, + /* The X shift isn't whole units but only Y is rounded here */ + {-2.5f, -7.0f}}, + {"bottom center, integral", + Alignment::BottomCenterIntegral, ShapeDirection::Unspecified, + /* The Y shift isn't whole units but only X is rounded here */ + {-2.0f, 1.25f}}, + {"line right", + Alignment::LineRight, ShapeDirection::Unspecified, + {-3.0f, 0.0f}}, + {"line begin, RTL", + Alignment::LineBegin, ShapeDirection::RightToLeft, + {-3.0f, 0.0f}}, /* Same as line right */ +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + /* Char begin, end, actual begin/end passed to the shaper. Cannot use a + nested Array because construction from a r-value array is broken on MSVC + 2017, so it's Array3 instead with a default-constructed suffix if only + 0, 1 or 2 calls are done. Ugh. */ + Containers::Array>>> items; + RendererCoreFlags flags; + Alignment alignment; + ShapeDirection shapeDirection; + UnsignedInt advertiseShapeDirectionAt; + bool direct; + Float lineAdvance; + Float expectedLineAdvance, expectedRectHeight; + Containers::Array> expectedRuns; + UnsignedInt expectedGlyphIds[10]; +} AddMultipleLinesData[]{ + /* These verify only what's not already sufficiently tested in + AddSingleLineData */ + {"all at once", {InPlaceInit, { + {3, 11, {{{3, 5}, {6, 8}, {10, 11}}}} + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + /* H h E e L l L l O o */ + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, direct render()", {InPlaceInit, { + {0, 8, {{{0, 2}, {3, 5}, {7, 8}}}} + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, true, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, with glyph clusters", {InPlaceInit, { + {3, 11, {{{3, 5}, {6, 8}, {10, 11}}}} + }}, RendererCoreFlag::GlyphClusters, Alignment::LineRight, ShapeDirection{}, 0, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"all at once, with glyph clusters, direct render()", {InPlaceInit, { + {0, 8, {{{0, 2}, {3, 5}, {7, 8}}}} + }}, RendererCoreFlag::GlyphClusters, Alignment::LineRight, ShapeDirection{}, 0, true, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"each line separately with \\n at the end", {InPlaceInit, { + { 3, 6, {{{3, 5}, {}, {}}}}, /* he\n */ + { 6, 10, {{{6, 8}, {}, {}}}}, /* ll\n\n */ + {10, 11, {{{10, 11}, {}, {}}}}, /* o */ + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 4}, + {0.5f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + {"each successive line separately with \\n at the beginning", {InPlaceInit, { + { 3, 5, {{{3, 5}, {}, {}}}}, /* he */ + { 5, 8, {{{6, 8}, {}, {}}}}, /* \nll */ + { 8, 11, {{{10, 11}, {}, {}}}}, /* \n\no */ + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 4}, + {0.5f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + {"\\n alone", {InPlaceInit, { + { 3, 5, {{{3, 5}, {}, {}}}}, /* he */ + { 5, 6, {}}, /* \n */ + { 6, 8, {{{6, 8}, {}, {}}}}, /* ll */ + { 8, 9, {}}, /* \n */ + { 9, 10, {}}, /* \n */ + {10, 11, {{{10, 11}, {}, {}}}}, /* o */ + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 4}, + {1.0f, 8}, + {0.5f, 10}, + }}, { + /* H h E e L l L l O o + first ---------------- second */ + 4, 6, 2, 1, 5, 3, 5, 3, 15, 16 + }}, + {"\\n alone and completely empty parts mixed", {InPlaceInit, { + { 3, 6, {{{3, 5}, {}, {}}}}, /* he\n */ + { 6, 6, {}}, + { 6, 8, {{{6, 8}, {}, {}}}}, /* ll */ + { 8, 9, {}}, /* \n */ + { 9, 9, {}}, + { 9, 10, {}}, /* \n */ + {10, 11, {{{10, 11}, {}, {}}}}, /* o */ + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 4}, + {1.0f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o */ + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"continuing from the middle of a line", {InPlaceInit, { + { 3, 4, {{{3, 4}, {}, {}}}}, /* h */ + { 4, 7, {{{4, 5}, {6, 7}, {}}}}, /* e\nl */ + { 7, 11, {{{7, 8}, {10, 11}, {}}}}, /* l\n\no */ + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 2}, + {0.5f, 6}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first second ------ first ---- */ + 4, 6, 10, 9, 13, 11, 5, 3, 7, 8 + }}, + /* This should correctly make it LineRight */ + {"each line separately, RTL", {InPlaceInit, { + { 3, 6, {{{3, 5}, {}, {}}}}, /* he\n */ + { 6, 10, {{{6, 8}, {}, {}}}}, /* ll\n\n */ + {10, 11, {{{10, 11}, {}, {}}}}, /* o */ + }}, {}, Alignment::LineBegin, ShapeDirection::RightToLeft, 3, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 4}, + {0.5f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + /* These two should fall back to Unspecified for the first line, resulting + in LineRight even though on the second line it'd resolve to LineLeft */ + {"each line separately with \\n at the end, RTL detected only at the second line", {InPlaceInit, { + { 3, 6, {{{3, 5}, {}, {}}}}, /* he\n */ + { 6, 10, {{{6, 8}, {}, {}}}}, /* ll\n\n */ + {10, 11, {{{10, 11}, {}, {}}}}, /* o */ + }}, {}, Alignment::LineEnd, ShapeDirection::RightToLeft, 6, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 4}, + {0.5f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + {"each successive line separately with \\n at the beginning, RTL detected at the second line", {InPlaceInit, { + { 3, 5, {{{3, 5}, {}, {}}}}, /* he */ + { 5, 8, {{{6, 8}, {}, {}}}}, /* \nll */ + { 8, 11, {{{10, 11}, {}, {}}}}, /* \n\no */ + }}, {}, Alignment::LineEnd, ShapeDirection::RightToLeft, 5, false, 0.0f, 32.0f, 3*32.0f + 24.0f, {InPlaceInit, { + {1.0f, 4}, + {0.5f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, + /* Overriding line advance */ + {"all at once, custom line advance", {InPlaceInit, { + {3, 11, {{{3, 5}, {6, 8}, {10, 11}}}} + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 29.0f, 29.0f, 3*29.0f + 24.0f, {InPlaceInit, { + {1.0f, 10} + }}, { + /* H h E e L l L l O o */ + 4, 6, 2, 1, 5, 3, 5, 3, 7, 8 + }}, + {"each line separately, custom line advance", {InPlaceInit, { + { 3, 6, {{{3, 5}, {}, {}}}}, /* he\n */ + { 6, 10, {{{6, 8}, {}, {}}}}, /* ll\n\n */ + {10, 11, {{{10, 11}, {}, {}}}}, /* o */ + }}, {}, Alignment::LineRight, ShapeDirection{}, 0, false, 29.0f, 29.0f, 3*29.0f + 24.0f, {InPlaceInit, { + {1.0f, 4}, + {0.5f, 8}, + {1.0f, 10}, + }}, { + /* H h E e L l L l O o + first ---- second ------- first */ + 4, 6, 2, 1, 13, 11, 13, 11, 7, 8 + }}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + Alignment alignment; + /* The Y offset value could be calculated, but this is easier to grasp and + makes it possible to test overrideable line height later, for example */ + Vector2 offset0, offset1, offset2; +} AddMultipleLinesAlignData[]{ + /* The individual alignment values are tested in alignLine() and + alignBlock() already, here just making sure that the output makes sense + when everything is combined together */ + {"line left", Alignment::LineLeft, + {0.0f, -0.0f}, + {0.0f, -4.0f}, + {0.0f, -12.0f}}, + {"bottom right, glyph bounds", Alignment::BottomRightGlyphBounds, + {-7.0f, 12.0f}, + {-3.0f, 8.0f}, + {-5.0f, 0.0f}}, + {"middle center, glyph bounds, integral", Alignment::MiddleCenterGlyphBoundsIntegral, + {-4.0f, 6.0f}, + {-2.0f, 2.0f}, + {-3.0f, -6.0f}}, + {"top right", Alignment::TopRight, + {-8.0f, -0.5f}, + {-4.0f, -4.5f}, + {-6.0f, -12.5f}}, +}; + +const struct { + const char* name; + RendererCoreFlags flags; +} MultipleBlocksData[]{ + {"", {}}, + {"with glyph clusters", RendererCoreFlag::GlyphClusters} +}; + +const struct { + const char* name; + RendererCoreFlags flags; + bool renderAddOnly, reset; + UnsignedInt expectedBuiltinGlyphAllocatorCapacity; +} ClearResetCoreData[]{ + /* After clear() it needs more space for the advances, so the capacity will + not be 3 even though it contained 3 glyphs before */ + {"clear", {}, false, false, 2}, + /* Here the glyph advances alias other memory so 3 can fit */ + {"clear, with glyph clusters", RendererCoreFlag::GlyphClusters, false, false, 3}, + {"reset", {}, false, true, 2}, + {"clear while in progress", {}, true, false, 2}, + /* Here the glyph advances alias other memory so 3 can fit */ + {"clear while in progress, with glyph clusters", RendererCoreFlag::GlyphClusters, true, false, 3}, + {"reset while in progress", {}, true, true, 2}, +}; + +#ifdef MAGNUM_TARGET_GL const struct { TestSuite::TestCaseDescriptionSourceLocation name; Alignment alignment; @@ -328,6 +1089,7 @@ const struct { {-2.0f, -5.0f}, {-3.0f, -13.0f}}, }; +#endif RendererTest::RendererTest() { addTests({&RendererTest::lineGlyphPositions, @@ -370,6 +1132,61 @@ RendererTest::RendererTest() { addInstancedTests({&RendererTest::glyphRangeForBytes}, Containers::arraySize(GlyphRangeForBytesData)); + addTests({&RendererTest::debugFlagCore, + &RendererTest::debugFlagsCore}); + + addInstancedTests({&RendererTest::constructCore, + &RendererTest::constructCoreAllocator}, + Containers::arraySize(ConstructCoreData)); + + addTests({&RendererTest::constructCoreNoCreate}); + + addTests({&RendererTest::constructCopyCore, + &RendererTest::constructMoveCore, + + &RendererTest::propertiesCore, + &RendererTest::propertiesCoreInvalid, + &RendererTest::propertiesCoreRenderingInProgress, + + &RendererTest::glyphsForRuns, + &RendererTest::glyphsForRunsInvalid}); + + addInstancedTests({&RendererTest::allocateCore}, + Containers::arraySize(AllocateData)); + + addInstancedTests({&RendererTest::allocateCoreGlyphAllocator}, + Containers::arraySize(AllocateCoreGlyphAllocatorData)); + + addInstancedTests({&RendererTest::allocateCoreGlyphAllocatorInvalid}, + Containers::arraySize(AllocateCoreGlyphAllocatorInvalidData)); + + addInstancedTests({&RendererTest::allocateCoreRunAllocator}, + Containers::arraySize(AllocateCoreRunAllocatorData)); + + addInstancedTests({&RendererTest::allocateCoreRunAllocatorInvalid}, + Containers::arraySize(AllocateCoreRunAllocatorInvalidData)); + + addInstancedTests({&RendererTest::addSingleLine}, + Containers::arraySize(AddSingleLineData)); + + addInstancedTests({&RendererTest::addSingleLineAlign}, + Containers::arraySize(AddSingleLineAlignData)); + + addInstancedTests({&RendererTest::addMultipleLines}, + Containers::arraySize(AddMultipleLinesData)); + + addInstancedTests({&RendererTest::addMultipleLinesAlign}, + Containers::arraySize(AddMultipleLinesAlignData)); + + addTests({&RendererTest::addFontNotFoundInCache}); + + addInstancedTests({&RendererTest::multipleBlocks}, + Containers::arraySize(MultipleBlocksData)); + + addInstancedTests({&RendererTest::clearResetCore, + &RendererTest::clearResetCoreAllocators}, + Containers::arraySize(ClearResetCoreData)); + #ifdef MAGNUM_TARGET_GL addInstancedTests({&RendererTest::renderData}, Containers::arraySize(RenderDataData)); @@ -382,6 +1199,7 @@ RendererTest::RendererTest() { #endif } +/* Used by addSingleLineAlign() / addMultipleLinesAlign() */ struct TestShaper: AbstractShaper { explicit TestShaper(AbstractFont& font, ShapeDirection direction): AbstractShaper{font}, _direction{direction} {} @@ -1217,6 +2035,2508 @@ void RendererTest::glyphRangeForBytes() { } } +void RendererTest::debugFlagCore() { + Containers::String out; + Debug{&out} << RendererCoreFlag::GlyphClusters << RendererCoreFlag(0xca); + CORRADE_COMPARE(out, "Text::RendererCoreFlag::GlyphClusters Text::RendererCoreFlag(0xca)\n"); +} + +void RendererTest::debugFlagsCore() { + Containers::String out; + Debug{&out} << (RendererCoreFlag::GlyphClusters|RendererCoreFlag(0xf0)) << RendererCoreFlags{}; + CORRADE_COMPARE(out, "Text::RendererCoreFlag::GlyphClusters|Text::RendererCoreFlag(0xf0) Text::RendererCoreFlags{}\n"); +} + +void RendererTest::constructCore() { + auto&& data = ConstructCoreData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, 2}}; + + RendererCore renderer{glyphCache, data.flags}; + CORRADE_COMPARE(&renderer.glyphCache(), &glyphCache); + CORRADE_COMPARE(renderer.flags(), data.flags); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.cursor(), Vector2{}); + CORRADE_COMPARE(renderer.alignment(), Alignment::MiddleCenter); + CORRADE_COMPARE(renderer.layoutDirection(), LayoutDirection::HorizontalTopToBottom); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); +} + +void RendererTest::constructCoreAllocator() { + auto&& data = ConstructCoreAllocatorData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, 2}}; + + int called = 0; + RendererCore renderer{glyphCache, data.glyphAllocator, &called, data.runAllocator, &called, data.flags}; + + CORRADE_COMPARE(&renderer.glyphCache(), &glyphCache); + CORRADE_COMPARE(renderer.flags(), data.flags); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.cursor(), Vector2{}); + CORRADE_COMPARE(renderer.alignment(), Alignment::MiddleCenter); + CORRADE_COMPARE(renderer.lineAdvance(), 0.0f); + CORRADE_COMPARE(renderer.layoutDirection(), LayoutDirection::HorizontalTopToBottom); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + + /* The allocators should not be called by default */ + CORRADE_COMPARE(called, 0); +} + +void RendererTest::constructCoreNoCreate() { + RendererCore renderer{NoCreate}; + + /* Shouldn't crash */ + CORRADE_VERIFY(true); + + /* Implicit construction is not allowed */ + CORRADE_VERIFY(!std::is_convertible::value); +} + +void RendererTest::constructCopyCore() { + CORRADE_VERIFY(!std::is_copy_constructible{}); + CORRADE_VERIFY(!std::is_copy_assignable{}); +} + +void RendererTest::constructMoveCore() { + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, 2}}, + anotherGlyphCache{PixelFormat::RGBA8Unorm, {4, 4}}; + + RendererCore a{glyphCache, RendererCoreFlag::GlyphClusters}; + + RendererCore b = Utility::move(a); + CORRADE_COMPARE(&b.glyphCache(), &glyphCache); + CORRADE_COMPARE(b.flags(), RendererCoreFlag::GlyphClusters); + + RendererCore c{anotherGlyphCache}; + c = Utility::move(b); + CORRADE_COMPARE(&c.glyphCache(), &glyphCache); + CORRADE_COMPARE(c.flags(), RendererCoreFlag::GlyphClusters); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + +void RendererTest::propertiesCore() { + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + RendererCore renderer{glyphCache}; + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.cursor(), Vector2{}); + CORRADE_COMPARE(renderer.alignment(), Alignment::MiddleCenter); + CORRADE_COMPARE(renderer.lineAdvance(), 0.0f); + CORRADE_COMPARE(renderer.layoutDirection(), LayoutDirection::HorizontalTopToBottom); + + renderer.setCursor({15.7f, -2.3f}); + CORRADE_COMPARE(renderer.cursor(), (Vector2{15.7f, -2.3f})); + + renderer.setAlignment(Alignment::BottomLeftGlyphBounds); + CORRADE_COMPARE(renderer.alignment(), Alignment::BottomLeftGlyphBounds); + + renderer.setLineAdvance(3.0f); + CORRADE_COMPARE(renderer.lineAdvance(), 3.0f); + + /* Layout direction has just one allowed value right now */ + /** @todo update once it's not just one anymore */ + renderer.setLayoutDirection(LayoutDirection::HorizontalTopToBottom); + CORRADE_COMPARE(renderer.layoutDirection(), LayoutDirection::HorizontalTopToBottom); +} + +void RendererTest::propertiesCoreInvalid() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + RendererCore renderer{glyphCache}; + + Containers::String out; + Error redirectError{&out}; + renderer.setLayoutDirection(LayoutDirection::VerticalLeftToRight); + renderer.glyphClusters(); + CORRADE_COMPARE(out, + "Text::RendererCore::setLayoutDirection(): only Text::LayoutDirection::HorizontalTopToBottom is supported right now, got Text::LayoutDirection::VerticalLeftToRight\n" + "Text::RendererCore::glyphClusters(): glyph clusters not enabled\n"); +} + +void RendererTest::propertiesCoreRenderingInProgress() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + glyphCache.addFont(0, &font); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 0; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} + } shaper{font}; + + RendererCore renderer{glyphCache}; + + /* It should be marked as in progress even if there aren't any glyphs, to + enforce correct usage in all cases */ + renderer.add(shaper, 1.0f, "hello"); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_VERIFY(renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + + /* It should blow up even if the properties are set to exactly the same as + before */ + Containers::String out; + Error redirectError{&out}; + renderer.setCursor({}); + renderer.setAlignment(Alignment::MiddleCenter); + renderer.setLineAdvance({}); + renderer.setLayoutDirection(LayoutDirection::HorizontalTopToBottom); + CORRADE_COMPARE_AS(out, + "Text::RendererCore::setCursor(): rendering in progress\n" + "Text::RendererCore::setAlignment(): rendering in progress\n" + "Text::RendererCore::setLineAdvance(): rendering in progress\n" + "Text::RendererCore::setLayoutDirection(): rendering in progress\n", + TestSuite::Compare::String); +} + +void RendererTest::glyphsForRuns() { + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}, {}}; + + 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; + return {1.0f, 1.0f, -1.0f, 1.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); + glyphCache.addFont(1, &font); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + for(UnsignedInt i = 0; i != ids.size(); ++i) + ids[i] = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} + } shaper{font}; + + RendererCore renderer{glyphCache}; + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + + /* With no runs this is the only value it accepts */ + CORRADE_COMPARE(renderer.glyphsForRuns({}), Range1Dui{}); + + /* A single finished run */ + CORRADE_COMPARE(renderer.render(shaper, 1.0f, "abcd").second(), (Range1Dui{0, 1})); + CORRADE_COMPARE(renderer.glyphCount(), 4); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 4); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); + CORRADE_COMPARE(renderer.glyphsForRuns({0, 1}), (Range1Dui{0, 4})); + + /* Should work for unfinished runs as well, and across them */ + renderer + .add(shaper, 1.0f, "ef") + .add(shaper, 1.0f, "ghi"); + CORRADE_COMPARE(renderer.glyphCount(), 4); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 9); + CORRADE_COMPARE(renderer.renderingRunCount(), 3); + CORRADE_COMPARE(renderer.glyphsForRuns({0, 3}), (Range1Dui{0, 9})); + CORRADE_COMPARE(renderer.glyphsForRuns({1, 2}), (Range1Dui{4, 6})); + CORRADE_COMPARE(renderer.glyphsForRuns({1, 3}), (Range1Dui{4, 9})); + CORRADE_COMPARE(renderer.glyphsForRuns({2, 3}), (Range1Dui{6, 9})); + + /* Zero-size, at both begin and end, and end < begin should also work */ + CORRADE_COMPARE(renderer.glyphsForRuns({2, 2}), (Range1Dui{6, 6})); + CORRADE_COMPARE(renderer.glyphsForRuns({0, 0}), (Range1Dui{0, 0})); + CORRADE_COMPARE(renderer.glyphsForRuns({3, 3}), (Range1Dui{9, 9})); + CORRADE_COMPARE(renderer.glyphsForRuns({3, 1}), (Range1Dui{9, 4})); + CORRADE_COMPARE(renderer.glyphsForRuns({2, 0}), (Range1Dui{6, 0})); +} + +void RendererTest::glyphsForRunsInvalid() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}, {}}; + + 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; + return {1.0f, 1.0f, -1.0f, 1.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); + glyphCache.addFont(1, &font); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + for(UnsignedInt i = 0; i != ids.size(); ++i) + ids[i] = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} + } shaper{font}; + + /* Have some runs finished and some still rendering */ + RendererCore renderer{glyphCache}; + renderer.render(shaper, 1.0f, "ab"); + renderer + .add(shaper, 1.0f, "cde") + .add(shaper, 1.0f, "fg"); + CORRADE_COMPARE(renderer.glyphCount(), 2); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 7); + CORRADE_COMPARE(renderer.renderingRunCount(), 3); + + /* This is still fine */ + renderer.glyphsForRuns({3, 3}); + + Containers::String out; + Error redirectError{&out}; + renderer.glyphsForRuns({3, 4}); + renderer.glyphsForRuns({4, 3}); + CORRADE_COMPARE_AS(out, + "Text::RendererCore::glyphsForRuns(): runs {3, 4} out of range for 3 runs\n" + "Text::RendererCore::glyphsForRuns(): runs {4, 3} out of range for 3 runs\n", + TestSuite::Compare::String); +} + +void RendererTest::allocateCore() { + auto&& data = AllocateData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + /* Set padding to zero for easier dummy glyph addition below */ + } glyphCache{PixelFormat::R8Unorm, {16, 16}, {}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) 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 {size, 2.5f, -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(23*2, &font); + /* Add just the first few glyphs, in shuffled order to not have their IDs + match the clusters */ + glyphCache.addGlyph(fontId, 4, {}, {}); /* 1 */ + glyphCache.addGlyph(fontId, 0, {}, {}); /* 2 */ + glyphCache.addGlyph(fontId, 2, {}, {}); /* 3 */ + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + for(UnsignedInt i = 0; i != ids.size(); ++i) + ids[i] = i*2; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + advances[i] = {1.5f, 0.0f}; + offsets[i] = {0.0f, i % 2 ? 0.0f : 0.5f}; + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D& clusters) const override { + for(UnsignedInt i = 0; i != clusters.size(); ++i) + clusters[i] = 10 + i; + } + } shaper{font}; + + RendererCore renderer{glyphCache, data.flagsCore}; + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphPositions().data(), nullptr); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().data(), nullptr); + if(data.flagsCore >= RendererCoreFlag::GlyphClusters) { + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().data(), nullptr); + } + + /* Reserving with 0 should be a no-op */ + renderer.reserve(0, 0); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphPositions().data(), nullptr); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().data(), nullptr); + if(data.flagsCore >= RendererCoreFlag::GlyphClusters) { + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().data(), nullptr); + } + + /* The views should be non-null now even if no glyphs are rendered */ + renderer.reserve(data.reserveGlyphs, data.reserveRuns); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserveGlyphs); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), data.reserveRuns); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_VERIFY(renderer.glyphPositions().data()); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + CORRADE_VERIFY(renderer.glyphIds().data()); + if(data.flagsCore >= RendererCoreFlag::GlyphClusters) { + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_VERIFY(renderer.glyphClusters().data()); + } + + /* Rendering shouldn't reallocate anything */ + if(data.render) { + renderer.add(shaper, 1.0f, "abc"); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserveGlyphs); + CORRADE_COMPARE(renderer.runCapacity(), data.reserveRuns); + if(data.renderAddOnly) { + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_VERIFY(renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + if(data.flagsCore >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.runScales().size(), 0); + CORRADE_COMPARE(renderer.runEnds().size(), 0); + } else { + renderer.render(); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_VERIFY(!renderer.isRendering()); + /* 3 letters, which is 4.5 units with advance being 1.5, so + starting at -2.25 when centered, vertical center is at 0.25. */ + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + {-2.25f, -0.25f}, + {-0.75f, -0.75f}, + { 0.75f, -0.25f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.glyphIds(), Containers::arrayView({ + 2, 3, 1 /* font glyphs 0, 2, 4 */ + }), TestSuite::Compare::Container); + if(data.flagsCore >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({ + 10, 11, 12 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runScales(), Containers::arrayView({ + 1.0f + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runEnds(), Containers::arrayView({ + 3u + }), TestSuite::Compare::Container); + } + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); + } + + /* Reserving / rendering again should copy the existing data if not + reserved enough */ + const void* currentPositions = renderer.glyphPositions().data(); + const void* currentIds = renderer.glyphIds().data(); + const void* currentClusters = data.flagsCore >= RendererCoreFlag::GlyphClusters ? renderer.glyphClusters().data() : nullptr; + const void* currentRunScales = renderer.runScales().data(); + const void* currentRunEnds = renderer.runEnds().data(); + /* Reserving while a render is in progress shouldn't reset any internal + state */ + if(data.secondReserveGlyphs || data.secondReserveRuns) { + renderer.reserve(data.secondReserveGlyphs, data.secondReserveRuns); + CORRADE_COMPARE(renderer.glyphCapacity(), data.expectedGlyphCapacity); + CORRADE_COMPARE(renderer.runCapacity(), data.expectedRunCapacity); + CORRADE_COMPARE(renderer.isRendering(), data.renderAddOnly); + } + if(data.render) { + /* Make two more runs */ + renderer + .add(shaper, 4.0f/3.0f, "defghijk") + .render(shaper, 4.0f/3.0f, "lmnopqrstuvwxyz"); + CORRADE_COMPARE(renderer.glyphCount(), 26); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 26); + CORRADE_COMPARE(renderer.renderingRunCount(), 3); + } else { + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + } + CORRADE_COMPARE(renderer.glyphCapacity(), 26); + CORRADE_COMPARE(renderer.runCapacity(), 3); + + /* If it shouldn't reallocate, the views should stay the same */ + if(data.expectNoGlyphReallocation) { + CORRADE_COMPARE(renderer.glyphPositions().data(), currentPositions); + CORRADE_COMPARE(renderer.glyphIds().data(), currentIds); + if(data.flagsCore >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().data(), currentClusters); + } + if(data.expectNoRunReallocation) { + CORRADE_COMPARE(renderer.runScales().data(), currentRunScales); + CORRADE_COMPARE(renderer.runEnds().data(), currentRunEnds); + } + + /* Verify that both the original data and (prefix of) the new is there. If + only reserving, we have no way to know. */ + if(data.render) { + /* If the first part wasn't finalized, it's 26 letters in total, which + is 50.5 units with advance being 1.5 for the first 3 and 2.0 for the + rest, so starting at -25.25 when centered, vertical center is + -0.16667. */ + if(data.renderAddOnly) + CORRADE_COMPARE_AS(renderer.glyphPositions().prefix(5), Containers::arrayView({ + {-25.25f, -0.5f}, + {-23.75f, -1.0f}, + {-22.25f, -0.5f}, + {-20.75f, -0.3333333f}, /* Second part starts here */ + {-18.75f, -1.0f}, + }), TestSuite::Compare::Container); + /* Otherwise the first part is the same as already finalized above, and + the second part is 23 letters with advance 2.0, so starting at -23 + when centered */ + else + CORRADE_COMPARE_AS(renderer.glyphPositions().prefix(5), Containers::arrayView({ + {-2.25f, -0.25f}, + {-0.75f, -0.75f}, + { 0.75f, -0.25f}, + {-23.0f, -0.3333333f}, /* Second part starts here */ + {-21.0f, -1.0f}, + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.glyphIds().prefix(5), Containers::arrayView({ + 2, 3, 1, 2, 3 /* font glyphs 0, 2, 4, 0, 2 */ + }), TestSuite::Compare::Container); + if(data.flagsCore >= RendererCoreFlag::GlyphClusters) { + CORRADE_COMPARE_AS(renderer.glyphClusters().prefix(5), Containers::arrayView({ + 10, 11, 12, 10, 11 + }), TestSuite::Compare::Container); + } + CORRADE_COMPARE_AS(renderer.runScales(), Containers::arrayView({ + 1.0f, + 4.0f/3.0f, + 4.0f/3.0f + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runEnds(), Containers::arrayView({ + 3u, + 11u, + 26u + }), TestSuite::Compare::Container); + } +} + +void RendererTest::allocateCoreGlyphAllocator() { + auto&& data = AllocateCoreGlyphAllocatorData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + glyphCache.addFont(1, &font); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + /* Zero the IDs to not hit an OOB assert in the glyph cache */ + for(UnsignedInt& id: ids) + id = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + } shaper{font}; + + struct Allocation { + bool expectedGlyphClusters; + const Vector2* expectedGlyphPositionData; + const UnsignedInt* expectedGlyphIdData; + const UnsignedInt* expectedGlyphClusterData; + const Vector2* expectedGlyphAdvanceData; + + UnsignedInt expectedViewSize; + UnsignedInt expectedGlyphCount; + + Containers::StridedArrayView1D glyphPositions; + Containers::StridedArrayView1D glyphAdvances; + Containers::StridedArrayView1D glyphIds; + Containers::StridedArrayView1D glyphClusters; + int called = 0; + } allocation; + + RendererCore renderer{glyphCache, [](void* state, UnsignedInt glyphCount, Containers::StridedArrayView1D& glyphPositions, Containers::StridedArrayView1D& glyphIds, Containers::StridedArrayView1D* glyphClusters, Containers::StridedArrayView1D& glyphAdvances){ + Allocation& allocation = *static_cast(state); + CORRADE_COMPARE(glyphCount, allocation.expectedGlyphCount); + CORRADE_COMPARE(glyphPositions.data(), allocation.expectedGlyphPositionData); + CORRADE_COMPARE(glyphPositions.size(), allocation.expectedViewSize); + CORRADE_COMPARE(glyphIds.data(), allocation.expectedGlyphIdData); + CORRADE_COMPARE(glyphIds.size(), allocation.expectedViewSize); + CORRADE_COMPARE(glyphClusters, allocation.expectedGlyphClusters); + if(glyphClusters) { + CORRADE_COMPARE(glyphClusters->data(), allocation.expectedGlyphClusterData); + CORRADE_COMPARE(glyphClusters->size(), allocation.expectedViewSize); + } + CORRADE_COMPARE(glyphAdvances.data(), allocation.expectedGlyphAdvanceData); + /* The advances are never needed to be preserved, so it's always + empty */ + CORRADE_COMPARE(glyphAdvances.size(), 0); + + glyphPositions = allocation.glyphPositions; + glyphIds = allocation.glyphIds; + if(glyphClusters) + *glyphClusters = allocation.glyphClusters; + glyphAdvances = allocation.glyphAdvances; + ++allocation.called; + }, &allocation, nullptr, nullptr, data.flags}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + /* Initially it should pass all null views */ + allocation.expectedViewSize = 0; + allocation.expectedGlyphClusters = data.flags >= RendererCoreFlag::GlyphClusters; + allocation.expectedGlyphPositionData = nullptr; + allocation.expectedGlyphIdData = nullptr; + allocation.expectedGlyphClusterData = nullptr; + allocation.expectedGlyphAdvanceData = nullptr; + + /* Reserving with 0 should be a no-op */ + renderer.reserve(0, 0); + CORRADE_COMPARE(allocation.called, 0); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphPositions().data(), nullptr); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().data(), nullptr); + if(data.flags >= RendererCoreFlag::GlyphClusters) { + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().data(), nullptr); + } + + /* Rendering an empty text should be a no-op as well */ + if(data.render) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + renderer.render(shaper, 0.0f, ""); + CORRADE_COMPARE(allocation.called, 0); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphPositions().data(), nullptr); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().data(), nullptr); + if(data.flags >= RendererCoreFlag::GlyphClusters) { + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().data(), nullptr); + } + } + + /* Reserve an initial size to have somewhere to render to, pass each view + the same size */ + Vector2 glyphPositions[32]; + UnsignedInt glyphIds[32]; + UnsignedInt glyphClusters[32]; + Vector2 glyphAdvances[32]; + allocation.expectedViewSize = 0; + allocation.expectedGlyphCount = data.reserve; + allocation.glyphPositions = Containers::arrayView(glyphPositions) + .prefix(data.reserve); + allocation.glyphIds = Containers::arrayView(glyphIds) + .prefix(data.reserve); + allocation.glyphClusters = Containers::arrayView(glyphClusters) + .prefix(data.reserve); + allocation.glyphAdvances = Containers::arrayView(glyphAdvances) + .prefix(data.reserve); + { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + renderer.reserve(data.reserve, 0); + } + CORRADE_COMPARE(allocation.called, 1); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserve); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + + /* Rendering with enough capacity shouldn't reallocate anything */ + if(data.render) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + renderer.add(shaper, 0.0f, "abc"); + if(data.renderAddOnly) { + CORRADE_VERIFY(renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + } else { + renderer.render(); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_COMPARE(renderer.glyphPositions().size(), 3); + CORRADE_COMPARE(renderer.glyphIds().size(), 3); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 3); + } + CORRADE_COMPARE(allocation.called, 1); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserve); + CORRADE_COMPARE(renderer.runCapacity(), 1); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); + /* No need to verify the actual contents, just that the views didn't + change since last time */ + CORRADE_COMPARE(renderer.glyphPositions().data(), glyphPositions); + CORRADE_COMPARE(renderer.glyphIds().data(), glyphIds); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().data(), glyphClusters); + } + + /* Reserve / render second time. Pass each with a different size, it should + pick the smallest as capacity. */ + allocation.expectedGlyphPositionData = glyphPositions; + allocation.expectedGlyphIdData = glyphIds; + allocation.expectedGlyphClusterData = glyphClusters; + allocation.expectedGlyphAdvanceData = glyphAdvances; + Vector2 glyphPositions2[32]; + UnsignedInt glyphIds2[32]; + UnsignedInt glyphClusters2[32]; + Vector2 glyphAdvances2[32]; + allocation.glyphPositions = Containers::arrayView(glyphPositions2) + .prefix(data.positionSize); + allocation.glyphIds = Containers::arrayView(glyphIds2) + .prefix(data.idSize); + allocation.glyphClusters = Containers::arrayView(glyphClusters2) + .prefix(data.clusterSize); + allocation.glyphAdvances = Containers::arrayView(glyphAdvances2) + .prefix(data.advanceSize); + if(data.render) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + allocation.expectedViewSize = 3; + allocation.expectedGlyphCount = data.secondReserve - 3; + renderer.render(shaper, 0.0f, "defghijklmnopqrstuvwxyz"); + CORRADE_COMPARE(renderer.glyphCount(), 26); + CORRADE_COMPARE(renderer.runCount(), 2); + CORRADE_COMPARE(renderer.runCapacity(), 2); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 26); + CORRADE_COMPARE(renderer.renderingRunCount(), 2); + } else { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + allocation.expectedViewSize = 0; + allocation.expectedGlyphCount = data.secondReserve; + renderer.reserve(data.secondReserve, 0); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + } + CORRADE_COMPARE(renderer.glyphCapacity(), data.expectedCapacity); + + /* If it shouldn't reallocate, the views should stay the same as before, + otherwise they should be what was passed above. The allocator is assumed + to perform the data copy, the one in this test deliberately doesn't. */ + if(data.expectNoReallocation) { + CORRADE_COMPARE(allocation.called, 1); + CORRADE_COMPARE(renderer.glyphPositions().data(), glyphPositions); + CORRADE_COMPARE(renderer.glyphIds().data(), glyphIds); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().data(), glyphClusters); + } else { + CORRADE_COMPARE(allocation.called, 2); + CORRADE_COMPARE(renderer.glyphPositions().data(), glyphPositions2); + CORRADE_COMPARE(renderer.glyphIds().data(), glyphIds2); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().data(), glyphClusters2); + } +} + +void RendererTest::allocateCoreGlyphAllocatorInvalid() { + auto&& data = AllocateCoreGlyphAllocatorInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + glyphCache.addFont(1, &font); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + /* Zero the IDs to not hit an OOB assert in the glyph cache */ + for(UnsignedInt& id: ids) + id = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + } shaper{font}; + + struct Allocation { + Vector2 glyphPositions[20]; + UnsignedInt glyphIds[20]; + UnsignedInt glyphClusters[20]; + Vector2 glyphAdvances[20]; + + /* For the initial render() */ + UnsignedInt positionSize = 10; + UnsignedInt idSize = 10; + UnsignedInt clusterSize = 10; + UnsignedInt advanceSize = 10; + } allocation; + + RendererCore renderer{glyphCache, [](void* state, UnsignedInt, Containers::StridedArrayView1D& glyphPositions, Containers::StridedArrayView1D& glyphIds, Containers::StridedArrayView1D* glyphClusters, Containers::StridedArrayView1D& glyphAdvances){ + Allocation& allocation = *static_cast(state); + + glyphPositions = Containers::arrayView(allocation.glyphPositions).prefix(allocation.positionSize); + glyphIds = Containers::arrayView(allocation.glyphIds).prefix(allocation.idSize); + if(glyphClusters) + *glyphClusters = Containers::arrayView(allocation.glyphClusters).prefix(allocation.clusterSize); + glyphAdvances = Containers::arrayView(allocation.glyphAdvances).prefix(allocation.advanceSize); + }, &allocation, nullptr, nullptr, data.flags}; + + /* Render something to have a non-zero glyph count */ + renderer.render(shaper, 0.0f, "abcdefghij"); + CORRADE_COMPARE(renderer.glyphCount(), 10); + CORRADE_COMPARE(renderer.glyphCapacity(), 10); + + /* Next reserve / render should be with these */ + allocation.positionSize = data.positionSize; + allocation.idSize = data.idSize; + allocation.clusterSize = data.clusterSize; + allocation.advanceSize = data.advanceSize; + { + Containers::String out; + Error redirectError{&out}; + if(data.render) + renderer.render(shaper, 0.0f, "klmnopq"); + else + renderer.reserve(17, 0); + CORRADE_COMPARE_AS(out, data.expected, + TestSuite::Compare::String); + } + + /* Just to verify it's okay when the sizes are exactly right */ + allocation.positionSize = 17; + allocation.idSize = 17; + allocation.clusterSize = 17; + allocation.advanceSize = 7; /* This one in particular */ + if(data.render) { + renderer.render(shaper, 0.0f, "klmnopq"); + CORRADE_COMPARE(renderer.glyphCount(), 17); + } else { + renderer.reserve(17, 0); + CORRADE_COMPARE(renderer.glyphCount(), 10); + } + CORRADE_COMPARE(renderer.glyphCapacity(), 17); +} + +void RendererTest::allocateCoreRunAllocator() { + auto&& data = AllocateCoreRunAllocatorData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + glyphCache.addFont(1, &font); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + /* Zero the IDs to not hit an OOB assert in the glyph cache */ + for(UnsignedInt& id: ids) + id = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + } shaper{font}; + + struct Allocation { + const Float* expectedRunScaleData; + const UnsignedInt* expectedRunEndData; + + UnsignedInt expectedViewSize; + UnsignedInt expectedRunCount; + + Containers::StridedArrayView1D runScales; + Containers::StridedArrayView1D runEnds; + int called = 0; + } allocation; + + RendererCore renderer{glyphCache, nullptr, nullptr, [](void* state, UnsignedInt runCount, Containers::StridedArrayView1D& runScales, Containers::StridedArrayView1D& runEnds){ + Allocation& allocation = *static_cast(state); + CORRADE_COMPARE(runCount, allocation.expectedRunCount); + CORRADE_COMPARE(runScales.data(), allocation.expectedRunScaleData); + CORRADE_COMPARE(runScales.size(), allocation.expectedViewSize); + CORRADE_COMPARE(runEnds.data(), allocation.expectedRunEndData); + CORRADE_COMPARE(runEnds.size(), allocation.expectedViewSize); + + runScales = allocation.runScales; + runEnds = allocation.runEnds; + ++allocation.called; + }, &allocation}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + /* Initially it should pass all null views */ + allocation.expectedViewSize = 0; + allocation.expectedRunScaleData = nullptr; + allocation.expectedRunEndData = nullptr; + + /* Reserving with 0 should be a no-op */ + renderer.reserve(0, 0); + CORRADE_COMPARE(allocation.called, 0); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.runScales().size(), 0); + CORRADE_COMPARE(renderer.runScales().data(), nullptr); + CORRADE_COMPARE(renderer.runEnds().size(), 0); + CORRADE_COMPARE(renderer.runEnds().data(), nullptr); + + /* Rendering an empty text should be a no-op as well, even with multiple + add() calls */ + if(data.render) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + renderer + .add(shaper, 0.0f, "") + .add(shaper, 0.0f, "") + .render(shaper, 0.0f, ""); + CORRADE_COMPARE(allocation.called, 0); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.runScales().size(), 0); + CORRADE_COMPARE(renderer.runScales().data(), nullptr); + CORRADE_COMPARE(renderer.runEnds().size(), 0); + CORRADE_COMPARE(renderer.runEnds().data(), nullptr); + } + + /* Reserve an initial size to have somewhere to render to, pass each view + the same size */ + Float runScales[8]; + UnsignedInt runEnds[8]; + allocation.expectedViewSize = 0; + allocation.expectedRunCount = data.reserve; + allocation.runScales = Containers::arrayView(runScales) + .prefix(data.reserve); + allocation.runEnds = Containers::arrayView(runEnds) + .prefix(data.reserve); + { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + renderer.reserve(0, data.reserve); + } + CORRADE_COMPARE(allocation.called, 1); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), data.reserve); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + + /* Rendering with enough capacity shouldn't reallocate anything */ + if(data.render) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + renderer + .add(shaper, 0.0f, "a") + .add(shaper, 0.0f, "b") + .add(shaper, 0.0f, "c"); + if(data.renderAddOnly) { + CORRADE_VERIFY(renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runScales().size(), 0); + CORRADE_COMPARE(renderer.runEnds().size(), 0); + } else { + renderer.render(); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.runCount(), 3); + CORRADE_COMPARE(renderer.runScales().size(), 3); + CORRADE_COMPARE(renderer.runEnds().size(), 3); + } + CORRADE_COMPARE(allocation.called, 1); + CORRADE_COMPARE(renderer.glyphCapacity(), 3); + CORRADE_COMPARE(renderer.runCapacity(), data.reserve); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 3); + /* No need to verify the actual contents, just that the views didn't + change since last time */ + CORRADE_COMPARE(renderer.runScales().data(), runScales); + CORRADE_COMPARE(renderer.runEnds().data(), runEnds); + } + + /* Reserve / render second time. Pass each with a different size, it should + pick the smallest as capacity. */ + allocation.expectedRunScaleData = runScales; + allocation.expectedRunEndData = runEnds; + Float runScales2[8]; + UnsignedInt runEnds2[8]; + allocation.runScales = Containers::arrayView(runScales2) + .prefix(data.scaleSize); + allocation.runEnds = Containers::arrayView(runEnds2) + .prefix(data.endSize); + if(data.render) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + allocation.expectedViewSize = 3; + allocation.expectedRunCount = 1; + renderer.render(shaper, 0.0f, "defghijklmnopqrstuvwxyz"); + CORRADE_COMPARE(renderer.glyphCount(), 26); + CORRADE_COMPARE(renderer.glyphCapacity(), 26); + CORRADE_COMPARE(renderer.runCount(), 4); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 26); + CORRADE_COMPARE(renderer.renderingRunCount(), 4); + } else { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + allocation.expectedViewSize = 0; + allocation.expectedRunCount = data.secondReserve; + renderer.reserve(0, data.secondReserve); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + } + CORRADE_COMPARE(renderer.runCapacity(), data.expectedCapacity); + + /* If it shouldn't reallocate, the views should stay the same as before, + otherwise they should be what was passed above. The allocator is assumed + to perform the data copy, the one in this test deliberately doesn't. */ + if(data.expectNoReallocation) { + CORRADE_COMPARE(allocation.called, 1); + CORRADE_COMPARE(renderer.runScales().data(), runScales); + CORRADE_COMPARE(renderer.runEnds().data(), runEnds); + } else { + CORRADE_COMPARE(allocation.called, 2); + CORRADE_COMPARE(renderer.runScales().data(), runScales2); + CORRADE_COMPARE(renderer.runEnds().data(), runEnds2); + } +} + +void RendererTest::allocateCoreRunAllocatorInvalid() { + auto&& data = AllocateCoreRunAllocatorInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font; + glyphCache.addFont(1, &font); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + /* Zero the IDs to not hit an OOB assert in the glyph cache */ + for(UnsignedInt& id: ids) + id = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + } shaper{font}; + + struct Allocation { + Float runScales[8]; + UnsignedInt runEnds[8]; + + /* For the initial render() */ + UnsignedInt scaleSize = 2; + UnsignedInt endSize = 2; + } allocation; + + RendererCore renderer{glyphCache, nullptr, nullptr, [](void* state, UnsignedInt, Containers::StridedArrayView1D& runScales, Containers::StridedArrayView1D& runEnds){ + Allocation& allocation = *static_cast(state); + + runScales = Containers::arrayView(allocation.runScales).prefix(allocation.scaleSize); + runEnds = Containers::arrayView(allocation.runEnds).prefix(allocation.endSize); + }, &allocation}; + + /* Render something to have a non-zero run count */ + renderer + .add(shaper, 0.0f, "abcde") + .render(shaper, 0.0f, "fghij"); + CORRADE_COMPARE(renderer.runCount(), 2); + CORRADE_COMPARE(renderer.runCapacity(), 2); + + /* Next reserve / render should be with these */ + allocation.scaleSize = data.scaleSize; + allocation.endSize = data.endSize; + { + if(data.render) + renderer + .add(shaper, 0.0f, "kl") + .render(shaper, 0.0f, "mn"); + + Containers::String out; + Error redirectError{&out}; + if(data.render) + renderer.render(shaper, 0.0f, "opq"); + else + renderer.reserve(0, 5); + CORRADE_COMPARE_AS(out, data.expected, + TestSuite::Compare::String); + } + + /* Just to verify it's okay when the sizes are exactly right */ + allocation.scaleSize = 5; + allocation.endSize = 5; + if(data.render) { + renderer.render(shaper, 0.0f, "opq"); + CORRADE_COMPARE(renderer.runCount(), 5); + } else { + renderer.reserve(0, 5); + CORRADE_COMPARE(renderer.runCount(), 2); + } + CORRADE_COMPARE(renderer.runCapacity(), 5); +} + +void RendererTest::addSingleLine() { + auto&& data = AddSingleLineData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + /* Set padding to zero for easier dummy glyph addition below */ + } glyphCache{PixelFormat::R8Unorm, {16, 16}, {}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) override { + _opened = true; + /* The size is used to scale everything. Ascent, descent is used + for the bounds rect. Line height isn't used for anything, glyph + count is overriden in addFont() below. */ + return {size, 16.0f, -8.0f, 1000.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; + } font1, font2; + /* Two fonts that do the same but each is opened with a different size */ + font1.openFile("", 1.0f); + font2.openFile("", 2.0f); + for(AbstractFont* font: {&font1, &font2}) { + UnsignedInt fontId = glyphCache.addFont('o' + 1, font); + /* Shuffled order to not have their IDs match the clusters */ + glyphCache.addGlyph(fontId, 'e', {}, {}); /* 1 or 9 */ + glyphCache.addGlyph(fontId, 'E', {}, {}); /* 2 or 10 */ + glyphCache.addGlyph(fontId, 'l', {}, {}); /* 3 or 11 */ + glyphCache.addGlyph(fontId, 'H', {}, {}); /* 4 or 12 */ + glyphCache.addGlyph(fontId, 'L', {}, {}); /* 5 or 13 */ + glyphCache.addGlyph(fontId, 'h', {}, {}); /* 6 or 14 */ + glyphCache.addGlyph(fontId, 'O', {}, {}); /* 7 or 15 */ + glyphCache.addGlyph(fontId, 'o', {}, {}); /* 8 or 16 */ + } + + struct Shaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features) override { + if(begin == advertiseShapeDirectionAt) + _direction = shapeDirectionToAdvertise; + else + _direction = ShapeDirection::Unspecified; + + /* The text is always the same, the begin / end is different */ + CORRADE_COMPARE(text, expectedText); + CORRADE_COMPARE(begin, expectedBegin); + CORRADE_COMPARE(end, expectedEnd); + + /* Verify just that these are passed at all, it's always the same */ + CORRADE_COMPARE(features.size(), 2); + CORRADE_COMPARE(features[1].feature(), Feature::CharacterVariants66); + + /* Produce twice as many glyphs for the input to verify it's not a + 1:1 mapping from bytes to glyphs */ + return (end - begin)*2; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + /* Each input letter is mapped to a pair of uppercase and + lowercase chars, which act as glyph IDs */ + for(UnsignedInt i = 0; i != ids.size(); ++i) { + ids[i] = expectedText[expectedBegin + i/2]; + if(i % 2 == 0) + ids[i] &= ~('A' ^ 'a'); + } + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + /* Uppercase letters have bigger advance than lowercase, L is + special, lowercase additionally have an Y offset, except L. + + Undoing the size multiplier here so the final output has still + the same absolute advances and only scales the ascent/descent. */ + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + char glyphId = expectedText[expectedBegin + i/2]; + if(glyphId == 'h' || glyphId == 'e' || glyphId == 'o') + advances[i] = {i % 2 ? 4.0f/(sizeMultiplier/font().size()) : 6.0f/(sizeMultiplier/font().size()), 0.0f}; + else if(glyphId == 'l') + advances[i] = {3.0f/(sizeMultiplier/font().size()), 0.0f}; + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + + if(i % 2 && (glyphId == 'h' || glyphId == 'e' || glyphId == 'o')) + offsets[i] = {0.0f, -1.0f/(sizeMultiplier/font().size())}; + else + offsets[i] = {0.0f, 0.0f}; + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D& clusters) const override { + for(UnsignedInt i = 0; i != clusters.size(); ++i) + clusters[i] = expectedBegin + i/2; + } + ShapeDirection doDirection() const override { + /* In case of a single line shape() should always get called before + direction is queried. In a multi-line scenario not, which is + verified in addMultipleLines() below. */ + CORRADE_FAIL_IF(_direction == ShapeDirection(0xff), + "Shape direction queried before calling shape()"); + return _direction; + } + + ShapeDirection shapeDirectionToAdvertise; + UnsignedInt advertiseShapeDirectionAt; + Float sizeMultiplier; + + const char* expectedText; + UnsignedInt expectedBegin, expectedEnd; + + private: + ShapeDirection _direction = ShapeDirection(0xff); + } shaper1{font1}, shaper2{font2}; + for(Shaper* shaper: {&shaper1, &shaper2}) { + shaper->shapeDirectionToAdvertise = data.shapeDirection; + shaper->advertiseShapeDirectionAt = data.advertiseShapeDirectionAt; + } + + RendererCore renderer{glyphCache, data.flags}; + renderer + /* Non-default cursor position */ + .setCursor({-50.0f, 100.0f}) + /* Alignment to LineRight, but can be specified as start / end and then + it'd depend on used LayoutDirection */ + .setAlignment(data.alignment); + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + Containers::Pair out; + if(data.direct) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + CORRADE_COMPARE(data.items.size(), 1); + auto& item = data.items[0]; + shaper1.sizeMultiplier = item.third(); + shaper1.expectedText = "hello"; + shaper1.expectedBegin = item.first(); + shaper1.expectedEnd = item.second(); + out = renderer.render(shaper1, item.third(), "hello", { + Feature::Kerning, + Feature::CharacterVariants66 + }); + } else { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + for(std::size_t i = 0; i != data.items.size(); ++i) { + auto& item = data.items[i]; + CORRADE_ITERATION(item); + + Shaper& shaper = i % 2 ? shaper2 : shaper1; + + shaper.sizeMultiplier = item.third(); + shaper.expectedText = "___hello--"; + shaper.expectedBegin = item.first(); + shaper.expectedEnd = item.second(); + renderer.add(shaper, item.third(), "___hello--", item.first(), item.second(), { + Feature::Kerning, + Feature::CharacterVariants66 + }); + + /* The cursor should stay as set initially, only the "rendering" + count gets updated */ + CORRADE_COMPARE(renderer.cursor(), (Vector2{-50.0f, 100.0f})); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + /* Not testing the "rendering" counts here as it's too laborous, + only at the end */ + CORRADE_COMPARE(renderer.runScales().size(), 0); + CORRADE_COMPARE(renderer.runEnds().size(), 0); + } + + out = renderer.render(); + } + + /* At the end, it shouldn't be in progress anymore. The cursor should be + still as set initially. */ + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 10); + CORRADE_COMPARE(renderer.runCount(), data.expectedRuns.size()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 10); + CORRADE_COMPARE(renderer.renderingRunCount(), data.expectedRuns.size()); + CORRADE_COMPARE(renderer.cursor(), (Vector2{-50.0f, 100.0f})); + CORRADE_COMPARE_AS(renderer.glyphCapacity(), 10, + TestSuite::Compare::GreaterOrEqual); + CORRADE_COMPARE_AS(renderer.runCapacity(), data.expectedRuns.size(), + TestSuite::Compare::GreaterOrEqual); + + /* The rendered output should have 2x as many glyphs as input bytes, should + have the right baseline at the cursor in all cases and the rect height + should be depending on the largest font size. */ + CORRADE_COMPARE(out, Containers::pair( + Range2D::fromSize({-42.0f, -data.expectedRectHeight/3.0f}, + {42.0f, data.expectedRectHeight}) + .translated({-50.0f, 100.0f}), + Range1Dui{0, UnsignedInt(data.expectedRuns.size())})); + + /* The contents should be the same independently of how many pieces was + added. All glyph positions are shifted based on the cursor. */ + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + {-50.0f - 42.0f, 100.0f - 0.0f}, /* H */ + {-50.0f - 36.0f, 100.0f - 1.0f}, /* h */ + {-50.0f - 32.0f, 100.0f - 0.0f}, /* E */ + {-50.0f - 26.0f, 100.0f - 1.0f}, /* e */ + {-50.0f - 22.0f, 100.0f - 0.0f}, /* L */ + {-50.0f - 19.0f, 100.0f - 0.0f}, /* l */ + {-50.0f - 16.0f, 100.0f - 0.0f}, /* L */ + {-50.0f - 13.0f, 100.0f - 0.0f}, /* l */ + {-50.0f - 10.0f, 100.0f - 0.0f}, /* O */ + {-50.0f - 4.0f, 100.0f - 1.0f}, /* o */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.glyphIds(), + Containers::arrayView(data.expectedGlyphIds), + TestSuite::Compare::Container); + if(data.flags >= RendererCoreFlag::GlyphClusters) { + if(data.direct) + CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({ + 0, 0, 1, 1, 2, 2, 3, 3, 4, 4 + }), TestSuite::Compare::Container); + else + CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({ + 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 + }), TestSuite::Compare::Container); + } + CORRADE_COMPARE_AS(renderer.runScales(), + stridedArrayView(data.expectedRuns).slice(&Containers::Pair::first), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runEnds(), + stridedArrayView(data.expectedRuns).slice(&Containers::Pair::second), + TestSuite::Compare::Container); +} + +void RendererTest::addSingleLineAlign() { + auto&& data = AddSingleLineAlignData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + TestFont font; + font.openFile({}, 0.5f); + DummyGlyphCache glyphCache = testGlyphCache(font); + TestShaper shaper{font, data.shapeDirection}; + + RendererCore renderer{glyphCache}; + renderer.setAlignment(data.alignment); + + /* Bounds are different depending on whether or not GlyphBounds alignment + is used */ + CORRADE_COMPARE(renderer.render(shaper, 0.25f, "abc"), Containers::pair( + (UnsignedByte(data.alignment) & Implementation::AlignmentGlyphBounds ? + Range2D{{2.5f, 3.75f}, {12.5f, 10.5f}} : + Range2D{{0.0f, -1.25f}, {3.0f, 2.25f}}).translated(data.offset), + Range1Dui{0, 1})); + + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + /* Cursor is {0, 0}. Glyph offset {0, 1}, scaled by 0.5. */ + Vector2{0.0f, 0.5f} + data.offset, + + /* Advance was {1, 0.5}*0.5, cursor is {0.5, 0.25}. Glyph offset is + {0, 2}, scaled by 0.5. */ + Vector2{0.5f, 1.25f} + data.offset, + + /* Advance was {2, -0.5}*0.5, cursor is {1.5, 0}. Glyph offset is + {0, 3}, scaled by 0.5. */ + Vector2{1.5f, 1.5f} + data.offset, + }), TestSuite::Compare::Container); +} + +void RendererTest::addMultipleLines() { + auto&& data = AddMultipleLinesData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Expanded variant of addSingleLine() with newlines being a part of the + text and optional line advance adjustment in exchange for dropped size + multiplication */ + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + /* Set padding to zero for easier dummy glyph addition below */ + } glyphCache{PixelFormat::R8Unorm, {16, 16}, {}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) override { + _opened = true; + /* The size is used to scale everything. Ascent, descent, line + height is used for the bounds rect. Glyph count is overriden in + addFont() below. */ + return {size, 16.0f*size, -8.0f*size, 32.0f*size, 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; + } font1, font2; + font1.openFile("", 1.0f); + font2.openFile("", 2.0f); + for(AbstractFont* font: {&font1, &font2}) { + UnsignedInt fontId = glyphCache.addFont('o' + 1, font); + /* Shuffled order to not have their IDs match the clusters */ + glyphCache.addGlyph(fontId, 'e', {}, {}); /* 1 or 9 */ + glyphCache.addGlyph(fontId, 'E', {}, {}); /* 2 or 10 */ + glyphCache.addGlyph(fontId, 'l', {}, {}); /* 3 or 11 */ + glyphCache.addGlyph(fontId, 'H', {}, {}); /* 4 or 12 */ + glyphCache.addGlyph(fontId, 'L', {}, {}); /* 5 or 13 */ + glyphCache.addGlyph(fontId, 'h', {}, {}); /* 6 or 14 */ + glyphCache.addGlyph(fontId, 'O', {}, {}); /* 7 or 15 */ + glyphCache.addGlyph(fontId, 'o', {}, {}); /* 8 or 16 */ + } + + struct Shaper: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features) override { + if(begin == advertiseShapeDirectionAt) + _direction = shapeDirectionToAdvertise; + else + _direction = ShapeDirection::Unspecified; + + /* The text is always the same, the begin / end is different */ + CORRADE_COMPARE(text, expectedText); + currentBegin = begin; + arrayAppend(calls, InPlaceInit, begin, end); + + /* Verify just that these are passed at all, it's always the same */ + CORRADE_COMPARE(features.size(), 2); + CORRADE_COMPARE(features[1].feature(), Feature::CharacterVariants66); + + /* Produce twice as many glyphs for the input to verify it's not a + 1:1 mapping from bytes to glyphs */ + return (end - begin)*2; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + /* Each input letter is mapped to a pair of uppercase and + lowercase chars, which act as glyph IDs */ + for(UnsignedInt i = 0; i != ids.size(); ++i) { + ids[i] = expectedText[currentBegin + i/2]; + if(i % 2 == 0) + ids[i] &= ~('A' ^ 'a'); + } + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + /* Uppercase letters have bigger advance than lowercase, L is + special, lowercase additionally have an Y offset, except L. */ + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + char glyphId = expectedText[currentBegin + i/2]; + if(glyphId == 'h' || glyphId == 'e' || glyphId == 'o') + advances[i] = {i % 2 ? 4.0f*font().size() : 6.0f*font().size(), 0.0f}; + else if(glyphId == 'l') + advances[i] = {3.0f*font().size(), 0.0f}; + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + + if(i % 2 && (glyphId == 'h' || glyphId == 'e' || glyphId == 'o')) + offsets[i] = {0.0f, -1.0f*font().size()}; + else + offsets[i] = {0.0f, 0.0f}; + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D& clusters) const override { + for(UnsignedInt i = 0; i != clusters.size(); ++i) + clusters[i] = currentBegin + i/2; + } + ShapeDirection doDirection() const override { + return _direction; + } + + ShapeDirection shapeDirectionToAdvertise; + UnsignedInt advertiseShapeDirectionAt; + + const char* expectedText; + UnsignedInt currentBegin; + Containers::Array> calls; + + private: + /* It may happen that direction is queried even before shape(), in + particular in the "each successive line separately with \n at + the beginning" case, so provide a non-random value there */ + ShapeDirection _direction = ShapeDirection::Unspecified; + } shaper1{font1}, shaper2{font2}; + for(Shaper* shaper: {&shaper1, &shaper2}) { + shaper->shapeDirectionToAdvertise = data.shapeDirection; + shaper->advertiseShapeDirectionAt = data.advertiseShapeDirectionAt; + } + + RendererCore renderer{glyphCache, data.flags}; + renderer + /* Non-default cursor position */ + .setCursor({-50.0f, 100.0f}) + /* Alignment to the right / bottom, but can be specified as start / end + and then it'd depend on used LayoutDirection */ + .setAlignment(data.alignment); + if(data.lineAdvance != 0.0f) + renderer.setLineAdvance(data.lineAdvance); + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + Containers::Pair out; + if(data.direct) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + CORRADE_COMPARE(data.items.size(), 1); + shaper1.expectedText = "he\nll\n\no"; + out = renderer.render(shaper1, 1.0f, "he\nll\n\no", { + Feature::Kerning, + Feature::CharacterVariants66 + }); + CORRADE_COMPARE_AS(shaper1.calls, + /* This is always three items for three lines */ + Containers::arrayView(data.items[0].third()), + TestSuite::Compare::Container); + } else { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + for(std::size_t i = 0; i != data.items.size(); ++i) { + auto& item = data.items[i]; + CORRADE_ITERATION(Containers::pair(item.first(), item.second())); + + Shaper& shaper = i % 2 ? shaper2 : shaper1; + + /* Extra newline characters outside of the desired range shouldn't + be taken into account in any way */ + shaper.calls = {}; + shaper.expectedText = "\n\n_he\nll\n\no-\n"; + renderer.add(shaper, 1.0f, "\n\n_he\nll\n\no-\n", item.first(), item.second(), { + Feature::Kerning, + Feature::CharacterVariants66 + }); + /* Consider only the non-empty prefix in the expected output */ + std::size_t prefix = 0; + for(Containers::Pair j: item.third()) + if(j == Containers::pair(0u, 0u)) + break; + else + ++prefix; + CORRADE_COMPARE_AS(shaper.calls, + Containers::arrayView(item.third()).prefix(prefix), + TestSuite::Compare::Container); + + /* The cursor should stay as set initially, only the "rendering" + count gets updated */ + CORRADE_COMPARE(renderer.cursor(), (Vector2{-50.0f, 100.0f})); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphIds().size(), 0); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + /* Not testing the "rendering" counts here as it's too laborous, + only at the end */ + CORRADE_COMPARE(renderer.runScales().size(), 0); + CORRADE_COMPARE(renderer.runEnds().size(), 0); + CORRADE_COMPARE(renderer.cursor(), (Vector2{-50.0f, 100.0f})); + } + + out = renderer.render(); + } + + /* At the end, it shouldn't be in progress anymore. The cursor should be + still as set initially. */ + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 10); + CORRADE_COMPARE(renderer.runCount(), data.expectedRuns.size()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 10); + CORRADE_COMPARE(renderer.renderingRunCount(), data.expectedRuns.size()); + CORRADE_COMPARE(renderer.cursor(), (Vector2{-50.0f, 100.0f})); + CORRADE_COMPARE_AS(renderer.glyphCapacity(), 10, + TestSuite::Compare::GreaterOrEqual); + CORRADE_COMPARE_AS(renderer.runCapacity(), data.expectedRuns.size(), + TestSuite::Compare::GreaterOrEqual); + + /* The rendered output should have 2x as many glyphs as input bytes, should + have the right baseline at the cursor in all cases and the rect height + should be depending on the largest font size. */ + CORRADE_COMPARE(out, Containers::pair( + Range2D::fromSize({-20.0f, -data.expectedRectHeight + 16.0f}, + {20.0f, data.expectedRectHeight}) + .translated({-50.0f, 100.0f}), + Range1Dui{0, UnsignedInt(data.expectedRuns.size())})); + + /* The contents should be the same independently of how many pieces was + added. All glyph positions are shifted based on the cursor. */ + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + {-50.0f - 20.0f, 100.0f - 0.0f*data.expectedLineAdvance - 0.0f}, /* H */ + {-50.0f - 14.0f, 100.0f - 0.0f*data.expectedLineAdvance - 1.0f}, /* h */ + {-50.0f - 10.0f, 100.0f - 0.0f*data.expectedLineAdvance - 0.0f}, /* E */ + {-50.0f - 4.0f, 100.0f - 0.0f*data.expectedLineAdvance - 1.0f}, /* e */ + /* One newline here */ + {-50.0f - 12.0f, 100.0f - 1.0f*data.expectedLineAdvance - 0.0f}, /* L */ + {-50.0f - 9.0f, 100.0f - 1.0f*data.expectedLineAdvance - 0.0f}, /* l */ + {-50.0f - 6.0f, 100.0f - 1.0f*data.expectedLineAdvance - 0.0f}, /* L */ + {-50.0f - 3.0f, 100.0f - 1.0f*data.expectedLineAdvance - 0.0f}, /* l */ + /* Two newlines here */ + {-50.0f - 10.0f, 100.0f - 3.0f*data.expectedLineAdvance - 0.0f}, /* O */ + {-50.0f - 4.0f, 100.0f - 3.0f*data.expectedLineAdvance - 1.0f}, /* o */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.glyphIds(), + Containers::arrayView(data.expectedGlyphIds), + TestSuite::Compare::Container); + if(data.flags >= RendererCoreFlag::GlyphClusters) { + if(data.direct) + CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({ + /* 2, 5, 6 is a \n */ + 0, 0, 1, 1, 3, 3, 4, 4, 7, 7 + }), TestSuite::Compare::Container); + else + CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({ + /* 5, 8, 9 is a \n */ + 3, 3, 4, 4, 6, 6, 7, 7, 10, 10 + }), TestSuite::Compare::Container); + } + CORRADE_COMPARE_AS(renderer.runScales(), + stridedArrayView(data.expectedRuns).slice(&Containers::Pair::first), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runEnds(), + stridedArrayView(data.expectedRuns).slice(&Containers::Pair::second), + TestSuite::Compare::Container); +} + +void RendererTest::addMultipleLinesAlign() { + auto&& data = AddMultipleLinesAlignData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) override { + _opened = true; + /* Compared to the glyph bounds, which are from 0 to 2, this is + shifted by one unit, thus by 0.5 in the output */ + return {size, 1.0f, -1.0f, 8.0f, 10}; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D& glyphs) override { + for(UnsignedInt& i: glyphs) + i = 0; + } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + + Containers::Pointer doCreateShaper() override { return {}; } + + bool _opened = false; + } font; + font.openFile({}, 0.5f); + + 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] = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + offsets[i] = {}; + advances[i] = Vector2::xAxis(4.0f); + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} + } shaper{font}; + + /* Just a single glyph that scales to {1, 1} in the end. Default padding is + 1 which would prevent this, set it back to 0. */ + DummyGlyphCache glyphCache{PixelFormat::R8Unorm, {20, 20}, {}}; + UnsignedInt fontId = glyphCache.addFont(1, &font); + glyphCache.addGlyph(fontId, 0, {}, {{}, {2, 2}}); + + RendererCore renderer{glyphCache}; + renderer.setAlignment(data.alignment); + + /* We're rendering text at 0.25f size and the font is scaled to 0.5f, so + the line advance should be 8.0f*0.25f/0.5f = 4.0f */ + CORRADE_COMPARE(font.size(), 0.5f); + CORRADE_COMPARE(font.lineHeight(), 8.0f); + + /* Bounds are different depending on whether or not GlyphBounds alignment + is used. The advance for the rightmost glyph is one unit larger than the + actual bounds so it's different on X between the two variants */ + CORRADE_COMPARE(renderer.render(shaper, 0.25f, "abcd\nef\n\nghi"), Containers::pair( + (UnsignedByte(data.alignment) & Implementation::AlignmentGlyphBounds ? + Range2D{{0.0f, -12.0f}, {7.0f, 1.0f}} : + Range2D{{0.0f, -12.5f}, {8.0f, 0.5f}}).translated(data.offset0), + Range1Dui{0, 1})); + + /* Vertices + [a] [b] [c] [d] + [e] [f] + + [g] [h] [i] */ + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + Vector2{0.0f, 0.0f} + data.offset0, /* a */ + Vector2{2.0f, 0.0f} + data.offset0, /* b */ + Vector2{4.0f, 0.0f} + data.offset0, /* c */ + Vector2{6.0f, 0.0f} + data.offset0, /* d */ + + Vector2{0.0f, 0.0f} + data.offset1, /* e */ + Vector2{2.0f, 0.0f} + data.offset1, /* f */ + + /* Two linebreaks here */ + + Vector2{0.0f, 0.0f} + data.offset2, /* g */ + Vector2{2.0f, 0.0f} + data.offset2, /* h */ + Vector2{4.0f, 0.0f} + data.offset2, /* i */ + }), TestSuite::Compare::Container); +} + +void RendererTest::addFontNotFoundInCache() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + } font1, font2, font3; + glyphCache.addFont(0, &font1); + /* font2 not */ + glyphCache.addFont(0, &font3); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 0; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&) const override {} + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) const override {} + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override {} + } shaper{font2}; + + RendererCore renderer{glyphCache}; + + Containers::String out; + Error redirectError{&out}; + renderer.add(shaper, 0.0f, "hello"); + CORRADE_COMPARE(out, "Text::RendererCore::add(): shaper font not found among 2 fonts in associated glyph cache\n"); +} + +void RendererTest::multipleBlocks() { + auto&& data = MultipleBlocksData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + /* Set padding to zero for easier dummy glyph addition below */ + } glyphCache{PixelFormat::R8Unorm, {16, 16}, {}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) override { + _opened = true; + return {size, 2.0f*size, -1.0f*size, 4.0f*size, 0}; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D& glyphs) override { + for(UnsignedInt& i: glyphs) + i = 0; + } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + + Containers::Pointer doCreateShaper() override { return {}; } + + bool _opened = false; + } font1, font2; + /* Two fonts that do the same but each is opened with a different size */ + font1.openFile("", 1.0f); + font2.openFile("", 2.0f); + for(AbstractFont* font: {&font1, &font2}) { + UnsignedInt fontId = glyphCache.addFont('l' + 1, font); + /* Shuffled order to not have their IDs match the clusters */ + glyphCache.addGlyph(fontId, 'a', {}, {}); /* 1 or 13 */ + glyphCache.addGlyph(fontId, 'c', {}, {}); /* 2 or 14 */ + glyphCache.addGlyph(fontId, 'e', {}, {}); /* 3 or 15 */ + glyphCache.addGlyph(fontId, 'j', {}, {}); /* 4 or 16 */ + glyphCache.addGlyph(fontId, 'b', {}, {}); /* 5 or 17 */ + glyphCache.addGlyph(fontId, 'f', {}, {}); /* 6 or 18 */ + glyphCache.addGlyph(fontId, 'd', {}, {}); /* 7 or 19 */ + glyphCache.addGlyph(fontId, 'g', {}, {}); /* 8 or 20 */ + glyphCache.addGlyph(fontId, 'h', {}, {}); /* 9 or 21 */ + glyphCache.addGlyph(fontId, 'k', {}, {}); /* 10 or 22 */ + glyphCache.addGlyph(fontId, 'i', {}, {}); /* 11 or 23 */ + glyphCache.addGlyph(fontId, 'l', {}, {}); /* 12 or 24 */ + } + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView) override { + _text = text; + _begin = begin; + return end - begin; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + for(UnsignedInt i = 0; i != ids.size(); ++i) + ids[i] = _text[_begin + i]; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + offsets[i] = {}; + advances[i] = Vector2::xAxis(2.0f)*font().size(); + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D& clusters) const override { + for(UnsignedInt i = 0; i != clusters.size(); ++i) + clusters[i] = _begin + i; + } + + ShapeDirection doDirection() const override { + return direction; + } + + ShapeDirection direction; + + private: + Containers::StringView _text; + UnsignedInt _begin; + } shaper1{font1}, shaper2{font2}; + + RendererCore renderer{glyphCache, data.flags}; + + /* Right alignment and custom line advance */ + shaper1.direction = ShapeDirection::RightToLeft; + renderer + .setCursor({50, 100}) + .setAlignment(Alignment::LineBegin) + .setLineAdvance(30.0f); + CORRADE_COMPARE(renderer.render(shaper1, 2.0f, "ab\nc"), + Containers::pair(Range2D{{42.0f, 68.0f}, {50.0f, 104.0f}}, + Range1Dui{0, 1})); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.glyphPositions().size(), 3); + CORRADE_COMPARE(renderer.glyphIds().size(), 3); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 3); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); + CORRADE_COMPARE(renderer.runScales().size(), 1); + CORRADE_COMPARE(renderer.runEnds().size(), 1); + + /* Left alignment and default line advance */ + shaper2.direction = ShapeDirection::RightToLeft; + renderer + .setCursor({-100, 50.0f}) + .setAlignment(Alignment::TopEnd) + .setLineAdvance(0.0f) + .add(shaper2, 4.0f, "de\nfgh\ni", 0, 3) + .add(shaper2, 4.0f, "de\nfgh\ni", 3, 6) + .add(shaper2, 4.0f, "de\nfgh\ni", 6, 8); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 9); + CORRADE_COMPARE(renderer.renderingRunCount(), 4); + CORRADE_COMPARE(renderer.render(), + Containers::pair(Range2D{{-100.0f, 6.0f}, {-76.0f, 50.0f}}, + Range1Dui{1, 4})); + CORRADE_COMPARE(renderer.glyphCount(), 9); + CORRADE_COMPARE(renderer.glyphPositions().size(), 9); + CORRADE_COMPARE(renderer.glyphIds().size(), 9); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 9); + CORRADE_COMPARE(renderer.runCount(), 4); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 9); + CORRADE_COMPARE(renderer.renderingRunCount(), 4); + CORRADE_COMPARE(renderer.runScales().size(), 4); + CORRADE_COMPARE(renderer.runEnds().size(), 4); + + /* Right alignment, custom line advance again */ + shaper1.direction = ShapeDirection::Unspecified; + renderer + .setCursor({0, -50.0f}) + .setAlignment(Alignment::BottomEnd) + .setLineAdvance(10.0f); + CORRADE_COMPARE(renderer.render(shaper1, 1.0f, "j\nkl"), + Containers::pair(Range2D{{-4.0f, -50.0f}, {0.0f, -37.0f}}, + Range1Dui{4, 5})); + CORRADE_COMPARE(renderer.glyphCount(), 12); + CORRADE_COMPARE(renderer.glyphPositions().size(), 12); + CORRADE_COMPARE(renderer.glyphIds().size(), 12); + if(data.flags >= RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE(renderer.glyphClusters().size(), 12); + CORRADE_COMPARE(renderer.runCount(), 5); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 12); + CORRADE_COMPARE(renderer.renderingRunCount(), 5); + CORRADE_COMPARE(renderer.runScales().size(), 5); + CORRADE_COMPARE(renderer.runEnds().size(), 5); + + /* Glyph data of previous blocks shouldn't get corrupted by new renders */ + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + {42.0f, 100.0f}, /* a */ + {46.0f, 100.0f}, /* b */ + {46.0f, 70.0f}, /* c */ + + {-100.0f, 42.0f}, /* d */ + { -92.0f, 42.0f}, /* e */ + {-100.0f, 26.0f}, /* f */ + { -92.0f, 26.0f}, /* g */ + { -84.0f, 26.0f}, /* h */ + {-100.0f, 10.0f}, /* i */ + + {-2.0f, -39.0f}, /* j */ + {-4.0f, -49.0f}, /* k */ + {-2.0f, -49.0f}, /* l */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.glyphIds(), Containers::arrayView({ + /* a b c d e f g h i j k l + first - second --------------- first -- */ + 1, 5, 2, 19, 15, 18, 20, 21, 23, 4, 10, 12 + }), TestSuite::Compare::Container); + if(data.flags & RendererCoreFlag::GlyphClusters) + CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({ + /* a b c d e f g h i j k l */ + 0, 1, 3, 0, 1, 3, 4, 5, 7, 0, 2, 3 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runScales(), Containers::arrayView({ + 2.0f, + 2.0f, + 2.0f, + 2.0f, + 1.0f + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runEnds(), Containers::arrayView({ + 3u, + 5u, + 8u, + 9u, + 12u + }), TestSuite::Compare::Container); +} + +void RendererTest::clearResetCore() { + auto&& data = ClearResetCoreData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) override { + _opened = true; + /* The size is used to scale advances. Ascent & descent is used + just for vertical rect size which isn't needed as we can check + just that the horizontal size got reset. Line height is used to + test that line advance is correctly reset as well. Glyph count + is overriden in addFont() below. */ + return {size, 0.0f, 0.0f, 2.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); + glyphCache.addFont(1, &font); + + 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 { + /* Zero the IDs to not hit an OOB assert in the glyph cache */ + for(UnsignedInt& id: ids) + id = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + advances[i] = {1.0f, 0.0f}; + offsets[i] = {}; + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + ShapeDirection doDirection() const override { + return direction; + } + + ShapeDirection direction = ShapeDirection::Unspecified; + } shaper{font}; + + RendererCore renderer{glyphCache, data.flags}; + + /* Clearing right from the initial state should be a no-op */ + if(data.reset) + renderer.reset(); + else + renderer.clear(); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + + shaper.direction = ShapeDirection::RightToLeft; + renderer + .setAlignment(Alignment::LineEnd) + .setCursor({100.0f, 50.0f}) + .setLineAdvance(30.0f) + .add(shaper, 1.0f, "ab\nc"); + if(data.renderAddOnly) { + CORRADE_VERIFY(renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + } else { + renderer.render(); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.runCount(), 1); + /* Verify initial glyph position values to be sure that the offset + doesn't leak to after clear() */ + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + {100.0f, 50.0f}, + {101.0f, 50.0f}, + {100.0f, 20.0f}, /* On another line with custom advance */ + }), TestSuite::Compare::Container); + /* Similarly, per-run glyph offset shouldn't leak to after clear() */ + CORRADE_COMPARE_AS(renderer.runEnds(), Containers::arrayView({ + 3u + }), TestSuite::Compare::Container); + } + CORRADE_COMPARE(renderer.glyphCapacity(), 3); + CORRADE_COMPARE(renderer.runCapacity(), 1); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); + + /* Clearing should call the allocator with 0, which should then give back + the existing capacity it has, and then reset all in-progress rendering + state. */ + if(data.reset) + renderer.reset(); + else + renderer.clear(); + CORRADE_COMPARE(renderer.glyphCapacity(), data.expectedBuiltinGlyphAllocatorCapacity); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 1); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + + /* Resetting goes back to the initial cursor, alignment and layout + direction */ + if(data.reset) { + CORRADE_COMPARE(renderer.cursor(), Vector2{}); + CORRADE_COMPARE(renderer.alignment(), Alignment::MiddleCenter); + CORRADE_COMPARE(renderer.lineAdvance(), 0.0f); + CORRADE_COMPARE(renderer.layoutDirection(), LayoutDirection::HorizontalTopToBottom); + } else { + CORRADE_COMPARE(renderer.cursor(), (Vector2{100.0f, 50.0f})); + CORRADE_COMPARE(renderer.alignment(), Alignment::LineEnd); + CORRADE_COMPARE(renderer.lineAdvance(), 30.0f); + /** @todo verify with a different value once vertical layout direction + is supported */ + CORRADE_COMPARE(renderer.layoutDirection(), LayoutDirection::HorizontalTopToBottom); + } + + /* Clear the custom line advance if it wasn't reset, to not have it affect + the next. The clear() should have internally cleared the detected one + as well. */ + /** @todo eh, this could theoretically hide a bug where this actually does + reset the detected one as well (and reset() does so as well), silently + keeping the last detected if setLineAdvance() wasn't called at all .. + but I'm being paranoid now */ + if(!data.reset) + renderer.setLineAdvance(0.0f); + + /* Rendering again at a different cursor and alignment shouldn't have the + previous cursor, previous rectangles, resolved alignment, line advance + or run glyph offsets leaking in any way. The three glyphs should now be + at -53, -52, -51 because it's a RTL text aligned to the right. */ + shaper.direction = ShapeDirection::RightToLeft; + renderer + .setAlignment(Alignment::LineBegin) + .setCursor({-50.0f, 100.0f}); + CORRADE_COMPARE(renderer.render(shaper, 1.0f, "a\nbc"), Containers::pair( + Range2D::fromSize({-52.0f, 98.0f}, {2.0f, 2.0f}), Range1Dui{0, 1})); + CORRADE_COMPARE(renderer.glyphCapacity(), 3); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.runCapacity(), 1); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + {-51.0f, 100.0f}, + /* On a new line (advance is negative Y), advance is font's default + {0, 6} */ + {-52.0f, 98.0f}, + {-51.0f, 98.0f}, + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runEnds(), Containers::arrayView({ + 3u + }), TestSuite::Compare::Container); +} + +void RendererTest::clearResetCoreAllocators() { + auto&& data = ClearResetCoreData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Properties doOpenFile(Containers::StringView, Float size) override { + _opened = true; + /* The size is used to scale advances, ascent, descent and line + height is used for vertical alignment which we don't need and + can stay zero. Glyph count is overriden in addFont() below. */ + return {size, 0.0f, 0.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); + glyphCache.addFont(1, &font); + + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; + + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { + /* Zero the IDs to not hit an OOB assert in the glyph cache */ + for(UnsignedInt& id: ids) + id = 0; + } + void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D& offsets, const Containers::StridedArrayView1D& advances) const override { + for(UnsignedInt i = 0; i != offsets.size(); ++i) { + advances[i] = {1.0f, 0.0f}; + offsets[i] = {}; + } + } + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ + } + } shaper{font}; + + struct Allocation { + Vector2 glyphPositions[20]; + UnsignedInt glyphIds[18]; /* deliberately smaller */ + UnsignedInt glyphClusters[20]; + Vector2 glyphAdvances[20]; + + Float runScales[4]; + UnsignedInt runEnds[3]; /* deliberately smaller */ + + UnsignedInt expectedGlyphCount, expectedRunCount; + int glyphCalled = 0, runCalled = 0; + } allocation; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + RendererCore renderer{glyphCache, [](void* state, UnsignedInt glyphCount, Containers::StridedArrayView1D& glyphPositions, Containers::StridedArrayView1D& glyphIds, Containers::StridedArrayView1D* glyphClusters, Containers::StridedArrayView1D& glyphAdvances){ + Allocation& allocation = *static_cast(state); + CORRADE_COMPARE(glyphCount, allocation.expectedGlyphCount); + CORRADE_COMPARE(glyphPositions.size(), 0); + CORRADE_COMPARE(glyphIds.size(), 0); + if(glyphClusters) + CORRADE_COMPARE(glyphClusters->size(), 0); + CORRADE_COMPARE(glyphAdvances.size(), 0); + + glyphPositions = allocation.glyphPositions; + glyphIds = allocation.glyphIds; + if(glyphClusters) + *glyphClusters = allocation.glyphClusters; + glyphAdvances = allocation.glyphAdvances; + ++allocation.glyphCalled; + }, &allocation, [](void* state, UnsignedInt runCount, Containers::StridedArrayView1D& runScales, Containers::StridedArrayView1D& runEnds) { + Allocation& allocation = *static_cast(state); + CORRADE_COMPARE(runCount, allocation.expectedRunCount); + CORRADE_COMPARE(runScales.size(), 0); + CORRADE_COMPARE(runEnds.size(), 0); + + runScales = allocation.runScales; + runEnds = allocation.runEnds; + ++allocation.runCalled; + }, &allocation, data.flags}; + + allocation.expectedGlyphCount = 3; + allocation.expectedRunCount = 1; + renderer.add(shaper, 1.0f, "abc"); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); + if(data.renderAddOnly) { + CORRADE_VERIFY(renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + } else { + renderer.render(); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.runCount(), 1); + } + CORRADE_COMPARE(allocation.glyphCalled, 1); + CORRADE_COMPARE(allocation.runCalled, 1); + /* Minimum of all returned view sizes */ + CORRADE_COMPARE(renderer.glyphCapacity(), 18); + CORRADE_COMPARE(renderer.runCapacity(), 3); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); + + /* Clearing should call the allocator with 0, and then calculate the + capacity the same way as before. The capacity calculation was tested + sufficiently in allocateAllocator() already, and as clear() uses the + same helper internally, we just test a single case of one array being + shorter. */ + allocation.expectedGlyphCount = 0; + allocation.expectedRunCount = 0; + if(data.reset) + renderer.reset(); + else + renderer.clear(); + CORRADE_COMPARE(allocation.glyphCalled, 2); + CORRADE_COMPARE(allocation.runCalled, 2); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + /* Minimum of all returned view sizes */ + CORRADE_COMPARE(renderer.glyphCapacity(), 18); + CORRADE_COMPARE(renderer.runCapacity(), 3); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + + /* Clearing again should not result in anything different, but the + allocators get called again */ + if(data.reset) + renderer.reset(); + else + renderer.clear(); + CORRADE_COMPARE(allocation.glyphCalled, 3); + CORRADE_COMPARE(allocation.runCalled, 3); + /* Minimum of all returned view sizes */ + CORRADE_COMPARE(renderer.glyphCapacity(), 18); + CORRADE_COMPARE(renderer.runCapacity(), 3); + + /* Other resetting behavior is sufficiently tested by clearResetCore() + already */ +} + #ifdef MAGNUM_TARGET_GL void RendererTest::renderData() { auto&& data = RenderDataData[testCaseInstanceId()]; diff --git a/src/Magnum/Text/Text.h b/src/Magnum/Text/Text.h index f8aa8c217..d2b7d76af 100644 --- a/src/Magnum/Text/Text.h +++ b/src/Magnum/Text/Text.h @@ -56,6 +56,8 @@ enum class Script: UnsignedInt; class FeatureRange; +class RendererCore; + #ifdef MAGNUM_TARGET_GL class DistanceFieldGlyphCacheGL; class GlyphCacheGL;