diff --git a/src/Magnum/Text/Implementation/rendererState.h b/src/Magnum/Text/Implementation/rendererState.h index e831ee7d4..ff04d5e5e 100644 --- a/src/Magnum/Text/Implementation/rendererState.h +++ b/src/Magnum/Text/Implementation/rendererState.h @@ -32,13 +32,15 @@ #include #include +#include "Magnum/Mesh.h" #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 */ +/* Is inherited by RendererCore::AllocatorState, Renderer::State and then + RendererGL::State to avoid extra allocations for each class' state */ struct RendererCore::State { /* Gets called by RendererCore only if both allocators are specified by the user. If not, AllocatorState is constructed instead. */ @@ -118,6 +120,49 @@ struct RendererCore::AllocatorState: RendererCore::State { Containers::Array runData; }; +/** @todo this includes the glyphData + runData (+ indexData, vertexData) + members even when they're unused because custom allocators are used, have + some templated RendererCore::AllocatorState that inherits either the + RendererCore::State or Renderer::State and adds one or more of those based + on what all builtin allocators are used? or am I overdoing it for measly 96 + byte savings? */ +struct Renderer::State: RendererCore::AllocatorState { + /* Defined in Renderer.cpp because it needs access to default allocator + implementations */ + 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, void(*indexAllocator)(void*, UnsignedInt, Containers::ArrayView&), void* indexAllocatorState, void(*vertexAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* vertexAllocatorState, RendererFlags flags); + + void(*const indexAllocator)(void*, UnsignedInt, Containers::ArrayView&); + void* const indexAllocatorState; + void(*const vertexAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&); + void* const vertexAllocatorState; + + MeshIndexType minIndexType = MeshIndexType::UnsignedByte; + MeshIndexType indexType = MeshIndexType::UnsignedByte; + Containers::ArrayView indices; + Containers::StridedArrayView1D vertexPositions; + /* If using an array glyph cache, it can be cast to Vector3 */ + Containers::StridedArrayView1D vertexTextureCoordinates; + + /* Used only if the builtin vertex allocator is used */ + Containers::Array indexData; + Containers::Array vertexData; +}; + +namespace Implementation { + +/* Not used in the state structs above but needed by Renderer */ +struct Vertex { + Vector2 position; + Vector2 textureCoordinates; +}; + +struct VertexArray { + Vector2 position; + Vector3 textureCoordinates; +}; + +} + }} #endif diff --git a/src/Magnum/Text/Renderer.cpp b/src/Magnum/Text/Renderer.cpp index 0308ea523..8c533284e 100644 --- a/src/Magnum/Text/Renderer.cpp +++ b/src/Magnum/Text/Renderer.cpp @@ -62,7 +62,6 @@ #include /** @todo remove once Renderer is STL-free */ #include /** @todo remove once Renderer is STL-free */ -#include "Magnum/Mesh.h" #include "Magnum/GL/Context.h" #include "Magnum/GL/Extensions.h" #include "Magnum/GL/Mesh.h" @@ -201,6 +200,8 @@ RendererCore::RendererCore(const AbstractGlyphCache& glyphCache, void(*glyphAllo Containers::pointer(glyphCache, glyphAllocator, glyphAllocatorState, runAllocator, runAllocatorState, flags) : Containers::pointer(glyphCache, glyphAllocator, glyphAllocatorState, runAllocator, runAllocatorState, flags)} {} +RendererCore::RendererCore(Containers::Pointer&& state): _state{Utility::move(state)} {} + RendererCore::RendererCore(NoCreateT) noexcept {} RendererCore::RendererCore(RendererCore&&) noexcept = default; @@ -214,7 +215,8 @@ const AbstractGlyphCache& RendererCore::glyphCache() const { } RendererCoreFlags RendererCore::flags() const { - return _state->flags; + /* Subclasses inherit and add their own flags, mask them away */ + return _state->flags & RendererCoreFlags{0x1}; } UnsignedInt RendererCore::glyphCount() const { @@ -504,7 +506,8 @@ void RendererCore::resetInternal() { RendererCore& RendererCore::reset() { clear(); - /* Reset also all other settable state to defaults */ + /* Reset also all other settable state to defaults. Is in a separate helper + because it gets called from Renderer::reset() as well. */ resetInternal(); return *this; @@ -772,6 +775,589 @@ Containers::Pair RendererCore::render(AbstractShaper& shaper return render(shaper, size, text, Containers::arrayView(features)); } +Debug& operator<<(Debug& debug, const RendererFlag value) { + debug << "Text::RendererFlag" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case RendererFlag::v: return debug << "::" #v; + _c(GlyphPositionsClusters) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << Debug::hex << UnsignedByte(value) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const RendererFlags value) { + return Containers::enumSetDebugOutput(debug, value, "Text::RendererFlags{}", { + RendererFlag::GlyphPositionsClusters + }); +} + +namespace { + +template auto defaultGlyphAllocatorFor(const RendererFlags flags, const bool hasCustomVertexAllocator) -> void(*)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&) { + /* If glyph positions and clusters are meant to be preserved, or if a + custom vertex allocator is used and thus shouldn't allocate the whole + vertex data again just to store glyph data inside, use the default + RendererCore allocator */ + /** @todo it will still result in IDs being allocated and then never used + after, provide a custom allocator for this case as well */ + if(flags >= RendererFlag::GlyphPositionsClusters || hasCustomVertexAllocator) + return nullptr; + + return [](void* const state, const UnsignedInt glyphCount, Containers::StridedArrayView1D& glyphPositions, Containers::StridedArrayView1D& glyphIds, Containers::StridedArrayView1D*, Containers::StridedArrayView1D& glyphAdvances) { + Containers::Array& vertexData = *static_cast*>(state); + + const std::size_t existingSize = glyphPositions.size(); + const std::size_t desiredByteSize = 4*(existingSize + glyphCount)*sizeof(Vertex); + if(desiredByteSize > vertexData.size()) { + /* Using arrayAppend() as it reallocates with a growth strategy, + arrayResize() would take the size literally */ + arrayAppend(vertexData, NoInit, desiredByteSize - vertexData.size()); + } + + const Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + /* As each glyph turns into four vertices, we have plenty of space to + store everything. Glyph positions occupy the position of each first + vertex, */ + glyphPositions = vertices.slice(&Vertex::position).every(4); + /* glyph IDs the first four bytes of the texture coordinates of each + first vertex, */ + glyphIds = Containers::arrayCast(vertices.slice(&Vertex::textureCoordinates)).every(4); + /* and advances the position of each *second* vertex from the + yet-unused suffix. If we have no vertex data at all however, which + can happen when calling clear() right after construction, don't + slice away any prefix to avoid OOB access. */ + glyphAdvances = vertices.slice(&Vertex::position).exceptPrefix( + vertexData.size() ? existingSize*4 + 1 : 0 + ).every(4); + }; +} + +void defaultIndexAllocator(void* state, UnsignedInt size, Containers::ArrayView& indices) { + Containers::Array& indexData = *static_cast*>(state); + + const std::size_t desiredByteSize = indices.size() + size; + if(desiredByteSize > indexData.size()) { + /* Using arrayAppend() as it reallocates with a growth strategy, + arrayResize() would take the size literally */ + arrayAppend(indexData, NoInit, desiredByteSize - indexData.size()); + } + + indices = indexData; +} + +template auto defaultVertexAllocatorFor(const RendererFlags flags, const bool hasCustomGlyphAllocator) -> void(*)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&) { + /* If glyph positions and clusters are meant to be preserved, or if a + custom glyph allocator is used so there's no data sharing between the + two, vertices are in a separate allocation. The second branch part + is explicitly verified in the indicesVertices(custom glyph allocator) + test. */ + if(flags >= RendererFlag::GlyphPositionsClusters || hasCustomGlyphAllocator) + return [](void* const state, const UnsignedInt vertexCount, Containers::StridedArrayView1D& vertexPositions, Containers::StridedArrayView1D& vertexTextureCoordinates) { + Containers::Array& vertexData = *static_cast*>(state); + + const std::size_t desiredByteSize = (vertexPositions.size() + vertexCount)*sizeof(Vertex); + if(desiredByteSize > vertexData.size()) { + /* Using arrayAppend() as it reallocates with a growth + strategy, arrayResize() would take the size literally */ + arrayAppend(vertexData, NoInit, desiredByteSize - vertexData.size()); + } + + const Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + vertexPositions = vertices.slice(&Vertex::position); + /* The texture coordinates are Vector3 for array glyph caches, the + allocator wants just a two-component prefix with an assumption + that the third component is there too. Can't use + .slice(&Vector3::xy) because the type may be Vector2. */ + vertexTextureCoordinates = Containers::arrayCast(vertices.slice(&Vertex::textureCoordinates)); + }; + /* If not, vertices share the allocation with glyph properties, and since + they're always allocated after, the size should be sufficient and it's + just about redirecting the views to new memory */ + else + return [](void* const state, const UnsignedInt + #ifndef CORRADE_NO_ASSERT + vertexCount + #endif + , Containers::StridedArrayView1D& vertexPositions, Containers::StridedArrayView1D& vertexTextureCoordinates) + { + Containers::Array& vertexData = *static_cast*>(state); + + /* As both the glyph allocator and vertex allocator share the same + array, the assumption is that the glyph allocator already + enlarged the array for all needed glyphs. Or this allocator is + called from clear() with zero vertex count, in which case the + array size can be whatever. */ + CORRADE_INTERNAL_ASSERT((vertexPositions.size() + vertexCount)*sizeof(Vertex) == vertexData.size() || vertexCount == 0); + + const Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + vertexPositions = vertices.slice(&Vertex::position); + /* The texture coordinates are Vector3 for array glyph caches, the + allocator wants just a two-component prefix with an assumption + that the third component is there too. Can't use + .slice(&Vector3::xy) because the type may be Vector2. */ + vertexTextureCoordinates = Containers::arrayCast(vertices.slice(&Vertex::textureCoordinates)); + }; +} + +} + +Renderer::State::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, void(*indexAllocator)(void*, UnsignedInt, Containers::ArrayView&), void* indexAllocatorState, void(*vertexAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* vertexAllocatorState, RendererFlags flags): + RendererCore::AllocatorState{glyphCache, + glyphAllocator ? glyphAllocator : + glyphCache.size().z() == 1 ? + defaultGlyphAllocatorFor(flags, !!vertexAllocator) : + defaultGlyphAllocatorFor(flags, !!vertexAllocator), + /* The defaultGlyphAllocatorFor() puts glyph data into the same + allocation as vertex data so it's `&vertexData`, not `&glyphData` + here. If such sharing isn't desired because the glyph data need to + be accessible etc., defaultGlyphAllocatorFor() returns nullptr, + which then causes `&vertexData` to be ignored and RendererCore then + picks its own default allocator and `&glyphData`. */ + glyphAllocator ? glyphAllocatorState : &vertexData, + runAllocator, runAllocatorState, + RendererCoreFlags{UnsignedByte(flags)}}, + indexAllocator{indexAllocator ? indexAllocator : defaultIndexAllocator}, + indexAllocatorState{indexAllocator ? indexAllocatorState : &indexData}, + vertexAllocator{vertexAllocator ? vertexAllocator : + glyphCache.size().z() == 1 ? + defaultVertexAllocatorFor(flags, !!glyphAllocator) : + defaultVertexAllocatorFor(flags, !!glyphAllocator)}, + vertexAllocatorState{vertexAllocator ? vertexAllocatorState : &vertexData} {} + +Renderer::Renderer(const AbstractGlyphCache& glyphCache, void(*glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&), void* glyphAllocatorState, void(*runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* runAllocatorState, void(*indexAllocator)(void*, UnsignedInt, Containers::ArrayView&), void* indexAllocatorState, void(*vertexAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&), void* vertexAllocatorState, RendererFlags flags): RendererCore{Containers::pointer(glyphCache, glyphAllocator, glyphAllocatorState, runAllocator, runAllocatorState, indexAllocator, indexAllocatorState, vertexAllocator, vertexAllocatorState, flags)} {} + +Renderer::Renderer(Renderer&&) noexcept = default; + +Renderer::~Renderer() = default; + +Renderer& Renderer::operator=(Renderer&&) noexcept = default; + +RendererFlags Renderer::flags() const { + return RendererFlags{UnsignedByte(_state->flags)}; +} + +namespace { + /* Like meshIndexTypeSize() but inline, constexpr, branchless and without + assertions */ + constexpr UnsignedInt indexTypeSize(MeshIndexType type) { + return 1 << (int(type) - 1); + } + static_assert( + indexTypeSize(MeshIndexType::UnsignedByte) == sizeof(UnsignedByte) && + indexTypeSize(MeshIndexType::UnsignedShort) == sizeof(UnsignedShort) && + indexTypeSize(MeshIndexType::UnsignedInt) == sizeof(UnsignedInt), + "broken assumptions about MeshIndexType values matching type sizes"); +} + +UnsignedInt Renderer::glyphIndexCapacity() const { + const State& state = static_cast(*_state); + CORRADE_INTERNAL_DEBUG_ASSERT(state.indices.size() % 6 == 0); + return state.indices.size()/(6*indexTypeSize(state.indexType)); +} + +UnsignedInt Renderer::glyphVertexCapacity() const { + const State& state = static_cast(*_state); + CORRADE_INTERNAL_DEBUG_ASSERT(state.vertexPositions.size() % 4 == 0); + return state.vertexPositions.size()/4; +} + +MeshIndexType Renderer::indexType() const { + return static_cast(*_state).indexType; +} + +namespace { + /* used by setIndexType() and allocateIndices() */ + MeshIndexType indexTypeFor(MeshIndexType minType, UnsignedInt glyphCount) { + MeshIndexType minTypeForGlyphCount; + if(glyphCount > 16384) + minTypeForGlyphCount = MeshIndexType::UnsignedInt; + else if(glyphCount > 64) + minTypeForGlyphCount = MeshIndexType::UnsignedShort; + else + minTypeForGlyphCount = MeshIndexType::UnsignedByte; + return Utility::max(minType, minTypeForGlyphCount); + } +} + +Renderer& Renderer::setIndexType(const MeshIndexType type) { + State& state = static_cast(*_state); + CORRADE_ASSERT(!state.rendering, + "Text::Renderer::setIndexType(): rendering in progress", *this); + + /* Remember the type as the smallest index type we can use going forward */ + state.minIndexType = type; + + /* If the capacity is zero, just update the currently used index type + without calling an allocator */ + if(state.glyphPositions.isEmpty()) { + state.indexType = type; + + /* Otherwise, if the index type for current capacity is now different from + what's currently used, reallocate the indices fully */ + } else if(indexTypeFor(type, state.glyphPositions.size()) != state.indexType) { + /* In particular, the allocator gets a zero-sized prefix of the view + it returned last time (*not* just nullptr), to hint that it can + reallocate without preserving any contents at all */ + state.indices = state.indices.prefix(0); + allocateIndices( + #ifndef CORRADE_NO_ASSERT + "Text::Renderer::setIndexType():", + #endif + state.glyphPositions.size() + ); + } + + return *this; +} + +Containers::StridedArrayView1D Renderer::glyphPositions() const { + const State& state = static_cast(*_state); + CORRADE_ASSERT(RendererFlags(UnsignedByte(state.flags)) >= RendererFlag::GlyphPositionsClusters, + "Text::Renderer::glyphPositions(): glyph positions and clusters not enabled", {}); + return state.glyphPositions.prefix(state.glyphCount); +} + +Containers::StridedArrayView1D Renderer::glyphClusters() const { + const State& state = static_cast(*_state); + CORRADE_ASSERT(RendererFlags(UnsignedByte(state.flags)) >= RendererFlag::GlyphPositionsClusters, + "Text::Renderer::glyphClusters(): glyph positions and clusters not enabled", {}); + return state.glyphClusters.prefix(state.glyphCount); +} + +Containers::StridedArrayView2D Renderer::indices() const { + const State& state = static_cast(*_state); + const UnsignedInt typeSize = indexTypeSize(state.indexType); + return stridedArrayView(state.indices.prefix(state.glyphCount*6*typeSize)).expanded<0, 2>({state.glyphCount*6, typeSize}); +} + +/* On Windows (MSVC, clang-cl and MinGw) these need an explicit export + otherwise the specializations don't get exported */ +template<> MAGNUM_TEXT_EXPORT Containers::ArrayView Renderer::indices() const { + const State& state = static_cast(*_state); + CORRADE_ASSERT(state.indexType == MeshIndexType::UnsignedByte, + "Text::Renderer::indices(): cannot retrieve" << state.indexType << "as an UnsignedByte", {}); + return Containers::arrayCast(state.indices).prefix(state.glyphCount*6); +} + +template<> MAGNUM_TEXT_EXPORT Containers::ArrayView Renderer::indices() const { + const State& state = static_cast(*_state); + CORRADE_ASSERT(state.indexType == MeshIndexType::UnsignedShort, + "Text::Renderer::indices(): cannot retrieve" << state.indexType << "as an UnsignedShort", {}); + return Containers::arrayCast(state.indices).prefix(state.glyphCount*6); +} + +template<> MAGNUM_TEXT_EXPORT Containers::ArrayView Renderer::indices() const { + const State& state = static_cast(*_state); + CORRADE_ASSERT(state.indexType == MeshIndexType::UnsignedInt, + "Text::Renderer::indices(): cannot retrieve" << state.indexType << "as an UnsignedInt", {}); + return Containers::arrayCast(state.indices).prefix(state.glyphCount*6); +} + +Containers::StridedArrayView1D Renderer::vertexPositions() const { + const State& state = static_cast(*_state); + return state.vertexPositions.prefix(state.glyphCount*4); +} + +Containers::StridedArrayView1D Renderer::vertexTextureCoordinates() const { + const State& state = static_cast(*_state); + CORRADE_ASSERT(state.glyphCache.size().z() == 1, + "Text::Renderer::vertexTextureCoordinates(): cannot retrieve two-dimensional coordinates with an array glyph cache", {}); + return state.vertexTextureCoordinates.prefix(state.glyphCount*4); +} + +Containers::StridedArrayView1D Renderer::vertexTextureArrayCoordinates() const { + const State& state = static_cast(*_state); + CORRADE_ASSERT(state.glyphCache.size().z() != 1, + "Text::Renderer::vertexTextureArrayCoordinates(): cannot retrieve three-dimensional coordinates with a non-array glyph cache", {}); + return Containers::arrayCast(state.vertexTextureCoordinates.prefix(state.glyphCount*4)); +} + +void Renderer::allocateIndices( + #ifndef CORRADE_NO_ASSERT + const char* const messagePrefix, + #endif + const UnsignedInt totalGlyphCount) +{ + State& state = static_cast(*_state); + + /* The data allocated by RendererCore should already be at this size or + more, since allocateGlyphs() is always called before this function. */ + CORRADE_INTERNAL_ASSERT(state.glyphPositions.size() >= totalGlyphCount); + + /* This function should only be called if we need more memory, from clear() + with everything empty or from setIndexType() if the type changes (where + it sets `state.indices` to an empty prefix). + + The expectation is that `state.indices` is only as large as makes sense + for given `state.indexType`, as is done below. */ + CORRADE_INTERNAL_DEBUG_ASSERT(6*totalGlyphCount*indexTypeSize(state.indexType) > state.indices.size() || (state.glyphCount == 0 && state.renderingGlyphCount == 0 && totalGlyphCount == 0)); + + /* Figure out index type needed for this glyph count. If it's different or + we're called from clear() with totalGlyphCount being 0, we're replacing + the whole index array. If it's not, we're generating just the extra + indices. */ + const MeshIndexType indexType = indexTypeFor(state.minIndexType, totalGlyphCount); + const UnsignedInt typeSize = indexTypeSize(indexType); + UnsignedInt previousFilledSize; + if(indexType != state.indexType || totalGlyphCount == 0) { + previousFilledSize = 0; + state.indexType = indexType; + } else { + previousFilledSize = state.indices.size(); + } + + /* Sliced copy of the view for the allocator to update */ + Containers::ArrayView indices = state.indices.prefix(previousFilledSize); + + /* While this function gets total glyph count, the allocator gets byte + count to grow by */ + state.indexAllocator(state.indexAllocatorState, + totalGlyphCount*6*typeSize - previousFilledSize, + indices); + + /* Cap the returned capacity to just what's possible to represent with + given type size. E.g., for an 8-bit type it can represent indices only + for 256 vertices / 64 glyphs at most, which is 384 indices, thus is + never larger than 384 bytes. */ + const UnsignedInt glyphCapacity = Math::min( + /* 64 for 1-byte indices, 16k for 2-byte, 1M for 4-byte */ + 1u << (8*typeSize - 2), + UnsignedInt(indices.size()/(6*typeSize))); + + /* 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(glyphCapacity >= totalGlyphCount, + messagePrefix << "expected allocated indices to have at least" << totalGlyphCount*6*typeSize << "bytes but got" << indices.size(), ); + + state.indices = indices.prefix(glyphCapacity*6*typeSize); + + /* Fill the indices during allocation already as they're not dependent on + the contents in any way */ + const UnsignedInt glyphOffset = previousFilledSize/(6*typeSize); + const Containers::ArrayView indicesToFill = state.indices.exceptPrefix(previousFilledSize); + if(indexType == MeshIndexType::UnsignedByte) + renderGlyphQuadIndicesInto(glyphOffset, Containers::arrayCast(indicesToFill)); + else if(indexType == MeshIndexType::UnsignedShort) + renderGlyphQuadIndicesInto(glyphOffset, Containers::arrayCast(indicesToFill)); + else if(indexType == MeshIndexType::UnsignedInt) + renderGlyphQuadIndicesInto(glyphOffset, Containers::arrayCast(indicesToFill)); + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +void Renderer::allocateVertices( + #ifndef CORRADE_NO_ASSERT + const char* const messagePrefix, + #endif + const UnsignedInt totalGlyphCount) +{ + State& state = static_cast(*_state); + + /* The data allocated by RendererCore should already be at this size or + more, since allocateGlyphs() is always called before this function */ + CORRADE_INTERNAL_ASSERT(state.glyphPositions.size() >= totalGlyphCount); + + /* This function should only be called if we need more memory or from + clear() with everything empty */ + CORRADE_INTERNAL_DEBUG_ASSERT(4*totalGlyphCount > state.vertexPositions.size() || (state.glyphCount == 0 && totalGlyphCount == 0)); + + /* Sliced copies of the views for the allocator to update. Unlike with + allocateGlyphs(), where `state.renderingGlyphCount` is used because it + gets called from add(), this is called with `state.glyphCount` because + it's only called from render(), and so the vertex capacity may not yet + include space for the in-progress glyphs. */ + Containers::StridedArrayView1D vertexPositions = + state.vertexPositions.prefix(state.glyphCount*4); + Containers::StridedArrayView1D vertexTextureCoordinates = + state.vertexTextureCoordinates.prefix(state.glyphCount*4); + + /* While this function gets total glyph count, the allocator gets vertex + count to grow by instead */ + state.vertexAllocator(state.vertexAllocatorState, (totalGlyphCount - state.glyphCount)*4, + vertexPositions, + vertexTextureCoordinates); + /* Take the smallest size of both as the new vertex capacity */ + const std::size_t minGlyphCapacity = Math::min({ + vertexPositions.size()/4, + vertexTextureCoordinates.size()/4}); + /* 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(minGlyphCapacity >= totalGlyphCount, + messagePrefix << "expected allocated vertex positions and texture coordinates to have at least" << totalGlyphCount*4 << "elements but got" << vertexPositions.size() << "and" << vertexTextureCoordinates.size(), ); + CORRADE_ASSERT(state.glyphCache.size().z() == 1 || std::size_t(Math::abs(vertexTextureCoordinates.stride())) >= sizeof(Vector3), + messagePrefix << "expected allocated texture coordinates to have a stride large enough to fit a Vector3 but got only" << Math::abs(vertexTextureCoordinates.stride()) << "bytes", ); + + /* Keep just the minimal size for both, which is the new capacity */ + state.vertexPositions = vertexPositions.prefix(minGlyphCapacity *4); + state.vertexTextureCoordinates = vertexTextureCoordinates.prefix(minGlyphCapacity *4); +} + +Renderer& Renderer::clear() { + RendererCore::clear(); + + /* Not calling allocateIndices() with 0 because it makes no sense to + regenerate the index buffer to the exact same contents on every clear */ + allocateVertices( + #ifndef CORRADE_NO_ASSERT + "", /* Asserts won't happen as returned sizes will be always >= 0 */ + #endif + 0); + + return *this; +} + +Renderer& Renderer::reset() { + /* Compared to RendererCore::reset() this calls our clear() instead of + RendererCore::clear() */ + clear(); + resetInternal(); + return *this; +} + +Renderer& Renderer::reserve(const UnsignedInt glyphCapacity, const UnsignedInt runCapacity) { + State& state = static_cast(*_state); + + /* Reserve glyph and run capacity. It's possible that there's already + enough glyph/run capacity but the index/vertex capacity not yet because + glyphs/runs get allocated during add() already and index/vertex only + during the final render(). */ + RendererCore::reserve(glyphCapacity, runCapacity); + + /* Reserve (and fill) indices if there's too little of them for the + required glyph capacity. Done separately from vertex allocation because + each of the allocations can have a different growth pattern and the + index type can change during the renderer lifetime. + + The expectation is that `state.indices` is only as large as makes sense + for given `state.indexType` (e.g., for an 8-bit type it can represent + indices only for 256 vertices / 64 glyphs at most, which is 384 indices, + thus is never larger than 384 bytes). */ + if(state.indices.size() < glyphCapacity*6*indexTypeSize(state.indexType)) + allocateIndices( + #ifndef CORRADE_NO_ASSERT + "Text::Renderer::reserve():", + #endif + glyphCapacity); + + /* Reserve vertices if there's too little of them for the required glyph + capacity */ + if(state.vertexPositions.size() < glyphCapacity*4) + allocateVertices( + #ifndef CORRADE_NO_ASSERT + "Text::Renderer::reserve():", + #endif + glyphCapacity); + + return *this; +} + +Containers::Pair Renderer::render() { + State& state = static_cast(*_state); + + /* If we need to generate more indices / vertices than what's in the + capacity, allocate more. The logic is the same as in reserve(), see + there for more information. + + This has to be called before RendererCore::render() in order to know + which glyphs have only positions + IDs (state.renderingGlyphCount) and + which have also index and vertex data (state.glyphCount). The + RendererCore::render() then makes both values the same. */ + if(state.indices.size() < state.renderingGlyphCount *6*indexTypeSize(state.indexType)) + allocateIndices( + #ifndef CORRADE_NO_ASSERT + "Text::Renderer::render():", + #endif + state.renderingGlyphCount); + if(state.vertexPositions.size() < state.renderingGlyphCount *4) + allocateVertices( + #ifndef CORRADE_NO_ASSERT + "Text::Renderer::render():", + #endif + state.renderingGlyphCount); + #ifdef CORRADE_GRACEFUL_ASSERT + /* For testing only -- if vertex allocation failed, bail. Indices are only + touched in allocateIndices(), so if allocateIndices() fails we don't + need to exit here. */ + if(state.vertexPositions.size() < state.renderingGlyphCount *4) + return {}; + #endif + + /* Finish rendering of glyph positions and IDs */ + const bool isArray = state.glyphCache.size().z() > 1; + const Containers::Pair out = RendererCore::render(); + + /* Populate vertex data for all runs */ + UnsignedInt glyphBegin = out.second().min() ? state.runEnds[out.second().min() - 1] : 0; + for(UnsignedInt run = out.second().min(), runEnd = out.second().max(); run != runEnd; ++run) { + const UnsignedInt glyphEnd = state.runEnds[run]; + + const Containers::StridedArrayView1D glyphPositions = state.glyphPositions.slice(glyphBegin, glyphEnd); + const Containers::StridedArrayView1D glyphIds = state.glyphIds.slice(glyphBegin, glyphEnd); + const Containers::StridedArrayView1D vertexPositions = state.vertexPositions.slice(4*glyphBegin, 4*glyphEnd); + const Containers::StridedArrayView1D vertexTextureCoordinates = state.vertexTextureCoordinates.slice(4*glyphBegin, 4*glyphEnd); + if(!isArray) renderGlyphQuadsInto(state.glyphCache, + state.runScales[run], + glyphPositions, + glyphIds, + vertexPositions, + vertexTextureCoordinates); + else renderGlyphQuadsInto(state.glyphCache, + state.runScales[run], + glyphPositions, + glyphIds, + vertexPositions, + Containers::arrayCast(vertexTextureCoordinates)); + + glyphBegin = glyphEnd; + } + + return out; +} + +Renderer& Renderer::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const Containers::ArrayView features) { + return static_cast(RendererCore::add(shaper, size, text, begin, end, features)); +} + +Renderer& Renderer::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end) { + return static_cast(RendererCore::add(shaper, size, text, begin, end)); +} + +Renderer& Renderer::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const std::initializer_list features) { + return static_cast(RendererCore::add(shaper, size, text, begin, end, features)); +} + +Renderer& Renderer::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const Containers::ArrayView features) { + return static_cast(RendererCore::add(shaper, size, text, features)); +} + +Renderer& Renderer::add(AbstractShaper& shaper, const Float size, const Containers::StringView text) { + return static_cast(RendererCore::add(shaper, size, text)); +} + +Renderer& Renderer::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const std::initializer_list features) { + return static_cast(RendererCore::add(shaper, size, text, features)); +} + +Containers::Pair Renderer::render(AbstractShaper& shaper, const Float size, const Containers::StringView text, const Containers::ArrayView features) { + /* Compared to RendererCore::render() this calls our render() instead of + RendererCore::render() */ + add(shaper, size, text, features); + return render(); +} + +Containers::Pair Renderer::render(AbstractShaper& shaper, const Float size, const Containers::StringView text) { + return render(shaper, size, text, {}); +} + +Containers::Pair Renderer::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 e10e62e84..21b60eb92 100644 --- a/src/Magnum/Text/Renderer.h +++ b/src/Magnum/Text/Renderer.h @@ -27,7 +27,7 @@ */ /** @file - * @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() + * @brief Class @ref Magnum::Text::RendererCore, @ref Magnum::Text::Renderer, @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 @@ -67,6 +67,9 @@ enum class RendererCoreFlag: UnsignedByte { * text selection and editing purposes. */ GlyphClusters = 1 << 0, + + /* Additions to this enum have to be propagated to RendererFlag and the + mask in RendererCore::flag() */ }; /** @@ -415,9 +418,13 @@ class MAGNUM_TEXT_EXPORT RendererCore { * 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(). + * @ref glyphIds() and @ref glyphClusters() views; when multipled by + * @cpp 6 @ce to index the @ref Renderer::indices() view and when + * multiplied by @cpp 4 @ce to index the @ref Renderer::vertexPositions() + * and @relativeref{Renderer,vertexTextureCoordinates()} / + * @relativeref{Renderer,vertexTextureArrayCoordinates()} 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 @@ -554,7 +561,11 @@ class MAGNUM_TEXT_EXPORT RendererCore { * @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. + * views; when multipled by @cpp 6 @ce to index the + * @ref Renderer::indices() view and when multiplied by @cpp 4 @ce to + * index the @ref Renderer::vertexPositions() and + * @relativeref{Renderer,vertexTextureCoordinates()} / + * @relativeref{Renderer,vertexTextureArrayCoordinates()} views. * * The rendered glyph range is not touched or used by the renderer in * any way afterwards. If the renderer was created with custom @@ -600,7 +611,10 @@ class MAGNUM_TEXT_EXPORT RendererCore { struct AllocatorState; Containers::Pointer _state; - /* Called by reset() */ + /* Delegated to by Renderer constructors */ + explicit MAGNUM_TEXT_LOCAL RendererCore(Containers::Pointer&& state); + + /* Called by reset() and Renderer::reset() */ MAGNUM_TEXT_LOCAL void resetInternal(); private: @@ -619,6 +633,450 @@ class MAGNUM_TEXT_EXPORT RendererCore { MAGNUM_TEXT_LOCAL void alignAndFinishLine(); }; +/** +@brief Text renderer flag +@m_since_latest + +A superset of @ref RendererCoreFlag. +@see @ref RendererFlags, @ref Renderer +*/ +enum class RendererFlag: UnsignedByte { + /** + * Populate glyph cluster info in @ref Renderer::glyphPositions() and + * @ref Renderer::glyphClusters() for text selection and editing purposes. + * + * Compared to @ref RendererCore and @ref RendererCoreFlag::GlyphClusters, + * the @ref Renderer by default queries glyph positions to a temporary + * location that's later overwritten by quad vertex positions to save + * memory so this flag includes both clusters and positions. + */ + GlyphPositionsClusters = Int(RendererCoreFlag::GlyphClusters) +}; + +/** + * @debugoperatorenum{RendererFlag} + * @m_since_latest + */ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, RendererFlag value); + +/** +@brief Text renderer flags +@m_since_latest + +A superset of @ref RendererCoreFlags. +@see @ref Renderer +*/ +typedef Containers::EnumSet RendererFlags; + +CORRADE_ENUMSET_OPERATORS(RendererFlags) + +/** + * @debugoperatorenum{RendererFlags} + * @m_since_latest + */ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, RendererFlags value); + +/** +@brief Text renderer +@m_since_latest +*/ +class MAGNUM_TEXT_EXPORT Renderer: public RendererCore { + public: + /** + * @brief Construct + * @param glyphCache Glyph cache to use + * @param flags Opt-in feature flags + * + * By default, the renderer allocates the memory for glyph, run, index + * and vertex data internally. Use the overload below to supply + * external allocators. + * @todoc the damn thing can't link to functions taking functions + */ + explicit Renderer(const AbstractGlyphCache& glyphCache, RendererFlags flags = {}): Renderer{glyphCache, nullptr, nullptr, nullptr, nullptr, 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 indexAllocator Index allocator function or @cpp nullptr @ce + * @param indexAllocatorState State pointer to pass to @p indexAllocator + * @param vertexAllocator Vertex allocator function or @cpp nullptr @ce + * @param vertexAllocatorState State pointer to pass to @p vertexAllocator + * @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 RendererFlag::GlyphPositionsClusters, 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 @p indexAllocator gets called with desired @p size every time + * @ref glyphCapacity() increases. Size of passed-in @p indices array + * either matches @ref glyphCapacity() times @cpp 6 @ce times size of + * @ref indexType() if the index type stays the same, or is empty if + * the index type changes (and the whole index array is going to + * get rebuilt with a different type, thus no contents need to be + * preserved). The allocator is expected to replace the passed view + * with a new view that's larger by *at least* @p size, pointing to a + * reallocated memory with contents from the original view preserved. + * Initially @ref glyphCapacity() is @cpp 0 @ce and the view is passed + * in empty, every subsequent time the view matches a prefix of the + * view previously returned by the allocator. + * + * The @p vertexAllocator gets called with @p vertexCount every time + * @ref glyphCount() reaches @ref glyphCapacity(). Size of passed-in + * @p vertexPositions and @p vertexTextureCoordinates views matches + * @ref glyphCount() times @cpp 4 @ce. The allocator is expected to + * replace the views with new views that are larger by *at least* + * @p vertexCount, pointing to a reallocated memory with contents from + * the original views preserved. Initially @ref glyphCount() 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. If the @p glyphCache is an array, the allocator is + * expected to (re)allocate @p vertexTextureCoordinates for a + * @relativeref{Magnum,Vector3} type even though the view points to + * just the first two components of each texture coordinates. + * + * The renderer always requests only exactly the desired size and the + * growth strategy is up to the allocators themselves --- the returned + * glyph and run 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 glyphIndexCapacity(), @ref glyphVertexCapacity() and + * @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 / @p size / @p vertexCount being @cpp 0 @ce. This is to + * allow the allocators to perform any needed reset as well. + * + * If @p glyphAllocator, @p runAllocator, @p indexAllocator or + * @p vertexAllocator is @cpp nullptr @ce, @p glyphAllocatorState, + * @p runAllocatorState, @p indexAllocatorState or + * @p vertexAllocatorState is ignored and default builtin allocator get + * used for either. Passing @cpp nullptr @ce for all is equivalent to + * calling the @ref Renderer(const AbstractGlyphCache&, RendererFlags) + * constructor. + */ + explicit Renderer(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, void(*indexAllocator)(void* state, UnsignedInt size, Containers::ArrayView& indices), void* indexAllocatorState, void(*vertexAllocator)(void* state, UnsignedInt vertexCount, Containers::StridedArrayView1D& vertexPositions, Containers::StridedArrayView1D& vertexTextureCoordinates), void* vertexAllocatorState, RendererFlags 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 Renderer(NoCreateT) noexcept: RendererCore{NoCreate} {} + + /** @brief Copying is not allowed */ + Renderer(Renderer&) = delete; + + /** + * @brief Move constructor + * + * Performs a destructive move, i.e. the original object isn't usable + * afterwards anymore. + */ + Renderer(Renderer&&) noexcept; + + ~Renderer(); + + /** @brief Copying is not allowed */ + Renderer& operator=(Renderer&) = delete; + + /** @brief Move assignment */ + Renderer& operator=(Renderer&&) noexcept; + + /** @brief Flags */ + RendererFlags flags() const; + + /** + * @brief Glyph index capacity + * + * Describes how many glyphs can be rendered into the index buffer. The + * actual index count is six times the capacity. + * @see @ref glyphCapacity(), @ref glyphVertexCapacity(), + * @ref glyphCount(), @ref runCapacity(), @ref reserve() + */ + UnsignedInt glyphIndexCapacity() const; + + /** + * @brief Glyph vertex capacity + * + * Describes how many glyphs can be rendered into the vertex buffer. + * The actual vertex count is four times the capacity. + * @see @ref glyphCapacity(), @ref glyphIndexCapacity(), + * @ref glyphCount(), @ref runCapacity(), @ref reserve() + */ + UnsignedInt glyphVertexCapacity() const; + + /** + * @brief Index type + * + * The smallest type that can describe vertices for all + * @ref glyphCapacity() glyphs and isn't smaller than what was set in + * @ref setIndexType(). Initially set to + * @ref MeshIndexType::UnsignedByte, a lerger type is automatically + * switched to once the capacity exceeds @cpp 64 @ce and @cpp 16384 @ce + * glyphs. + */ + MeshIndexType indexType() const; + + /** + * @brief Set index type + * @return Reference to self (for method chaining) + * + * Sets the smallest possible index type to be used. Initially + * @ref MeshIndexType::UnsignedByte, a larger type is automatically + * switched to once @ref glyphCapacity() exceeds @cpp 64 @ce and + * @cpp 16384 @ce glyphs. Set to a larger type if you want it to be + * used even if the glyph capacity is smaller. Setting it back to a + * smaller type afterwards uses the type only if the glyph capacity + * allows it. + */ + Renderer& setIndexType(MeshIndexType atLeast); + + /** + * @brief Glyph positions + * + * Expects that the renderer was constructed with + * @ref RendererFlag::GlyphPositionsClusters. 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 are not accessible + * + * Unlike with @ref RendererCore, to save memory, glyph IDs are + * retrieved only to a temporary location to produce glyph quads and + * are subsequently overwritten by vertex data. + */ + Containers::StridedArrayView1D glyphIds() const = delete; + + /** + * @brief Glyph cluster IDs + * + * Expects that the renderer was constructed with + * @ref RendererFlag::GlyphPositionsClusters. 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 Type-erased glyph quad indices + * + * The returned view is contiguous with a size of @ref glyphCount() + * times @cpp 6 @ce, the second dimension having a size of + * @ref indexType(). The values index the @ref vertexPositions() and + * @ref vertexTextureCoordinates() / @ref vertexTextureArrayCoordinates() + * arrays. Note that the contents are not guaranteed to be meaningful + * if custom index allocator is used, as the user code is free to + * perform subsequent operations on those. + * + * Use the templated overload below to get the indices in a concrete + * type. + */ + Containers::StridedArrayView2D indices() const; + + /** + * @brief Glyph quad indices + * + * Expects that @p T is either @relativeref{Magnum,UnsignedByte}, + * @relativeref{Magnum,UnsignedShort} or + * @relativeref{Magnum,UnsignedInt} and matches @ref indexType(). The + * returned view has a size of @ref glyphCount() times @cpp 6 @ce. Note + * that the contents are not guaranteed to be meaningful if custom + * index allocator is used, as the user code is free to perform + * subsequent operations on those. + * + * Use the non-templated overload above to get a type-erased view on + * the indices. + */ + template Containers::ArrayView indices() const; + + /** + * @brief Vertex positions + * + * The returned view has a size of @ref glyphCount() times @cpp 4 @ce. + * Note that the contents are not guaranteed to be meaningful if custom + * vertex allocator is used, as the user code is free to perform + * subsequent operations on those. + */ + Containers::StridedArrayView1D vertexPositions() const; + + /** + * @brief Vertex texture coordinates + * + * Expects that the renderer was constructed with a non-array + * @ref AbstractGlyphCache, i.e. with a depth equal to @cpp 1 @ce. + * The returned view has a size of @ref glyphCount() times @cpp 4 @ce. + * Note that the contents are not guaranteed to be meaningful if custom + * vertex allocator is used, as the user code is free to perform + * subsequent operations on those. + */ + Containers::StridedArrayView1D vertexTextureCoordinates() const; + + /** + * @brief Vertex texture array coordinates + * + * Expects that the renderer was constructed with an array + * @ref AbstractGlyphCache, i.e. with a depth larger than @cpp 1 @ce. + * The returned view has a size of @ref glyphCount() times @cpp 4 @ce. + * Note that the contents are not guaranteed to be meaningful if custom + * vertex allocator is used, as the user code is free to perform + * subsequent operations on those. + */ + Containers::StridedArrayView1D vertexTextureArrayCoordinates() const; + + /** + * @brief Reserve capacity for given glyph count + * @return Reference to self (for method chaining) + * + * Calls @ref RendererCore::reserve() and additionally reserves + * capacity also for the corresponding index and vertex memory. Note + * that while reserved index and vertex capacity is derived from + * @p glyphCapacity and @ref indexType(), their actually allocated + * capacity doesn't need to match @ref glyphCapacity() and is exposed + * through @ref glyphIndexCapacity() and @ref glyphVertexCapacity(). + * @see @ref glyphCount(), @ref runCapacity(), @ref runCount() + */ + Renderer& reserve(UnsignedInt glyphCapacity, UnsignedInt runCapacity); + + /** + * @brief Clear rendered glyphs, runs and vertices + * @return Reference to self (for method chaining) + * + * Calls @ref RendererCore::clear(). 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, run or vertex allocators are used, + * they get called with empty views and zero sizes. Custom index + * allocator isn't called however, as the index buffer only needs + * updating when its capacity isn't large enough. + * + * Depending on allocator used, @ref glyphCapacity(), + * @ref glyphVertexCapacity() 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. + */ + Renderer& 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(), + * @ref glyphVertexCapacity() and @ref runCapacity() which may stay + * non-zero depending on allocator used, and @ref glyphIndexCapacity() + * plus @ref indexType() which are left untouched, the instance is + * equivalent to a default-constructed state. + */ + Renderer& reset(); + + /** + * @brief Wrap up rendering of all text added so far + * + * Calls @ref RendererCore::render() and populates also index and + * vertex data, subsequently available through @ref indices(), + * @ref vertexPositions() and @ref vertexTextureCoordinates() / + * @ref vertexTextureArrayCoordinates(). + * + * The function uses @ref renderGlyphQuadsInto() and + * @ref renderGlyphQuadIndicesInto() internally, see their + * documentation for more information. + */ + Containers::Pair render(); + + /* Overloads to remove a WTF factor from method chaining order, and to + ensure our render() is called instead of RenderCore::render() */ + #ifndef DOXYGEN_GENERATING_OUTPUT + Renderer& setCursor(const Vector2& cursor) { + return static_cast(RendererCore::setCursor(cursor)); + } + Renderer& setAlignment(Alignment alignment) { + return static_cast(RendererCore::setAlignment(alignment)); + } + Renderer& setLineAdvance(Float advance) { + return static_cast(RendererCore::setLineAdvance(advance)); + } + Renderer& setLayoutDirection(LayoutDirection direction) { + return static_cast(RendererCore::setLayoutDirection(direction)); + } + + Renderer& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView features); + Renderer& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end); + Renderer& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, std::initializer_list features); + Renderer& add(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView features); + Renderer& add(AbstractShaper& shaper, Float size, Containers::StringView text); + Renderer& add(AbstractShaper& shaper, Float size, Containers::StringView text, std::initializer_list features); + + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView features); + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text); + Containers::Pair render(AbstractShaper& shaper, Float size, Containers::StringView text, std::initializer_list features); + #endif + + #ifdef DOXYGEN_GENERATING_OUTPUT + private: + #else + protected: + #endif + struct State; + + private: + /* While the allocators get just size to grow by, these functions get + the total count */ + MAGNUM_TEXT_LOCAL void allocateIndices( + #ifndef CORRADE_NO_ASSERT + const char* messagePrefix, + #endif + UnsignedInt totalGlyphCount); + MAGNUM_TEXT_LOCAL void allocateVertices( + #ifndef CORRADE_NO_ASSERT + const char* messagePrefix, + #endif + UnsignedInt totalGlyphCount); +}; + /** @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 210a8ffea..f5b736fa8 100644 --- a/src/Magnum/Text/Test/RendererTest.cpp +++ b/src/Magnum/Text/Test/RendererTest.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include #include +#include "Magnum/Mesh.h" #include "Magnum/PixelFormat.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" @@ -87,17 +89,28 @@ struct RendererTest: TestSuite::Tester { void debugFlagCore(); void debugFlagsCore(); + void debugFlag(); + void debugFlags(); void constructCore(); void constructCoreAllocator(); void constructCoreNoCreate(); + void construct(); + void constructAllocator(); + void constructNoCreate(); + void constructCopyCore(); void constructMoveCore(); + void constructCopy(); + void constructMove(); void propertiesCore(); void propertiesCoreInvalid(); void propertiesCoreRenderingInProgress(); + void properties(); + void propertiesInvalid(); + void propertiesRenderingInProgress(); void glyphsForRuns(); void glyphsForRunsInvalid(); @@ -107,6 +120,14 @@ struct RendererTest: TestSuite::Tester { void allocateCoreGlyphAllocatorInvalid(); void allocateCoreRunAllocator(); void allocateCoreRunAllocatorInvalid(); + template void allocate(); + void allocateDifferentIndexType(); + void allocateIndexAllocator(); + void allocateIndexAllocatorInvalid(); + void allocateIndexAllocatorMaxIndexCountForType(); + void allocateVertexAllocator(); + void allocateVertexAllocatorInvalid(); + void allocateVertexAllocatorNotEnoughStrideForArrayGlyphCache(); void addSingleLine(); void addSingleLineAlign(); @@ -116,8 +137,12 @@ struct RendererTest: TestSuite::Tester { void multipleBlocks(); + template void indicesVertices(); + void clearResetCore(); void clearResetCoreAllocators(); + void clearReset(); + void clearResetAllocators(); #ifdef MAGNUM_TARGET_GL void renderData(); @@ -229,65 +254,150 @@ const struct { }, RendererCoreFlag::GlyphClusters}, }; +const struct { + const char* name; + Int glyphCacheArraySize; + RendererFlags flags; +} ConstructData[]{ + {"", 1, {}}, + {"with glyph positions and clusters", 1, RendererFlag::GlyphPositionsClusters}, + {"array glyph cache", 5, {}}, + {"array glyph cache, with glyph positions and clusters", 5, RendererFlag::GlyphPositionsClusters} +}; + +const struct { + const char* name; + Int glyphCacheArraySize; + void(*glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&); + void(*runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&); + void(*indexAllocator)(void*, UnsignedInt, Containers::ArrayView&); + void(*vertexAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&); + RendererFlags flags; +} ConstructAllocatorData[]{ + {"no allocators", 1, + nullptr, nullptr, nullptr, nullptr, {}}, + {"no allocators, with glyph positions & clusters", 1, + nullptr, nullptr, nullptr, nullptr, RendererFlag::GlyphPositionsClusters}, + {"no allocators, array glyph cache", 5, + nullptr, nullptr, nullptr, nullptr, {}}, + {"no allocators, array glyph cache, with glyph positions & clusters", 5, + nullptr, nullptr, nullptr, nullptr, RendererFlag::GlyphPositionsClusters}, + {"glyph allocator", 1, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, nullptr, nullptr, nullptr, {}}, + {"glyph allocator, with glyph positions & clusters", 1, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, nullptr, nullptr, nullptr, RendererFlag::GlyphPositionsClusters}, + {"run allocator", 1, nullptr, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, nullptr, nullptr, {}}, + {"index allocator", 1, nullptr, nullptr, [](void* called, UnsignedInt, Containers::ArrayView&){ + ++*static_cast(called); + }, nullptr, {}}, + {"vertex allocator", 1, nullptr, nullptr, nullptr, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, {}}, + {"vertex allocator. array glyph cache", 5, nullptr, nullptr, nullptr, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, {}}, + {"all allocators", 1, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::ArrayView&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, {}}, + {"all allocators, with glyph positions & clusters", 1, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::ArrayView&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, RendererFlag::GlyphPositionsClusters}, + {"all allocators, array glyph cache", 5, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::ArrayView&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, {}}, + {"all allocators, array glyph cache, with glyph positions & clusters", 5, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::ArrayView&){ + ++*static_cast(called); + }, [](void* called, UnsignedInt, Containers::StridedArrayView1D&, Containers::StridedArrayView1D&){ + ++*static_cast(called); + }, RendererFlag::GlyphPositionsClusters}, +}; + const struct { TestSuite::TestCaseDescriptionSourceLocation name; RendererCoreFlags flagsCore; + RendererFlags flags; 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 26, 3, 26, 1, false, false, true, true, 26, 3}, {"second reserve() reallocates glyphs", - {}, 3, 3, 26, 3, false, false, false, true, 26, 3}, + {}, {}, 3, 3, 26, 3, false, false, false, true, 26, 3}, {"second reserve() reallocates runs", - {}, 26, 1, 26, 3, false, false, true, false, 26, 3}, + {}, {}, 26, 1, 26, 3, false, false, true, false, 26, 3}, {"render", - {}, 26, 3, 0, 0, true, false, true, true, 26, 3}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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}, + {}, {}, 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, + RendererCoreFlag::GlyphClusters, RendererFlag::GlyphPositionsClusters, 26, 3, 26, 3, false, false, true, true, 26, 3}, {"with glyph (positions and) clusters, second reserve() less glyphs than first", - RendererCoreFlag::GlyphClusters, + RendererCoreFlag::GlyphClusters, RendererFlag::GlyphPositionsClusters, 26, 3, 23, 3, false, false, true, true, 26, 3}, {"with glyph (positions and) clusters, second reserve() reallocates glyphs", - RendererCoreFlag::GlyphClusters, + RendererCoreFlag::GlyphClusters, RendererFlag::GlyphPositionsClusters, 3, 3, 26, 3, false, false, false, true, 26, 3}, {"with glyph (positions and) clusters, render", - RendererCoreFlag::GlyphClusters, + RendererCoreFlag::GlyphClusters, RendererFlag::GlyphPositionsClusters, 26, 3, 0, 0, true, false, true, true, 26, 3}, {"with glyph (positions and) clusters, render, second render() reallocates glyphs", - RendererCoreFlag::GlyphClusters, + RendererCoreFlag::GlyphClusters, RendererFlag::GlyphPositionsClusters, 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, + RendererCoreFlag::GlyphClusters, RendererFlag::GlyphPositionsClusters, 3, 3, 0, 0, true, true, false, true, 26, 3} }; @@ -452,6 +562,399 @@ const struct { "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; + Containers::Optional indexTypeFirst; + UnsignedInt reserveFirst; + MeshIndexType expectedIndexTypeFirst; + Containers::Optional indexTypeSecond; + bool clear; + UnsignedInt reserveSecond, expectedCapacitySecond, expectedIndexCapacitySecond; + MeshIndexType expectedIndexTypeSecond; +} AllocateDifferentIndexTypeData[]{ + {"UnsignedByte to UnsignedShort due to capacity", + {}, 12, MeshIndexType::UnsignedByte, + {}, false, 65, 65, 65, MeshIndexType::UnsignedShort}, + {"UnsignedByte to UnsignedInt due to capacity", + {}, 12, MeshIndexType::UnsignedByte, + {}, false, 16385, 16385, 16385, MeshIndexType::UnsignedInt}, + {"UnsignedShort to UnsignedInt due to capacity", + {}, 65, MeshIndexType::UnsignedShort, + {}, false, 16385, 16385, 16385, MeshIndexType::UnsignedInt}, + + {"UnsignedShort stays even after reserving less", + {}, 65, MeshIndexType::UnsignedShort, + {}, false, 12, 65, 65, MeshIndexType::UnsignedShort}, + {"UnsignedInt stays even after reserving less", + {}, 16385, MeshIndexType::UnsignedInt, + {}, false, 12, 16385, 16385, MeshIndexType::UnsignedInt}, + + {"UnsignedByte changed to UnsignedShort", + {}, 12, MeshIndexType::UnsignedByte, + MeshIndexType::UnsignedShort, false, 0, 12, 12, MeshIndexType::UnsignedShort}, + {"UnsignedByte changed to UnsignedInt", + {}, 12, MeshIndexType::UnsignedByte, + MeshIndexType::UnsignedInt, false, 0, 12, 12, MeshIndexType::UnsignedInt}, + {"UnsignedShort changed to UnsignedInt", + MeshIndexType::UnsignedShort, 12, MeshIndexType::UnsignedShort, + MeshIndexType::UnsignedInt, false, 0, 12, 12, MeshIndexType::UnsignedInt}, + {"UnsignedShort due to capacity, changed to UnsignedInt", + {}, 65, MeshIndexType::UnsignedShort, + MeshIndexType::UnsignedInt, false, 0, 65, 65, MeshIndexType::UnsignedInt}, + {"UnsignedInt changed to UnsignedShort", + MeshIndexType::UnsignedInt, 12, MeshIndexType::UnsignedInt, + /* The full existing capacity gets reused for a smaller type, so it + doubles */ + MeshIndexType::UnsignedShort, false, 0, 12, 24, MeshIndexType::UnsignedShort}, + {"UnsignedInt changed to UnsignedByte", + MeshIndexType::UnsignedInt, 12, MeshIndexType::UnsignedInt, + /* The full existing capacity gets reused for a smaller type, so it + quadruples */ + MeshIndexType::UnsignedByte, false, 0, 12, 48, MeshIndexType::UnsignedByte}, + {"UnsignedShort changed to UnsignedByte", + MeshIndexType::UnsignedShort, 12, MeshIndexType::UnsignedShort, + /* The full existing capacity gets reused for a smaller type, so it + doubles */ + MeshIndexType::UnsignedByte, false, 0, 12, 24, MeshIndexType::UnsignedByte}, + + {"UnsignedInt changed to UnsignedByte but capacity needs UnsignedShort", + MeshIndexType::UnsignedInt, 65, MeshIndexType::UnsignedInt, + /* The full existing capacity gets reused for a smaller type, so it + doubles */ + MeshIndexType::UnsignedByte, false, 0, 65, 130, MeshIndexType::UnsignedShort}, + {"UnsignedInt changed to UnsignedByte but capacity needs UnsignedInt", + MeshIndexType::UnsignedInt, 16385, MeshIndexType::UnsignedInt, + MeshIndexType::UnsignedByte, false, 0, 16385, 16385, MeshIndexType::UnsignedInt}, + {"UnsignedInt changed to UnsignedShort but capacity needs UnsignedInt", + MeshIndexType::UnsignedInt, 16385, MeshIndexType::UnsignedInt, + MeshIndexType::UnsignedShort, false, 0, 16385, 16385, MeshIndexType::UnsignedInt}, + {"UnsignedShort changed to UnsignedByte but capacity needs UnsignedShort", + MeshIndexType::UnsignedShort, 65, MeshIndexType::UnsignedShort, + MeshIndexType::UnsignedByte, false, 0, 65, 65, MeshIndexType::UnsignedShort}, + + {"UnsignedByte, cleared, stays UnsignedByte", + {}, 64, MeshIndexType::UnsignedByte, + {}, true, 0, 64, 64, MeshIndexType::UnsignedByte}, + {"UnsignedShort explicit, cleared, stays UnsignedShort", + MeshIndexType::UnsignedShort, 12, MeshIndexType::UnsignedShort, + {}, true, 0, 12, 12, MeshIndexType::UnsignedShort}, + {"UnsignedShort explicit + capacity, cleared, stays UnsignedShort", + MeshIndexType::UnsignedShort, 16384, MeshIndexType::UnsignedShort, + {}, true, 0, 16384, 16384, MeshIndexType::UnsignedShort}, + {"UnsignedShort due to capacity, cleared, stays UnsignedShort", + {}, 65, MeshIndexType::UnsignedShort, + /* clear() doesn't touch the index buffer in any way so this doesn't + become UnsignedByte even though it could if the capacity would be + reset to < 65 */ + {}, true, 0, 65, 65, MeshIndexType::UnsignedShort}, + {"UnsignedInt explicit, cleared, stays UnsignedInt", + MeshIndexType::UnsignedInt, 12, MeshIndexType::UnsignedInt, + {}, true, 0, 12, 12, MeshIndexType::UnsignedInt}, + {"UnsignedInt explicit + capacity, cleared, stays UnsignedInt", + MeshIndexType::UnsignedInt, 30000, MeshIndexType::UnsignedInt, + {}, true, 0, 30000, 30000, MeshIndexType::UnsignedInt}, + {"UnsignedInt due to capacity, cleared, stays UnsignedInt", + {}, 16385, MeshIndexType::UnsignedInt, + /* clear() doesn't touch the index buffer in any way so this doesn't + become UnsignedShort or less even though it could if the capacity + would be reset to < 16385 */ + {}, true, 0, 16385, 16385, MeshIndexType::UnsignedInt}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + Containers::Optional indexType; + UnsignedInt reserve; + MeshIndexType expectedIndexType; + Containers::Optional secondIndexType; + UnsignedInt secondReserve; + MeshIndexType expectedSecondIndexType; + bool render, renderAddOnly, expectNoReallocation; + UnsignedInt indicesSize, expectedCapacity, expectedIndexCapacity; +} AllocateIndexAllocatorData[]{ + {"second reserve() same as first, UnsignedByte", + {}, 26, MeshIndexType::UnsignedByte, + {}, 26, MeshIndexType::UnsignedByte, + false, false, true, 0, 26, 26}, + {"second reserve() same as first, UnsignedShort", + MeshIndexType::UnsignedShort, 26, MeshIndexType::UnsignedShort, + {}, 26, MeshIndexType::UnsignedShort, + false, false, true, 0, 26, 26}, + {"second reserve() same as first, UnsignedInt", + MeshIndexType::UnsignedInt, 26, MeshIndexType::UnsignedInt, + {}, 26, MeshIndexType::UnsignedInt, + false, false, true, 0, 26, 26}, + {"second reserve() smaller than first, UnsignedByte", + MeshIndexType::UnsignedByte, 26, MeshIndexType::UnsignedByte, + {}, 23, MeshIndexType::UnsignedByte, + false, false, true, 0, 26, 26}, + {"second reserve() smaller than first, UnsignedShort", + MeshIndexType::UnsignedShort, 26, MeshIndexType::UnsignedShort, + {}, 23, MeshIndexType::UnsignedShort, + false, false, true, 0, 26, 26}, + {"second reserve() smaller than first, UnsignedInt", + MeshIndexType::UnsignedInt, 26, MeshIndexType::UnsignedInt, + {}, 23, MeshIndexType::UnsignedInt, + false, false, true, 0, 26, 26}, + {"second reserve() reallocates, UnsignedByte", + MeshIndexType::UnsignedByte, 3, MeshIndexType::UnsignedByte, + {}, 26, MeshIndexType::UnsignedByte, + /* Not a multiple of 6 type sizes, should get capped, same below */ + false, false, false, 27*6*1 + 3, 26, 27}, + {"second reserve() reallocates, UnsignedShort", + MeshIndexType::UnsignedShort, 3, MeshIndexType::UnsignedShort, + {}, 26, MeshIndexType::UnsignedShort, + false, false, false, 30*6*2 + 11, 26, 30}, + {"second reserve() reallocates, UnsignedInt", + MeshIndexType::UnsignedInt, 3, MeshIndexType::UnsignedInt, + {}, 26, MeshIndexType::UnsignedInt, + false, false, false, 26*6*4 + 21, 26, 26}, + {"second reserve() reallocates, type changes to UnsignedShort", + {}, 3, MeshIndexType::UnsignedByte, + {}, 65, MeshIndexType::UnsignedShort, + false, false, false, 69*6*2 + 11, 65, 69}, + {"second reserve() reallocates, type changes to UnsignedInt", + {}, 3, MeshIndexType::UnsignedByte, + {}, 16385, MeshIndexType::UnsignedInt, + false, false, false, 18343*6*4 + 21, 16385, 18343}, + {"second setIndexType() same as first, UnsignedByte", + {}, 26, MeshIndexType::UnsignedByte, + MeshIndexType::UnsignedByte, 0, MeshIndexType::UnsignedByte, + false, false, true, 0, 26, 26}, + {"second setIndexType() same as first, UnsignedShort", + MeshIndexType::UnsignedShort, 26, MeshIndexType::UnsignedShort, + MeshIndexType::UnsignedShort, 0, MeshIndexType::UnsignedShort, + false, false, true, 0, 26, 26}, + {"second setIndexType() same as first, UnsignedInt", + MeshIndexType::UnsignedInt, 26, MeshIndexType::UnsignedInt, + MeshIndexType::UnsignedInt, 0, MeshIndexType::UnsignedInt, + false, false, true, 0, 26, 26}, + {"second setIndexType() reallocates, UnsignedByte, type changes to UnsignedShort", + {}, 26, MeshIndexType::UnsignedByte, + MeshIndexType::UnsignedShort, 0, MeshIndexType::UnsignedShort, + false, false, false, 28*6*2 + 1, 26, 28}, + {"second setIndexType() reallocates, UnsignedByte, type changes to UnsignedInt", + {}, 26, MeshIndexType::UnsignedByte, + MeshIndexType::UnsignedInt, 0, MeshIndexType::UnsignedInt, + false, false, false, 28*6*4 + 1, 26, 28}, + {"second setIndexType() reallocates, UnsignedShort, type changes to UnsignedInt", + MeshIndexType::UnsignedShort, 26, MeshIndexType::UnsignedShort, + MeshIndexType::UnsignedInt, 0, MeshIndexType::UnsignedInt, + false, false, false, 28*6*4 + 1, 26, 28}, + {"second setIndexType() reallocates, UnsignedInt, type changes to UnsignedShort", + MeshIndexType::UnsignedInt, 26, MeshIndexType::UnsignedInt, + MeshIndexType::UnsignedShort, 0, MeshIndexType::UnsignedShort, + false, false, false, 28*6*2 + 1, 26, 28}, + {"second setIndexType() reallocates, UnsignedInt, type changes to UnsignedByte", + MeshIndexType::UnsignedInt, 26, MeshIndexType::UnsignedInt, + MeshIndexType::UnsignedByte, 0, MeshIndexType::UnsignedByte, + false, false, false, 28*6*1 + 1, 26, 28}, + {"second setIndexType() reallocates, UnsignedShort, type changes to UnsignedByte", + MeshIndexType::UnsignedShort, 26, MeshIndexType::UnsignedShort, + MeshIndexType::UnsignedByte, 0, MeshIndexType::UnsignedByte, + false, false, false, 28*6*1 + 1, 26, 28}, + {"second setIndexType() reallocates, UnsignedInt, type changed to UnsignedByte but capacity needs UnsignedShort", + MeshIndexType::UnsignedInt, 65, MeshIndexType::UnsignedInt, + MeshIndexType::UnsignedByte, 0, MeshIndexType::UnsignedShort, + false, false, false, 70*6*2 + 1, 65, 70}, + {"second setIndexType(), UnsignedInt, type changes to UnsignedByte but capacity still needs UnsignedInt", + MeshIndexType::UnsignedInt, 16385, MeshIndexType::UnsignedInt, + MeshIndexType::UnsignedByte, 0, MeshIndexType::UnsignedInt, + false, false, true, 0, 16385, 16385}, + {"second setIndexType(), UnsignedInt, type changes to UnsignedShort but capacity still needs UnsignedInt", + MeshIndexType::UnsignedInt, 16385, MeshIndexType::UnsignedInt, + MeshIndexType::UnsignedShort, 0, MeshIndexType::UnsignedInt, + false, false, true, 0, 16385, 16385}, + {"second setIndexType(), UnsignedShort, type changes to UnsignedByte but capacity still needs UnsignedShort", + MeshIndexType::UnsignedShort, 65, MeshIndexType::UnsignedShort, + MeshIndexType::UnsignedByte, 0, MeshIndexType::UnsignedShort, + false, false, true, 0, 65, 65}, + {"render, UnsignedByte", + {}, 26, MeshIndexType::UnsignedByte, + {}, 26, MeshIndexType::UnsignedShort, + true, false, true, 0, 26, 26}, + {"render, UnsignedShort", + MeshIndexType::UnsignedShort, 26, MeshIndexType::UnsignedShort, + {}, 26, MeshIndexType::UnsignedShort, + true, false, true, 0, 26, 26}, + {"render, UnsignedInt", + MeshIndexType::UnsignedInt, 26, MeshIndexType::UnsignedInt, + {}, 26, MeshIndexType::UnsignedInt, + true, false, true, 0, 26, 26}, + {"render, second render() reallocates, UnsignedByte", + MeshIndexType::UnsignedByte, 3, MeshIndexType::UnsignedByte, + {}, 26, MeshIndexType::UnsignedByte, + /* Not a multiple of 6 type sizes, should get capped, same below */ + true, false, false, 28*6*1 + 5, 26, 28}, + {"render, second render() reallocates, UnsignedShort", + MeshIndexType::UnsignedShort, 3, MeshIndexType::UnsignedShort, + {}, 26, MeshIndexType::UnsignedShort, + true, false, false, 27*6*2 + 9, 26, 27}, + {"render, second render() reallocates, UnsignedInt", + MeshIndexType::UnsignedInt, 3, MeshIndexType::UnsignedInt, + {}, 26, MeshIndexType::UnsignedInt, + true, false, false, 29*6*4 + 19, 26, 29}, + {"render, second render() reallocates while in progress, UnsignedByte", + MeshIndexType::UnsignedByte, 3, MeshIndexType::UnsignedByte, + {}, 26, MeshIndexType::UnsignedByte, + true, true, false, 28*6*1 + 5, 26, 28}, + {"render, second render() reallocates while in progress, UnsignedShort", + MeshIndexType::UnsignedShort, 3, MeshIndexType::UnsignedShort, + {}, 26, MeshIndexType::UnsignedShort, + true, true, false, 27*6*2 + 9, 26, 27}, + {"render, second render() reallocates while in progress, UnsignedInt", + MeshIndexType::UnsignedInt, 3, MeshIndexType::UnsignedInt, + {}, 26, MeshIndexType::UnsignedInt, + true, true, false, 29*6*4 + 19, 26, 29}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + MeshIndexType indexType; + bool setIndexType, render; + std::size_t size; + const char* expected; +} AllocateIndexAllocatorInvalidData[]{ + {"reserve, too small, UnsignedByte", + MeshIndexType::UnsignedByte, false, false, 101, + "Text::Renderer::reserve(): expected allocated indices to have at least 102 bytes but got 101\n"}, + {"reserve, too small, UnsignedShort", + MeshIndexType::UnsignedShort, false, false, 199, + "Text::Renderer::reserve(): expected allocated indices to have at least 204 bytes but got 199\n"}, + {"reserve, too small, UnsignedInt", + MeshIndexType::UnsignedInt, false, false, 405, + "Text::Renderer::reserve(): expected allocated indices to have at least 408 bytes but got 405\n"}, + /* Not testing setIndexType() with UnsignedByte, the initial allocation is + large enough for it already so the allocator doesn't even get called */ + {"setIndexType, too small, UnsignedShort", + /* Here it's just for the initial 10 glyphs, not 17 */ + MeshIndexType::UnsignedShort, true, false, 119, + "Text::Renderer::setIndexType(): expected allocated indices to have at least 120 bytes but got 119\n"}, + {"setIndexType, too small, UnsignedInt", + /* Here it's just for the initial 10 glyphs, not 17 */ + MeshIndexType::UnsignedInt, true, false, 239, + "Text::Renderer::setIndexType(): expected allocated indices to have at least 240 bytes but got 239\n"}, + {"render, too small, UnsignedByte", + MeshIndexType::UnsignedByte, false, true, 101, + "Text::Renderer::render(): expected allocated indices to have at least 102 bytes but got 101\n"}, + {"render, too small, UnsignedShort", + MeshIndexType::UnsignedShort, false, true, 199, + "Text::Renderer::render(): expected allocated indices to have at least 204 bytes but got 199\n"}, + {"render, too small, UnsignedInt", + MeshIndexType::UnsignedInt, false, true, 405, + "Text::Renderer::render(): expected allocated indices to have at least 408 bytes but got 405\n"}, +}; + +const struct { + const char* name; + MeshIndexType indexType; + UnsignedInt expected; +} AllocateIndexAllocatorMaxIndexCountForTypeData[]{ + {"UnsignedByte", MeshIndexType::UnsignedByte, + /* 256 indexable vertices is at most 64 glyphs */ + 64}, + {"UnsignedShort", MeshIndexType::UnsignedShort, + /* 65536 indexable vertices is at most 16384 glyphs */ + 16384}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + Int glyphCacheArraySize; + UnsignedInt reserve, secondReserve; + bool render, renderAddOnly, expectNoReallocation; + UnsignedInt + positionSize, + textureCoordinateSize, + expectedCapacity; +} AllocateVertexAllocatorData[]{ + {"second reserve() same as first", + 1, 26, 26, false, false, true, + 0, 0, 26}, + {"second reserve() smaller than first", + 1, 26, 23, false, false, true, + 0, 0, 26}, + {"second reserve() reallocates, positions smallest", + 1, 3, 26, false, false, false, + /* Not a multiple of 4, should get capped, same below */ + 27*4 + 3, 28*4 + 1, 27}, + {"second reserve() reallocates, texture coordinates smallest", + 1, 3, 26, false, false, false, + 27*4 + 2, 26*4 + 0, 26}, + {"array glyph cache, second reserve() same as first", + 5, 26, 26, false, false, true, + 0, 0, 26}, + {"array glyph cache, second reserve() smaller than first", + 5, 26, 23, false, false, true, + 0, 0, 26}, + {"array glyph cache, second reserve() reallocates, positions smallest", + 5, 23, 26, false, false, false, + /* Not a multiple of 4, should get capped, same below */ + 27*4 + 3, 28*4 + 1, 27}, + {"array glyph cache, second reserve() reallocates, texture coordinates smallest", + 5, 23, 26, false, false, false, + 27*4 + 2, 26*4 + 3, 26}, + {"array glyph cache, second reserve() reallocates while in progress", + 5, 23, 26, false, true, false, + 26*4 + 2, 26*4 + 2, 26}, + {"render", + 1, 26, 26, true, false, true, + 0, 0, 26}, + {"render, second render() reallocates, positions smallest", + 1, 3, 26, true, false, false, + /* Not a multiple of 4, should get capped, same below */ + 27*4 + 0, 28*4 + 3, 27}, + {"render, second render() reallocates, texture coordinates smallest", + 1, 3, 26, true, false, false, + 27*4 + 1, 26*4 + 3, 26}, + {"array glyph cache, render", + 5, 26, 26, true, false, true, + 0, 0, 26}, + {"array glyph cache, render, second render() reallocates, positions smallest", + 5, 3, 26, true, false, false, + 27*4 + 2, 28*4 + 3, 27}, + {"array glyph cache, render, second render() reallocates, texture coordinates smallest", + 5, 3, 26, true, false, false, + 27*4 + 1, 26*4 + 3, 26}, + {"array glyph cache, render, second render() reallocates while in progress", + 5, 3, 26, true, true, false, + 26*4 + 1, 26*4 + 1, 26}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + bool render; + std::size_t positionSize, textureCoordinateSize; + const char* expected; +} AllocateVertexAllocatorInvalidData[]{ + {"reserve, positions too small", + false, 67, 68, + "Text::Renderer::reserve(): expected allocated vertex positions and texture coordinates to have at least 68 elements but got 67 and 68\n"}, + {"render, positions too small", + true, 64, 68, + "Text::Renderer::render(): expected allocated vertex positions and texture coordinates to have at least 68 elements but got 64 and 68\n"}, + {"reserve, texture coordinates too small", + false, 68, 63, + "Text::Renderer::reserve(): expected allocated vertex positions and texture coordinates to have at least 68 elements but got 68 and 63\n"}, + {"render, texture coordinates too small", + true, 68, 65, + "Text::Renderer::render(): expected allocated vertex positions and texture coordinates to have at least 68 elements but got 68 and 65\n"}, +}; + +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + bool render; + bool flipped; + const char* expected; +} AllocateVertexAllocatorNotEnoughStrideForArrayGlyphCacheData[]{ + {"reserve", false, false, + "Text::Renderer::reserve(): expected allocated texture coordinates to have a stride large enough to fit a Vector3 but got only 8 bytes\n"}, + {"reserve, flipped", false, true, + "Text::Renderer::reserve(): expected allocated texture coordinates to have a stride large enough to fit a Vector3 but got only 8 bytes\n"}, + {"render", true, false, + "Text::Renderer::render(): expected allocated texture coordinates to have a stride large enough to fit a Vector3 but got only 8 bytes\n"}, +}; + const struct { TestSuite::TestCaseDescriptionSourceLocation name; /* Char begin, end, size multiplier */ @@ -891,6 +1394,47 @@ const struct { {"with glyph clusters", RendererCoreFlag::GlyphClusters} }; +const struct { + const char* name; + Int glyphCacheArraySize; + RendererFlags flags; + bool customGlyphAllocator; + UnsignedInt reserve; +} IndicesVerticesData[]{ + {"", + 1, {}, false, 0}, + {"array glyph cache", + 5, {}, false, 0}, + {"glyph positions + clusters", + 1, RendererFlag::GlyphPositionsClusters, false, 0}, + {"glyph positions + clusters, array glyph cache", + 5, RendererFlag::GlyphPositionsClusters, false, 0}, + {"reserve all upfront", + 1, {}, false, 16}, + {"reserve all upfront, array glyph cache", + 5, {}, false, 16}, + {"reserve all upfront, glyph positions + clusters", + 1, RendererFlag::GlyphPositionsClusters, false, 16}, + {"reserve all upfront, glyph positions + clusters, array glyph cache", + 5, RendererFlag::GlyphPositionsClusters, false, 16}, + {"reserve partially upfront", + 1, {}, false, 4}, + {"reserve partially upfront, array glyph cache", + 5, {}, false, 4}, + {"reserve partially upfront, glyph positions + clusters", + 1, RendererFlag::GlyphPositionsClusters, false, 4}, + {"reserve partially upfront, glyph positions + clusters, array glyph cache", + 5, RendererFlag::GlyphPositionsClusters, false, 4}, + {"custom glyph allocator", + 1, {}, true, 0}, + {"custom glyph allocator, array glyph cache", + 5, {}, true, 0}, + {"custom glyph allocator, glyph positions + clusters", + 1, RendererFlag::GlyphPositionsClusters, true, 0}, + {"custom glyph allocator, glyph positions + clusters, array glyph cache", + 5, RendererFlag::GlyphPositionsClusters, true, 0}, +}; + const struct { const char* name; RendererCoreFlags flags; @@ -909,6 +1453,19 @@ const struct { {"reset while in progress", {}, true, true, 2}, }; +const struct { + const char* name; + RendererFlags flags; + bool renderAddOnly, reset; +} ClearResetData[]{ + {"clear", {}, false, false}, + {"clear, with glyph positions & clusters", RendererFlag::GlyphPositionsClusters, false, false}, + {"reset", {}, false, true}, + {"clear while in progress", {}, true, false}, + {"clear while in progress, with glyph positions & clusters", RendererFlag::GlyphPositionsClusters, true, false}, + {"reset while in progress", {}, true, true}, +}; + #ifdef MAGNUM_TARGET_GL const struct { TestSuite::TestCaseDescriptionSourceLocation name; @@ -1133,7 +1690,9 @@ RendererTest::RendererTest() { Containers::arraySize(GlyphRangeForBytesData)); addTests({&RendererTest::debugFlagCore, - &RendererTest::debugFlagsCore}); + &RendererTest::debugFlagsCore, + &RendererTest::debugFlag, + &RendererTest::debugFlags}); addInstancedTests({&RendererTest::constructCore, &RendererTest::constructCoreAllocator}, @@ -1141,12 +1700,23 @@ RendererTest::RendererTest() { addTests({&RendererTest::constructCoreNoCreate}); + addInstancedTests({&RendererTest::construct, + &RendererTest::constructAllocator}, + Containers::arraySize(ConstructData)); + + addTests({&RendererTest::constructNoCreate}); + addTests({&RendererTest::constructCopyCore, &RendererTest::constructMoveCore, + &RendererTest::constructCopy, + &RendererTest::constructMove, &RendererTest::propertiesCore, &RendererTest::propertiesCoreInvalid, &RendererTest::propertiesCoreRenderingInProgress, + &RendererTest::properties, + &RendererTest::propertiesInvalid, + &RendererTest::propertiesRenderingInProgress, &RendererTest::glyphsForRuns, &RendererTest::glyphsForRunsInvalid}); @@ -1166,6 +1736,36 @@ RendererTest::RendererTest() { addInstancedTests({&RendererTest::allocateCoreRunAllocatorInvalid}, Containers::arraySize(AllocateCoreRunAllocatorInvalidData)); + addInstancedTests({ + &RendererTest::allocate, + &RendererTest::allocate, + &RendererTest::allocate, + &RendererTest::allocate, + &RendererTest::allocate, + &RendererTest::allocate}, + Containers::arraySize(AllocateData)); + + addInstancedTests({&RendererTest::allocateDifferentIndexType}, + Containers::arraySize(AllocateDifferentIndexTypeData)); + + addInstancedTests({&RendererTest::allocateIndexAllocator}, + Containers::arraySize(AllocateIndexAllocatorData)); + + addInstancedTests({&RendererTest::allocateIndexAllocatorInvalid}, + Containers::arraySize(AllocateIndexAllocatorInvalidData)); + + addInstancedTests({&RendererTest::allocateIndexAllocatorMaxIndexCountForType}, + Containers::arraySize(AllocateIndexAllocatorMaxIndexCountForTypeData)); + + addInstancedTests({&RendererTest::allocateVertexAllocator}, + Containers::arraySize(AllocateVertexAllocatorData)); + + addInstancedTests({&RendererTest::allocateVertexAllocatorInvalid}, + Containers::arraySize(AllocateVertexAllocatorInvalidData)); + + addInstancedTests({&RendererTest::allocateVertexAllocatorNotEnoughStrideForArrayGlyphCache}, + Containers::arraySize(AllocateVertexAllocatorNotEnoughStrideForArrayGlyphCacheData)); + addInstancedTests({&RendererTest::addSingleLine}, Containers::arraySize(AddSingleLineData)); @@ -1183,10 +1783,20 @@ RendererTest::RendererTest() { addInstancedTests({&RendererTest::multipleBlocks}, Containers::arraySize(MultipleBlocksData)); + addInstancedTests({ + &RendererTest::indicesVertices, + &RendererTest::indicesVertices, + &RendererTest::indicesVertices + }, Containers::arraySize(IndicesVerticesData)); + addInstancedTests({&RendererTest::clearResetCore, &RendererTest::clearResetCoreAllocators}, Containers::arraySize(ClearResetCoreData)); + addInstancedTests({&RendererTest::clearReset, + &RendererTest::clearResetAllocators}, + Containers::arraySize(ClearResetData)); + #ifdef MAGNUM_TARGET_GL addInstancedTests({&RendererTest::renderData}, Containers::arraySize(RenderDataData)); @@ -2047,6 +2657,18 @@ void RendererTest::debugFlagsCore() { CORRADE_COMPARE(out, "Text::RendererCoreFlag::GlyphClusters|Text::RendererCoreFlag(0xf0) Text::RendererCoreFlags{}\n"); } +void RendererTest::debugFlag() { + Containers::String out; + Debug{&out} << RendererFlag::GlyphPositionsClusters << RendererFlag(0xca); + CORRADE_COMPARE(out, "Text::RendererFlag::GlyphPositionsClusters Text::RendererFlag(0xca)\n"); +} + +void RendererTest::debugFlags() { + Containers::String out; + Debug{&out} << (RendererFlag::GlyphPositionsClusters|RendererFlag(0xf0)) << RendererFlags{}; + CORRADE_COMPARE(out, "Text::RendererFlag::GlyphPositionsClusters|Text::RendererFlag(0xf0) Text::RendererFlags{}\n"); +} + void RendererTest::constructCore() { auto&& data = ConstructCoreData[testCaseInstanceId()]; setTestCaseDescription(data.name); @@ -2121,6 +2743,100 @@ void RendererTest::constructCoreNoCreate() { CORRADE_VERIFY(!std::is_convertible::value); } +void RendererTest::construct() { + auto&& data = ConstructData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, data.glyphCacheArraySize}}; + + Renderer 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_COMPARE(renderer.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); + CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedByte); + 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); + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + } + /* Second dimension size matches index type size always */ + CORRADE_COMPARE(renderer.indices().size(), (Containers::Size2D{0, 1})); + CORRADE_COMPARE(renderer.indices().size(), 0); + CORRADE_COMPARE(renderer.vertexPositions().size(), 0); + if(data.glyphCacheArraySize == 1) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 0); +} + +void RendererTest::constructAllocator() { + auto&& data = ConstructAllocatorData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, data.glyphCacheArraySize}}; + + int called = 0; + Renderer renderer{glyphCache, data.glyphAllocator, &called, data.runAllocator, &called, data.indexAllocator, &called, data.vertexAllocator, &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_COMPARE(renderer.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); + CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedByte); + 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); + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + } + /* Second dimension size matches index type size always */ + CORRADE_COMPARE(renderer.indices().size(), (Containers::Size2D{0, 1})); + CORRADE_COMPARE(renderer.indices().size(), 0); + CORRADE_COMPARE(renderer.vertexPositions().size(), 0); + if(data.glyphCacheArraySize == 1) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 0); + + /* The allocators should not be called by default */ + CORRADE_COMPARE(called, 0); +} + +void RendererTest::constructNoCreate() { + Renderer 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{}); @@ -2149,6 +2865,39 @@ void RendererTest::constructMoveCore() { CORRADE_VERIFY(std::is_nothrow_move_assignable::value); } +void RendererTest::constructCopy() { + CORRADE_VERIFY(!std::is_copy_constructible{}); + CORRADE_VERIFY(!std::is_copy_assignable{}); +} + +void RendererTest::constructMove() { + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, 2}}, + anotherGlyphCache{PixelFormat::RGBA8Unorm, {4, 4}}; + + /* Verify that both the RendererCore and the Renderer state is + transferred */ + Renderer a{glyphCache, RendererFlag::GlyphPositionsClusters}; + a.setIndexType(MeshIndexType::UnsignedShort); + + Renderer b = Utility::move(a); + CORRADE_COMPARE(&b.glyphCache(), &glyphCache); + CORRADE_COMPARE(b.flags(), RendererFlag::GlyphPositionsClusters); + CORRADE_COMPARE(b.indexType(), MeshIndexType::UnsignedShort); + + Renderer c{anotherGlyphCache}; + c = Utility::move(b); + CORRADE_COMPARE(&c.glyphCache(), &glyphCache); + CORRADE_COMPARE(c.flags(), RendererFlag::GlyphPositionsClusters); + CORRADE_COMPARE(c.indexType(), MeshIndexType::UnsignedShort); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + void RendererTest::propertiesCore() { struct: AbstractGlyphCache { using AbstractGlyphCache::AbstractGlyphCache; @@ -2258,58 +3007,176 @@ void RendererTest::propertiesCoreRenderingInProgress() { TestSuite::Compare::String); } -void RendererTest::glyphsForRuns() { +void RendererTest::properties() { struct: AbstractGlyphCache { using AbstractGlyphCache::AbstractGlyphCache; GlyphCacheFeatures doFeatures() const override { return {}; } - } glyphCache{PixelFormat::R8Unorm, {16, 16}, {}}; + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + Renderer renderer{glyphCache}; + CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedByte); + /* Second dimension size matches index type size */ + CORRADE_COMPARE(renderer.indices().size(), (Containers::Size2D{0, 1})); + + renderer.setIndexType(MeshIndexType::UnsignedInt); + CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE(renderer.indices().size(), (Containers::Size2D{0, 4})); + + /* The setIndexType() behavior is tested thoroughly in + allocate(), allocateIndexAllocator() and indexTypeChange() */ +} + +void RendererTest::propertiesInvalid() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}, + glyphCacheArray{PixelFormat::R8Unorm, {16, 16, 2}}; + + Renderer renderer{glyphCache}; + Renderer rendererArray{glyphCacheArray}; + Renderer rendererUnsignedShortIndices{glyphCache}; + Renderer rendererUnsignedIntIndices{glyphCache}; + rendererUnsignedShortIndices.setIndexType(MeshIndexType::UnsignedShort); + rendererUnsignedIntIndices.setIndexType(MeshIndexType::UnsignedInt); + + Containers::String out; + Error redirectError{&out}; + renderer.glyphPositions(); + renderer.glyphClusters(); + renderer.indices(); + renderer.indices(); + rendererUnsignedShortIndices.indices(); + rendererUnsignedShortIndices.indices(); + rendererUnsignedIntIndices.indices(); + rendererUnsignedIntIndices.indices(); + renderer.vertexTextureArrayCoordinates(); + rendererArray.vertexTextureCoordinates(); + CORRADE_COMPARE_AS(out, + "Text::Renderer::glyphPositions(): glyph positions and clusters not enabled\n" + "Text::Renderer::glyphClusters(): glyph positions and clusters not enabled\n" + "Text::Renderer::indices(): cannot retrieve MeshIndexType::UnsignedByte as an UnsignedShort\n" + "Text::Renderer::indices(): cannot retrieve MeshIndexType::UnsignedByte as an UnsignedInt\n" + "Text::Renderer::indices(): cannot retrieve MeshIndexType::UnsignedShort as an UnsignedByte\n" + "Text::Renderer::indices(): cannot retrieve MeshIndexType::UnsignedShort as an UnsignedInt\n" + "Text::Renderer::indices(): cannot retrieve MeshIndexType::UnsignedInt as an UnsignedByte\n" + "Text::Renderer::indices(): cannot retrieve MeshIndexType::UnsignedInt as an UnsignedShort\n" + "Text::Renderer::vertexTextureArrayCoordinates(): cannot retrieve three-dimensional coordinates with a non-array glyph cache\n" + "Text::Renderer::vertexTextureCoordinates(): cannot retrieve two-dimensional coordinates with an array glyph cache\n", + TestSuite::Compare::String); +} + +void RendererTest::propertiesRenderingInProgress() { + 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}; - } + 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 {}; } - - private: - bool _opened = false; } font; - font.openFile("", 1.0f); - glyphCache.addFont(1, &font); + glyphCache.addFont(0, &font); struct: AbstractShaper { using AbstractShaper::AbstractShaper; - UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { - return text.size(); + UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return 0; } - void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { - for(UnsignedInt i = 0; i != ids.size(); ++i) - ids[i] = 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}; + Renderer renderer{glyphCache}; + + /* It should be marked as in progress even if there aren't any glyphs, to + enforce correct usage in all cases. The begin/end/features are used just + to make code coverage happier, nothing else. */ + renderer.add(shaper, 1.0f, "hello", 0, 5, Containers::ArrayView{}); CORRADE_COMPARE(renderer.glyphCount(), 0); CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_VERIFY(renderer.isRendering()); 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{}); - + /* It should blow up even if the properties are set to exactly the same as + before */ + Containers::String out; + Error redirectError{&out}; + renderer.setIndexType(MeshIndexType::UnsignedByte); + CORRADE_COMPARE_AS(out, + "Text::Renderer::setIndexType(): 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); @@ -3184,7 +4051,11 @@ void RendererTest::allocateCoreRunAllocator() { allocation.expectedRunScaleData = runScales; allocation.expectedRunEndData = runEnds; Float runScales2[8]; - UnsignedInt runEnds2[8]; + /* The run ends get used to slice up the glyph array in render(), and the + allocator assumes the data were transferred from previous. It doesn't + matter much what offsets are there, they just have to be in range to not + assert (or crash on no-assert builds). */ + UnsignedInt runEnds2[8]{}; allocation.runScales = Containers::arrayView(runScales2) .prefix(data.scaleSize); allocation.runEnds = Containers::arrayView(runEnds2) @@ -3325,16 +4196,40 @@ void RendererTest::allocateCoreRunAllocatorInvalid() { CORRADE_COMPARE(renderer.runCapacity(), 5); } -void RendererTest::addSingleLine() { - auto&& data = AddSingleLineData[testCaseInstanceId()]; +template struct IndexTraits; +template<> struct IndexTraits { + static MeshIndexType type() { return MeshIndexType::UnsignedByte; } +}; +template<> struct IndexTraits { + static MeshIndexType type() { return MeshIndexType::UnsignedShort; } +}; +template<> struct IndexTraits { + static MeshIndexType type() { return MeshIndexType::UnsignedInt; } +}; + +template struct TextureCoordinateTraits; +template<> struct TextureCoordinateTraits { + static const char* name() { return "Vector2"; } + enum: Int { GlyphCacheArraySize = 1 }; + enum: bool { HasArrayGlyphCache = false }; +}; +template<> struct TextureCoordinateTraits { + static const char* name() { return "Vector3"; } + enum: Int { GlyphCacheArraySize = 5 }; + enum: bool { HasArrayGlyphCache = true }; +}; + +template void RendererTest::allocate() { + auto&& data = AllocateData[testCaseInstanceId()]; setTestCaseDescription(data.name); + setTestCaseTemplateName({Math::TypeTraits::name(), TextureCoordinateTraits::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}, {}}; + } glyphCache{PixelFormat::R8Unorm, {16, 16, TextureCoordinateTraits::GlyphCacheArraySize}, {}}; struct: AbstractFont { FontFeatures doFeatures() const override { return {}; } @@ -3343,10 +4238,11 @@ void RendererTest::addSingleLine() { 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}; + /* 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 {} @@ -3356,164 +4252,1786 @@ void RendererTest::addSingleLine() { 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 */ - } + } 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. Just the simplest possible sizes to verify that the + data get correctly populated and not overwritten on reallocation, + detailed test for vertex data, proper per-run scaling etc. is in + indicesVertices(). */ + glyphCache.addGlyph(fontId, 4, {}, + TextureCoordinateTraits::GlyphCacheArraySize/2, + Range2Di::fromSize({8, 12}, {2, 1})); + glyphCache.addGlyph(fontId, 0, {}, + TextureCoordinateTraits::GlyphCacheArraySize - 1, + Range2Di::fromSize({12, 8}, {1, 2})); + glyphCache.addGlyph(fontId, 2, {}, + 0, + Range2Di::fromSize({12, 12}, {2, 2})); - struct Shaper: AbstractShaper { + struct: 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; + UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView) override { + return text.size(); } 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'); - } + for(UnsignedInt i = 0; i != ids.size(); ++i) + ids[i] = i*2; } 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}; + 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] = 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; + clusters[i] = 10 + i; } + } shaper{font}; - 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; + Renderer renderer{glyphCache, data.flags}; + renderer.setIndexType(IndexTraits::type()); + CORRADE_COMPARE(renderer.indexType(), IndexTraits::type()); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphPositions().data(), nullptr); + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().data(), nullptr); } - - 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 - }); + CORRADE_COMPARE(renderer.vertexPositions().size(), 0); + CORRADE_COMPARE(renderer.vertexPositions().data(), nullptr); + if(!TextureCoordinateTraits::HasArrayGlyphCache) { + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + CORRADE_COMPARE(renderer.vertexTextureCoordinates().data(), nullptr); } 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); - } + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 0); + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().data(), nullptr); + } - out = renderer.render(); + /* 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.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphPositions().data(), nullptr); + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().data(), nullptr); + } + CORRADE_COMPARE(renderer.vertexPositions().size(), 0); + CORRADE_COMPARE(renderer.vertexPositions().data(), nullptr); + if(!TextureCoordinateTraits::HasArrayGlyphCache) { + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + CORRADE_COMPARE(renderer.vertexTextureCoordinates().data(), nullptr); + } else { + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 0); + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().data(), nullptr); } - /* At the end, it shouldn't be in progress anymore. The cursor should be - still as set initially. */ + /* The views should be non-null now even if no glyphs are rendered */ + renderer.reserve(data.reserveGlyphs, data.reserveRuns); + CORRADE_COMPARE(renderer.indexType(), IndexTraits::type()); + CORRADE_COMPARE(renderer.glyphCount(), 0); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserveGlyphs); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.reserveGlyphs); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), data.reserveGlyphs); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), data.reserveRuns); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_VERIFY(renderer.glyphPositions().data()); + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + CORRADE_VERIFY(renderer.glyphClusters().data()); + } + CORRADE_COMPARE(renderer.vertexPositions().size(), 0); + CORRADE_VERIFY(renderer.vertexPositions().data()); + if(!TextureCoordinateTraits::HasArrayGlyphCache) { + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + CORRADE_VERIFY(renderer.vertexTextureCoordinates().data()); + } else { + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 0); + CORRADE_VERIFY(renderer.vertexTextureArrayCoordinates().data()); + } + + /* Rendering shouldn't reallocate anything */ + if(data.render) { + renderer.add(shaper, 1.0f, "abc"); + CORRADE_COMPARE(renderer.indexType(), IndexTraits::type()); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserveGlyphs); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.reserveGlyphs); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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()); + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + CORRADE_COMPARE(renderer.glyphPositions().size(), 0); + CORRADE_COMPARE(renderer.glyphClusters().size(), 0); + } + CORRADE_COMPARE(renderer.runScales().size(), 0); + CORRADE_COMPARE(renderer.runEnds().size(), 0); + CORRADE_COMPARE(renderer.vertexPositions().size(), 0); + if(!TextureCoordinateTraits::HasArrayGlyphCache) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().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. */ + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + 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.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_AS(renderer.indices(), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11 + }), TestSuite::Compare::Container); + /* 2---3 + | | + 0---1 ; vertex 0 is matching corresponding glyph position */ + CORRADE_COMPARE_AS(renderer.vertexPositions(), Containers::arrayView({ + {-2.25f, -0.25f}, /* a, 1x2 */ + {-1.25f, -0.25f}, + {-2.25f, 1.75f}, + {-1.25f, 1.75f}, + + {-0.75f, -0.75f}, /* b, 2x2 */ + { 1.25f, -0.75f}, + {-0.75f, 1.25f}, + { 1.25f, 1.25f}, + + { 0.75f, -0.25f}, /* c, 2x1 */ + { 2.75f, -0.25f}, + { 0.75f, 0.75f}, + { 2.75f, 0.75f}, + }), TestSuite::Compare::Container); + if(!TextureCoordinateTraits::HasArrayGlyphCache) + CORRADE_COMPARE_AS(renderer.vertexTextureCoordinates(), Containers::arrayView({ + {0.75f, 0.5f}, /* a, offset (3/4, 2/4) */ + {0.8125f, 0.5f}, + {0.75f, 0.625f}, + {0.8125f, 0.625f}, + + {0.75f, 0.75f}, /* b, offset (3/4, 3/4) */ + {0.875f, 0.75f}, + {0.75f, 0.875f}, + {0.875f, 0.875f}, + + {0.5f, 0.75f}, /* c, offset (2/4, 3/4) */ + {0.625f, 0.75f}, + {0.5f, 0.8125f}, + {0.625f, 0.8125f}, + }), TestSuite::Compare::Container); + else { + Float last = TextureCoordinateTraits::GlyphCacheArraySize - 1; + Float mid = TextureCoordinateTraits::GlyphCacheArraySize/2; + CORRADE_COMPARE_AS(renderer.vertexTextureArrayCoordinates(), Containers::arrayView({ + {0.75f, 0.5f, last}, /* a */ + {0.8125f, 0.5f, last}, + {0.75f, 0.625f, last}, + {0.8125f, 0.625f, last}, + + {0.75f, 0.75f, 0.0f}, /* b */ + {0.875f, 0.75f, 0.0f}, + {0.75f, 0.875f, 0.0f}, + {0.875f, 0.875f, 0.0f}, + + {0.5f, 0.75f, mid}, /* c */ + {0.625f, 0.75f, mid}, + {0.5f, 0.8125f, mid}, + {0.625f, 0.8125f, mid}, + }), 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 = + data.flags >= RendererFlag::GlyphPositionsClusters ? + renderer.glyphPositions().data() : nullptr; + const void* currentClusters = + data.flags >= RendererFlag::GlyphPositionsClusters ? + renderer.glyphClusters().data() : nullptr; + const void* currentRunScales = renderer.runScales().data(); + const void* currentRunEnds = renderer.runEnds().data(); + const void* currentVertexPositions = renderer.vertexPositions().data(); + const void* currentVertexTextureCoordinates = + TextureCoordinateTraits::HasArrayGlyphCache ? + renderer.vertexTextureArrayCoordinates().data() : + renderer.vertexTextureCoordinates().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.indexType(), IndexTraits::type()); + CORRADE_COMPARE(renderer.glyphCapacity(), data.expectedGlyphCapacity); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.expectedGlyphCapacity); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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_COMPARE(renderer.runCount(), 3); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 26); + CORRADE_COMPARE(renderer.renderingRunCount(), 3); + } + CORRADE_COMPARE(renderer.indexType(), IndexTraits::type()); + CORRADE_COMPARE(renderer.glyphCapacity(), 26); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 26); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 26); + CORRADE_COMPARE(renderer.runCapacity(), 3); + + /* If it shouldn't reallocate, the views should stay the same */ + if(data.expectNoGlyphReallocation) { + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + CORRADE_COMPARE(renderer.glyphPositions().data(), currentPositions); + CORRADE_COMPARE(renderer.glyphClusters().data(), currentClusters); + } + CORRADE_COMPARE(renderer.vertexPositions().data(), currentVertexPositions); + if(!TextureCoordinateTraits::HasArrayGlyphCache) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().data(), currentVertexTextureCoordinates); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().data(), currentVertexTextureCoordinates); + } + 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) { + CORRADE_COMPARE_AS(renderer.indices().prefix(5*6), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, /* Second part starts here */ + 16, 17, 18, 18, 17, 19 + }), TestSuite::Compare::Container); + /* 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) { + if(data.flags >= RendererFlag::GlyphPositionsClusters) + 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); + CORRADE_COMPARE_AS(renderer.vertexPositions().prefix(5*4), Containers::arrayView({ + {-25.25f, -0.5f}, + {-24.25f, -0.5f}, + {-25.25f, 1.5f}, + {-24.25f, 1.5f}, + + {-23.75f, -1.0f}, + {-21.75f, -1.0f}, + {-23.75f, 1.0f}, + {-21.75f, 1.0f}, + + {-22.25f, -0.5f}, + {-20.25f, -0.5f}, + {-22.25f, 0.5f}, + {-20.25f, 0.5f}, + + {-20.75f, -0.3333333f}, /* d, 1x2 times 1.333 */ + {-19.4166667f, -0.3333333f}, + {-20.75f, 2.3333333f}, + {-19.4166667f, 2.3333333f}, + + {-18.75f, -1.0f}, /* e, 2x2 times 1.333 */ + {-16.0833333f, -1.0f}, + {-18.75f, 1.6666667f}, + {-16.0833333f, 1.6666667f} + }), 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 { + if(data.flags >= RendererFlag::GlyphPositionsClusters) + 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.vertexPositions().prefix(5*4), Containers::arrayView({ + {-2.25f, -0.25f}, + {-1.25f, -0.25f}, + {-2.25f, 1.75f}, + {-1.25f, 1.75f}, + + {-0.75f, -0.75f}, + { 1.25f, -0.75f}, + {-0.75f, 1.25f}, + { 1.25f, 1.25f}, + + { 0.75f, -0.25f}, + { 2.75f, -0.25f}, + { 0.75f, 0.75f}, + { 2.75f, 0.75f}, + + {-23.0f, -0.3333333f}, /* d, 1x2 times 1.333 */ + {-21.6666667f, -0.3333333f}, + {-23.0f, 2.3333333f}, + {-21.6666667f, 2.3333333f}, + + {-21.0f, -1.0f}, /* e, 2x2 times 1.333 */ + {-18.3333333f, -1.0f}, + {-21.0f, 1.6666667f}, + {-18.3333333f, 1.6666667f} + }), TestSuite::Compare::Container); + } + if(data.flags >= RendererFlag::GlyphPositionsClusters) + 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); + if(!TextureCoordinateTraits::HasArrayGlyphCache) + CORRADE_COMPARE_AS(renderer.vertexTextureCoordinates().prefix(5*4), Containers::arrayView({ + {0.75f, 0.5f}, + {0.8125f, 0.5f}, + {0.75f, 0.625f}, + {0.8125f, 0.625f}, + + {0.75f, 0.75f}, + {0.875f, 0.75f}, + {0.75f, 0.875f}, + {0.875f, 0.875f}, + + {0.5f, 0.75f}, + {0.625f, 0.75f}, + {0.5f, 0.8125f}, + {0.625f, 0.8125f}, + + {0.75f, 0.5f}, /* d, offset (3/4, 2/4) */ + {0.8125f, 0.5f}, + {0.75f, 0.625f}, + {0.8125f, 0.625f}, + + {0.75f, 0.75f}, /* e, offset (3/4, 3/4) */ + {0.875f, 0.75f}, + {0.75f, 0.875f}, + {0.875f, 0.875f}, + }), TestSuite::Compare::Container); + else { + Float last = TextureCoordinateTraits::GlyphCacheArraySize - 1; + Float mid = TextureCoordinateTraits::GlyphCacheArraySize/2; + CORRADE_COMPARE_AS(renderer.vertexTextureArrayCoordinates().prefix(5*4), Containers::arrayView({ + {0.75f, 0.5f, last}, + {0.8125f, 0.5f, last}, + {0.75f, 0.625f, last}, + {0.8125f, 0.625f, last}, + + {0.75f, 0.75f, 0.0f}, + {0.875f, 0.75f, 0.0f}, + {0.75f, 0.875f, 0.0f}, + {0.875f, 0.875f, 0.0f}, + + {0.5f, 0.75f, mid}, + {0.625f, 0.75f, mid}, + {0.5f, 0.8125f, mid}, + {0.625f, 0.8125f, mid}, + + {0.75f, 0.5f, last}, + {0.8125f, 0.5f, last}, + {0.75f, 0.625f, last}, + {0.8125f, 0.625f, last}, + + {0.75f, 0.75f, 0.0f}, + {0.875f, 0.75f, 0.0f}, + {0.75f, 0.875f, 0.0f}, + {0.875f, 0.875f, 0.0f}, + }), TestSuite::Compare::Container); + } + } +} + +void RendererTest::allocateDifferentIndexType() { + auto&& data = AllocateDifferentIndexTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* See also allocateDifferentIndexType() for consequences of reserve() or + setIndexType() that don't depend on the allocator */ + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + Renderer renderer{glyphCache}; + + /* Initial index type and capacity from which the actual used type is + determined */ + if(data.indexTypeFirst) + renderer.setIndexType(*data.indexTypeFirst); + renderer.reserve(data.reserveFirst, 0); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserveFirst); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.reserveFirst); + CORRADE_COMPARE(renderer.indexType(), data.expectedIndexTypeFirst); + + /* Second reserve, index type change or clear which may change the type */ + if(data.reserveSecond) + renderer.reserve(data.reserveSecond, 0); + else if(data.indexTypeSecond) + renderer.setIndexType(*data.indexTypeSecond); + else if(data.clear) + renderer.clear(); + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + CORRADE_COMPARE(renderer.indexType(), data.expectedIndexTypeSecond); + CORRADE_COMPARE(renderer.glyphCapacity(), data.expectedCapacitySecond); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.expectedIndexCapacitySecond); + + /* Verify the index contents get updated if the operation changes the type. + Since it's all just reserve(), the indices() give back an empty array so + we have to fake the view. */ + if(renderer.indexType() == MeshIndexType::UnsignedByte) + CORRADE_COMPARE_AS(Containers::arrayView(renderer.indices().data(), 5*6), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, + 16, 17, 18, 18, 17, 19 + }), TestSuite::Compare::Container); + else if(renderer.indexType() == MeshIndexType::UnsignedShort) + CORRADE_COMPARE_AS(Containers::arrayView(renderer.indices().data(), 5*6), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, + 16, 17, 18, 18, 17, 19 + }), TestSuite::Compare::Container); + else if(renderer.indexType() == MeshIndexType::UnsignedInt) + CORRADE_COMPARE_AS(Containers::arrayView(renderer.indices().data(), 5*6), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11, + 12, 13, 14, 14, 13, 15, + 16, 17, 18, 18, 17, 19 + }), TestSuite::Compare::Container); + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); +} + +void RendererTest::allocateIndexAllocator() { + auto&& data = AllocateIndexAllocatorData[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 void* expectedData; + + UnsignedInt expectedViewSize; + UnsignedInt expectedSize; + + Containers::ArrayView indices; + int called = 0; + } allocation; + + Renderer renderer{glyphCache, nullptr, nullptr, nullptr, nullptr, [](void* state, UnsignedInt size, Containers::ArrayView& indices){ + Allocation& allocation = *static_cast(state); + CORRADE_COMPARE(size, allocation.expectedSize); + CORRADE_COMPARE(indices.data(), allocation.expectedData); + CORRADE_COMPARE(indices.size(), allocation.expectedViewSize); + + indices = allocation.indices; + ++allocation.called; + }, &allocation, nullptr, nullptr}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + /* Setting index type with no capacity yet should not call the allocator */ + if(data.indexType) { + renderer.setIndexType(*data.indexType); + CORRADE_COMPARE(renderer.indexType(), data.indexType); + CORRADE_COMPARE(allocation.called, 0); + } + + /* Initially it should pass all null views */ + allocation.expectedViewSize = 0; + allocation.expectedData = 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.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.indices().size()[0], 0); + CORRADE_COMPARE(renderer.indices().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.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.indices().size()[0], 0); + CORRADE_COMPARE(renderer.indices().data(), nullptr); + } + + /* Reserve an initial size to have somewhere to render to, pass each view + the same size. Using a heap allocation to not go over limited stack + sizes on Emscripten etc */ + Containers::Array indices{NoInit, 20000*6*4}; + allocation.expectedViewSize = 0; + allocation.expectedSize = data.reserve*6*meshIndexTypeSize(data.expectedIndexType); + allocation.indices = indices.prefix(data.reserve*6*meshIndexTypeSize(data.expectedIndexType)); + { + 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.glyphIndexCapacity(), data.reserve); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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.indices().size(), (Containers::Size2D{0, meshIndexTypeSize(data.expectedIndexType)})); + } else { + renderer.render(); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_COMPARE(renderer.indices().size(), (Containers::Size2D{3*6, meshIndexTypeSize(data.expectedIndexType)})); + } + CORRADE_COMPARE(allocation.called, 1); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserve); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.reserve); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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 view didn't + change since last time */ + CORRADE_COMPARE(renderer.indices().data(), indices); + } + + /* Reserve / render / set index type second time. Pass with a size that's + not a multiple of 6 times type size, it should round that down. */ + allocation.expectedData = indices; + /* Using a heap allocation to not go over limited stack sizes on + Emscripten etc */ + Containers::Array indices2{NoInit, 20000*6*4}; + allocation.indices = indices2.prefix(data.indicesSize); + /* Since the index buffer is populated at allocation time already, unless + the type changes, next time the size is excluding the previous + allocation regardless of whether render() was called */ + if(data.expectedSecondIndexType == data.expectedIndexType) { + allocation.expectedViewSize = 3*6*meshIndexTypeSize(data.expectedSecondIndexType); + allocation.expectedSize = (data.secondReserve - 3)*6*meshIndexTypeSize(data.expectedSecondIndexType); + } else { + allocation.expectedViewSize = 0; + allocation.expectedSize = data.expectedCapacity*6*meshIndexTypeSize(data.expectedSecondIndexType); + } + if(data.render) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + 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 if(data.secondReserve) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + 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); + } else if(data.secondIndexType) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + renderer.setIndexType(*data.secondIndexType); + 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); + } + /* The other two are using builtin allocators, which give back exactly what + requested */ + CORRADE_COMPARE(renderer.glyphCapacity(), data.expectedCapacity); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.expectedIndexCapacity); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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.indices().data(), indices); + } else { + CORRADE_COMPARE(allocation.called, 2); + CORRADE_COMPARE(renderer.indices().data(), indices2); + } +} + +void RendererTest::allocateIndexAllocatorInvalid() { + auto&& data = AllocateIndexAllocatorInvalidData[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 { + char indices[20*6*4]; + + UnsignedInt size; + } allocation; + /* For the initial render(). If index type is meant to be set later, count + just the default UnsignedByte indices. */ + allocation.size = 10*6*(data.setIndexType ? 1 : meshIndexTypeSize(data.indexType)); + + Renderer renderer{glyphCache, nullptr, nullptr, nullptr, nullptr, [](void* state, UnsignedInt, Containers::ArrayView& indices){ + Allocation& allocation = *static_cast(state); + + indices = Containers::arrayView(allocation.indices).prefix(allocation.size); + }, &allocation, nullptr, nullptr}; + + /* Set index type initially if not meant to be updated later */ + if(!data.setIndexType) + renderer.setIndexType(data.indexType); + + /* 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); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 10); + + /* Next reserve / setIndexType / render should be with these */ + allocation.size = data.size; + { + Containers::String out; + Error redirectError{&out}; + if(data.render) + renderer.render(shaper, 0.0f, "klmnopq"); + else if(data.setIndexType) + renderer.setIndexType(data.indexType); + 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. Note that, + compared to RendererCore::render(), the above passed partially with the + extra glyphs, so we now need 19 instead of 17. */ + allocation.size = 19*6*meshIndexTypeSize(data.indexType); + if(data.render) { + renderer.render(shaper, 0.0f, "rs"); + CORRADE_COMPARE(renderer.glyphCount(), 19); + } else { + renderer.reserve(19, 0); + CORRADE_COMPARE(renderer.glyphCount(), 10); + } + CORRADE_COMPARE(renderer.glyphCapacity(), 19); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 19); +} + +void RendererTest::allocateIndexAllocatorMaxIndexCountForType() { + auto&& data = AllocateIndexAllocatorMaxIndexCountForTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; + + Containers::Array indices{NoInit, 100*1024*2}; + Renderer renderer{glyphCache, nullptr, nullptr, nullptr, nullptr, [](void* state, UnsignedInt, Containers::ArrayView& indices){ + indices = *static_cast*>(state); + }, &indices, nullptr, nullptr}; + renderer.setIndexType(data.indexType); + + renderer.reserve(1, 0); + CORRADE_COMPARE(renderer.glyphCapacity(), 1); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.expected); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 1); +} + +void RendererTest::allocateVertexAllocator() { + auto&& data = AllocateVertexAllocatorData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, data.glyphCacheArraySize}}; + + 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 Vector2* expectedVertexPositionData; + const void* expectedVertexTextureCoordinateData; + + UnsignedInt expectedViewSize; + UnsignedInt expectedVertexCount; + + Containers::StridedArrayView1D vertexPositions; + Containers::StridedArrayView1D vertexTextureCoordinates; + int called = 0; + } allocation; + + Renderer renderer{glyphCache, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, [](void* state, UnsignedInt vertexCount, Containers::StridedArrayView1D& vertexPositions, Containers::StridedArrayView1D& vertexTextureCoordinates){ + Allocation& allocation = *static_cast(state); + CORRADE_COMPARE(vertexCount, allocation.expectedVertexCount); + CORRADE_COMPARE(vertexPositions.data(), allocation.expectedVertexPositionData); + CORRADE_COMPARE(vertexPositions.size(), allocation.expectedViewSize); + CORRADE_COMPARE(vertexTextureCoordinates.data(), allocation.expectedVertexTextureCoordinateData); + CORRADE_COMPARE(vertexTextureCoordinates.size(), allocation.expectedViewSize); + + vertexPositions = allocation.vertexPositions; + vertexTextureCoordinates = allocation.vertexTextureCoordinates; + ++allocation.called; + }, &allocation}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + /* Initially it should pass all null views */ + allocation.expectedViewSize = 0; + allocation.expectedVertexPositionData = nullptr; + allocation.expectedVertexTextureCoordinateData = 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.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.vertexPositions().size(), 0); + CORRADE_COMPARE(renderer.vertexPositions().data(), nullptr); + if(data.glyphCacheArraySize == 1) { + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + CORRADE_COMPARE(renderer.vertexTextureCoordinates().data(), nullptr); + } else { + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 0); + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().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.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); + CORRADE_COMPARE(renderer.runCount(), 0); + CORRADE_COMPARE(renderer.runCapacity(), 0); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); + CORRADE_COMPARE(renderer.renderingRunCount(), 0); + CORRADE_COMPARE(renderer.vertexPositions().size(), 0); + CORRADE_COMPARE(renderer.vertexPositions().data(), nullptr); + if(data.glyphCacheArraySize == 1) { + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + CORRADE_COMPARE(renderer.vertexTextureCoordinates().data(), nullptr); + } else { + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 0); + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().data(), nullptr); + } + } + + /* Reserve an initial size to have somewhere to render to, pass each view + the same size */ + Vector2 vertexPositions[32*4]; + Vector3 vertexTextureCoordinates[32*4]; + allocation.expectedViewSize = 0; + allocation.expectedVertexCount = data.reserve*4; + allocation.vertexPositions = Containers::arrayView(vertexPositions) + .prefix(data.reserve*4); + allocation.vertexTextureCoordinates = Containers::stridedArrayView(vertexTextureCoordinates) + .prefix(data.reserve*4) + .slice(&Vector3::xy); + { + 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.glyphIndexCapacity(), data.reserve); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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.vertexPositions().size(), 0); + if(data.glyphCacheArraySize == 1) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 0); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 0); + } else { + renderer.render(); + CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphCount(), 3); + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_COMPARE(renderer.vertexPositions().size(), 3*4); + if(data.glyphCacheArraySize == 1) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().size(), 3*4); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().size(), 3*4); + } + CORRADE_COMPARE(allocation.called, 1); + CORRADE_COMPARE(renderer.glyphCapacity(), data.reserve); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.reserve); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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.vertexPositions().data(), vertexPositions); + if(data.glyphCacheArraySize == 1) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().data(), vertexTextureCoordinates); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().data(), vertexTextureCoordinates); + } + + /* Reserve / render second time. Pass each with a different size, it should + pick the smallest as capacity, and with a size that's not a multiple of + 4, it should round that down. */ + allocation.expectedVertexPositionData = vertexPositions; + allocation.expectedVertexTextureCoordinateData = vertexTextureCoordinates; + Vector2 vertexPositions2[32*4]; + Vector3 vertexTextureCoordinates2[32*4]; + allocation.vertexPositions = Containers::arrayView(vertexPositions2) + .prefix(data.positionSize); + allocation.vertexTextureCoordinates = Containers::stridedArrayView(vertexTextureCoordinates2) + .prefix(data.textureCoordinateSize) + .slice(&Vector3::xy); + if(data.render) { + CORRADE_ITERATION(Utility::format("{}:{}", __FILE__, __LINE__)); + /* If only add() was called before, there are no vertex data to + preserve from previous allocations */ + if(data.renderAddOnly) { + allocation.expectedViewSize = 0; + allocation.expectedVertexCount = data.secondReserve*4; + } else { + allocation.expectedViewSize = 3*4; + allocation.expectedVertexCount = (data.secondReserve - 3)*4; + } + 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.expectedVertexCount = data.secondReserve*4; + 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); + } + /* The other two are using builtin allocators, which give back exactly what + requested */ + CORRADE_COMPARE(renderer.glyphCapacity(), 26); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 26); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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.vertexPositions().data(), vertexPositions); + if(data.glyphCacheArraySize == 1) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().data(), vertexTextureCoordinates); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().data(), vertexTextureCoordinates); + } else { + CORRADE_COMPARE(allocation.called, 2); + CORRADE_COMPARE(renderer.vertexPositions().data(), vertexPositions2); + if(data.glyphCacheArraySize == 1) + CORRADE_COMPARE(renderer.vertexTextureCoordinates().data(), vertexTextureCoordinates2); + else + CORRADE_COMPARE(renderer.vertexTextureArrayCoordinates().data(), vertexTextureCoordinates2); + } +} + +void RendererTest::allocateVertexAllocatorInvalid() { + auto&& data = AllocateVertexAllocatorInvalidData[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 vertexPositions[20*4]; + Vector2 vertexTextureCoordinates[20*4]; + + /* For the initial render() */ + UnsignedInt vertexPositionSize = 10*4; + UnsignedInt vertexTextureCoordinateSize = 10*4; + } allocation; + + Renderer renderer{glyphCache, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, [](void* state, UnsignedInt, Containers::StridedArrayView1D& vertexPositions, Containers::StridedArrayView1D& vertexTextureCoordinates){ + Allocation& allocation = *static_cast(state); + + vertexPositions = Containers::arrayView(allocation.vertexPositions).prefix(allocation.vertexPositionSize); + vertexTextureCoordinates = Containers::arrayView(allocation.vertexTextureCoordinates).prefix(allocation.vertexTextureCoordinateSize); + }, &allocation}; + + /* 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); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 10); + + /* Next reserve / render should be with these */ + allocation.vertexPositionSize = data.positionSize; + allocation.vertexTextureCoordinateSize = data.textureCoordinateSize; + { + 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. Note that, + compared to RendererCore::render(), the above passed partially with the + extra glyphs, so we now need 19 instead of 17. */ + allocation.vertexPositionSize = 19*4; + allocation.vertexTextureCoordinateSize = 19*4; + if(data.render) { + renderer.render(shaper, 0.0f, "rs"); + CORRADE_COMPARE(renderer.glyphCount(), 19); + } else { + renderer.reserve(19, 0); + CORRADE_COMPARE(renderer.glyphCount(), 10); + } + CORRADE_COMPARE(renderer.glyphCapacity(), 19); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 19); +} + +void RendererTest::allocateVertexAllocatorNotEnoughStrideForArrayGlyphCache() { + auto&& data = AllocateVertexAllocatorNotEnoughStrideForArrayGlyphCacheData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16, 5}}; + + 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 vertexPositions[11*4]; /* large enough also for the rest */ + Containers::StridedArrayView1D vertexTextureCoordinates; + } allocation; + + Vector2 vertexTextureCoordinates[5*4]; + if(data.flipped) + allocation.vertexTextureCoordinates = Containers::stridedArrayView(vertexTextureCoordinates).flipped<0>(); + else + allocation.vertexTextureCoordinates = vertexTextureCoordinates; + + Renderer renderer{glyphCache, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, [](void* state, UnsignedInt, Containers::StridedArrayView1D& vertexPositions, Containers::StridedArrayView1D& vertexTextureCoordinates){ + Allocation& allocation = *static_cast(state); + + vertexPositions = allocation.vertexPositions; + vertexTextureCoordinates = allocation.vertexTextureCoordinates; + }, &allocation}; + + { + Containers::String out; + Error redirectError{&out}; + if(data.render) { + renderer.render(shaper, 0.0f, "abcde"); + } else { + renderer.reserve(5, 0); + } + CORRADE_COMPARE_AS(out, data.expected, + TestSuite::Compare::String); + } + + /* Just to verify it's okay when the stride is exactly enough */ + Vector3 vertexTextureArrayCoordinates[8*4]; + allocation.vertexTextureCoordinates = Containers::stridedArrayView(vertexTextureArrayCoordinates).slice(&Vector3::xy); + if(data.render) { + renderer.render(shaper, 0.0f, "fgh"); + } else { + renderer.reserve(8, 0); + } + CORRADE_COMPARE(renderer.glyphCapacity(), 8); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 8); + + /* And flipped stride as well */ + Vector3 vertexTextureArrayCoordinates2[11*4]; + allocation.vertexTextureCoordinates = Containers::stridedArrayView(vertexTextureArrayCoordinates2).slice(&Vector3::xy).flipped<0>(); + if(data.render) { + renderer.render(shaper, 0.0f, "ijk"); + } else { + renderer.reserve(11, 0); + } + CORRADE_COMPARE(renderer.glyphCapacity(), 11); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 11); +} + +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()); @@ -3529,24 +6047,26 @@ void RendererTest::addSingleLine() { 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}) + 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 - 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 */ + {-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), @@ -3554,11 +6074,13 @@ void RendererTest::addSingleLine() { 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 + /* 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({ - 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 + /* 5, 8, 9 is a \n */ + 3, 3, 4, 4, 6, 6, 7, 7, 10, 10 }), TestSuite::Compare::Container); } CORRADE_COMPARE_AS(renderer.runScales(), @@ -3569,48 +6091,147 @@ void RendererTest::addSingleLine() { TestSuite::Compare::Container); } -void RendererTest::addSingleLineAlign() { - auto&& data = AddSingleLineAlignData[testCaseInstanceId()]; - setTestCaseDescription(data.name); +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(); - TestFont font; - font.openFile({}, 0.5f); - DummyGlyphCache glyphCache = testGlyphCache(font); - TestShaper shaper{font, data.shapeDirection}; + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; - RendererCore renderer{glyphCache}; - renderer.setAlignment(data.alignment); + GlyphCacheFeatures doFeatures() const override { return {}; } + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; - /* 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})); + struct: AbstractFont { + FontFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} - 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, + 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); - /* 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, + struct: AbstractShaper { + using AbstractShaper::AbstractShaper; - /* 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); + 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::addMultipleLines() { - auto&& data = AddMultipleLinesData[testCaseInstanceId()]; +void RendererTest::multipleBlocks() { + auto&& data = MultipleBlocksData[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; @@ -3625,246 +6246,200 @@ void RendererTest::addMultipleLines() { 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}; + return {size, 2.0f*size, -1.0f*size, 4.0f*size, 0}; } - void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + 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 {}; } - private: - bool _opened = false; + 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); + UnsignedInt fontId = glyphCache.addFont('l' + 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 */ + 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 Shaper: AbstractShaper { + struct: 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; + 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 { - /* 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'); - } + 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 { - /* 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}; + 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] = currentBegin + i/2; + clusters[i] = _begin + i; } + ShapeDirection doDirection() const override { - return _direction; + return direction; } - ShapeDirection shapeDirectionToAdvertise; - UnsignedInt advertiseShapeDirectionAt; - - const char* expectedText; - UnsignedInt currentBegin; - Containers::Array> calls; + ShapeDirection direction; 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; + Containers::StringView _text; + UnsignedInt _begin; } 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(); - } + /* 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); - /* 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); + /* 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); - /* 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())})); + /* 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); - /* The contents should be the same independently of how many pieces was - added. All glyph positions are shifted based on the cursor. */ + /* Glyph data of previous blocks shouldn't get corrupted by new renders */ 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 */ + {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); - 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()]; +template void RendererTest::indicesVertices() { + auto&& data = IndicesVerticesData[testCaseInstanceId()]; setTestCaseDescription(data.name); + setTestCaseTemplateName(Math::TypeTraits::name()); + + /* Verifies various corner cases related to index and vertex data + population, except for allocator behavior which is tested in allocate(), + allocateIndexAllocator() and allocateVertexAllocator() already */ + + 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, data.glyphCacheArraySize}, {}}; struct: AbstractFont { FontFeatures doFeatures() const override { return {}; } @@ -3873,91 +6448,253 @@ void RendererTest::addMultipleLinesAlign() { 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}; + /* 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.0f*size, -1.0f*size, 10000.0f, 0}; } - void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D& glyphs) override { - for(UnsignedInt& i: glyphs) - i = 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 {}; } - bool _opened = false; - } font; - font.openFile({}, 0.5f); + private: + bool _opened = false; + } font1, font2; + /* The same font open twice with a different size, and the same glyphs + being in different places */ + font1.openFile("", 1.0f); + font2.openFile("", 0.5f); + UnsignedInt font1Id = glyphCache.addFont(5, &font1); + UnsignedInt font2Id = glyphCache.addFont(5, &font2); + /* Glyphs, in shuffled order to not have their IDs match the clusters, + deliberately with glyph offsets to verify those get correctly used as + well */ + glyphCache.addGlyph(font1Id, 4, {0, 1}, /* c, h */ + data.glyphCacheArraySize/2, + Range2Di::fromSize({8, 12}, {2, 1})); + glyphCache.addGlyph(font1Id, 0, {2, 0}, /* a, f */ + data.glyphCacheArraySize - 1, + Range2Di::fromSize({12, 8}, {1, 2})); + glyphCache.addGlyph(font1Id, 2, {0, 2}, /* b, g */ + 0, + Range2Di::fromSize({12, 12}, {2, 2})); + glyphCache.addGlyph(font2Id, 2, {-1, 0}, /* e */ + data.glyphCacheArraySize*3/4, + Range2Di::fromSize({8, 8}, {1, 1})); + glyphCache.addGlyph(font2Id, 0, {-1, -1}, /* d */ + data.glyphCacheArraySize - 1, + Range2Di::fromSize({4, 8}, {2, 1})); struct: AbstractShaper { using AbstractShaper::AbstractShaper; UnsignedInt doShape(Containers::StringView, UnsignedInt begin, UnsignedInt end, Containers::ArrayView) override { + _begin = begin; return end - begin; } void doGlyphIdsInto(const Containers::StridedArrayView1D& ids) const override { for(UnsignedInt i = 0; i != ids.size(); ++i) - ids[i] = 0; + ids[i] = i*2; } 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); + advances[i] = {3.0f*font().size(), 0.0f}; + /* Every third is moved -4 on X, every odd 0.5 on Y */ + offsets[i] = {i % 3 ? 0.0f : -4.0f*font().size(), + i % 2 ? 0.5f*font().size() : 0.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); + void doGlyphClustersInto(const Containers::StridedArrayView1D& clusters) const override { + for(UnsignedInt i = 0; i != clusters.size(); ++i) + /* Just to have something non-trivial in the output */ + clusters[i] = 10*_begin + i; + } - /* 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})); + private: + UnsignedInt _begin; + } shaper1{font1}, shaper2{font2}; - /* Vertices - [a] [b] [c] [d] - [e] [f] + struct Allocation { + struct Glyph { + Vector2 position; + Vector2 advance; + UnsignedInt id; + UnsignedInt cluster; + } glyphs[8]; + } allocation; - [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 */ + /* Verify that the vertex allocator doesn't assume the memory was already + allocated by a builtin glyph allocation if a custom one is used. There + could be + for the lambda to turn it into a function pointer to avoid ?: + getting confused, but that doesn't work on MSVC 2015 so instead it's the + nullptr being cast. */ + Renderer renderer{glyphCache, + data.customGlyphAllocator ? [](void* state, UnsignedInt, Containers::StridedArrayView1D& glyphPositions, Containers::StridedArrayView1D& glyphIds, Containers::StridedArrayView1D* glyphClusters, Containers::StridedArrayView1D& glyphAdvances){ + Allocation& allocation = *static_cast(state); + + glyphPositions = Containers::stridedArrayView(allocation.glyphs).slice(&Allocation::Glyph::position); + glyphIds = Containers::stridedArrayView(allocation.glyphs).slice(&Allocation::Glyph::id); + if(glyphClusters) + *glyphClusters = Containers::stridedArrayView(allocation.glyphs).slice(&Allocation::Glyph::cluster); + glyphAdvances = Containers::stridedArrayView(allocation.glyphs).slice(&Allocation::Glyph::advance); + } : static_cast&, Containers::StridedArrayView1D&, Containers::StridedArrayView1D*, Containers::StridedArrayView1D&)>(nullptr), &allocation, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, data.flags}; + + /* Attempt to preserve part or all to verify it doesn't cause any strange + subsequent data corruption */ + if(data.reserve) + renderer.reserve(data.reserve, 0); - Vector2{0.0f, 0.0f} + data.offset1, /* e */ - Vector2{2.0f, 0.0f} + data.offset1, /* f */ + renderer + /* Alignment tested sufficiently elsewhere, opt for simplicity here. + Same with newlines and such, no need to further complicate this. */ + .setAlignment(Alignment::LineLeft) + .setIndexType(IndexTraits::type()) + /* Using different overloads to add pieces with different font and + scale combinations */ + .add(shaper1, 0.5f, "__abc_", 2, 5) /* scale is 0.5 */ + .add(shaper2, 2.0f, "de", {}) /* scale is 4.0 */ + .add(shaper1, 1.0f, "___fgh__", 3, 6, {}); /* scale is 1.0 */ + CORRADE_COMPARE(renderer.render(), Containers::pair( + /* The ascent / descent is (2, -1) and max scaling is 4*0.5 */ + Range2D{{0.0f, -2.0f}, {25.5f, 4.0f}}, Range1Dui{0, 3})); + CORRADE_COMPARE(renderer.glyphCount(), 8); + + /* There should be no surprises for runs, just verifying that these match + expectations */ + CORRADE_COMPARE_AS(renderer.runScales(), Containers::arrayView({ + 0.5f, 4.0f, 1.0f + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.runEnds(), Containers::arrayView({ + 3u, 5u, 8u + }), TestSuite::Compare::Container); - /* Two linebreaks here */ + /* If enabled, these shouldn't get corrupted when vertex data get + generated, no matter how the allocation is done */ + if(data.flags >= RendererFlag::GlyphPositionsClusters) { + CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView({ + /* posi shaper shaper font shape + tion offset offset size scale */ + { 0.0f - 2.0f, 0.0f}, /* a, *0.5 */ + { 1.5f, 0.25f}, /* b, *0.5 */ + { 3.0f, 0.0f}, /* c, *0.5 */ + + { 4.5f - 8.0f, 0.0f}, /* d, *0.5 *4.0 */ + {10.5f, 1.0f}, /* e, *0.5 *4.0 */ + + {16.5f - 4.0f, 0.0f}, /* f, *1.0 */ + {19.5f, 0.5f}, /* g, *1.0 */ + {22.5f, 0.0f}, /* h, *1.0 */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({ + 20u, 21u, 22u, + 0u, 1u, + 30u, 31u, 32u + }), TestSuite::Compare::Container); + } - Vector2{0.0f, 0.0f} + data.offset2, /* g */ - Vector2{2.0f, 0.0f} + data.offset2, /* h */ - Vector2{4.0f, 0.0f} + data.offset2, /* i */ + CORRADE_COMPARE_AS(renderer.vertexPositions(), Containers::arrayView({ + /* posi cache shaper posi cache shaper + tion offset offset tion offset offset */ + { 0.0f + 1.0f - 2.0f, 0.0f}, /* a, 1x2, +(2, 0), *0.5 */ + { 0.5f + 1.0f - 2.0f, 0.0f}, + { 0.0f + 1.0f - 2.0f, 1.0f}, + { 0.5f + 1.0f - 2.0f, 1.0f}, + + { 1.5f, 0.0f + 1.0f + 0.25f}, /* b, 2x2, +(0, 2), *0.5 */ + { 2.5f, 0.0f + 1.0f + 0.25f}, + { 1.5f, 1.0f + 1.0f + 0.25f}, + { 2.5f, 1.0f + 1.0f + 0.25f}, + + { 3.0f, 0.0f + 0.5f}, /* c, 2x1, +(0, 1), *0.5 */ + { 4.0f, 0.0f + 0.5f}, + { 3.0f, 0.5f + 0.5f}, + { 4.0f, 0.5f + 0.5f}, + + { 4.5f - 4.0f - 8.0f, 0.0f - 4.0f}, /* d, 2x1, -(1, 1), *0.5*4.0 */ + {12.5f - 4.0f - 8.0f, 0.0f - 4.0f}, + { 4.5f - 4.0f - 8.0f, 4.0f - 4.0f}, + {12.5f - 4.0f - 8.0f, 4.0f - 4.0f}, + + {10.5f - 4.0f, 0.0f + 1.0f}, /* e, 1x1, -(1, 0), *0.5*4.0 */ + {14.5f - 4.0f, 0.0f + 1.0f}, + {10.5f - 4.0f, 4.0f + 1.0f}, + {14.5f - 4.0f, 4.0f + 1.0f}, + + {16.5f + 2.0f - 4.0f, 0.0f}, /* f, 1x2, +(2, 0), *1.0 */ + {17.5f + 2.0f - 4.0f, 0.0f}, + {16.5f + 2.0f - 4.0f, 2.0f}, + {17.5f + 2.0f - 4.0f, 2.0f}, + + {19.5f, 0.0f + 2.0f + 0.5f}, /* g, 2x2, +(0, 2), *1.0 */ + {21.5f, 0.0f + 2.0f + 0.5f}, + {19.5f, 2.0f + 2.0f + 0.5f}, + {21.5f, 2.0f + 2.0f + 0.5f}, + + {22.5f, 0.0f + 1.0f}, /* h, 2x1, +(0, 1), *1.0 */ + {24.5f, 0.0f + 1.0f}, + {22.5f, 1.0f + 1.0f}, + {24.5f, 1.0f + 1.0f}, }), TestSuite::Compare::Container); + + Vector3 expectedTextureCoordinates[]{ + {0.75f, 0.5f, Float(data.glyphCacheArraySize - 1)}, /* a */ + {0.8125f, 0.5f, Float(data.glyphCacheArraySize - 1)}, + {0.75f, 0.625f, Float(data.glyphCacheArraySize - 1)}, + {0.8125f, 0.625f, Float(data.glyphCacheArraySize - 1)}, + + {0.75f, 0.75f, 0.0f}, /* b */ + {0.875f, 0.75f, 0.0f}, + {0.75f, 0.875f, 0.0f}, + {0.875f, 0.875f, 0.0f}, + + {0.5f, 0.75f, Float(data.glyphCacheArraySize/2)}, /* c */ + {0.625f, 0.75f, Float(data.glyphCacheArraySize/2)}, + {0.5f, 0.8125f, Float(data.glyphCacheArraySize/2)}, + {0.625f, 0.8125f, Float(data.glyphCacheArraySize/2)}, + + {0.25f, 0.5f, Float(data.glyphCacheArraySize - 1)}, /* d */ + {0.375f, 0.5f, Float(data.glyphCacheArraySize - 1)}, + {0.25f, 0.5625f, Float(data.glyphCacheArraySize - 1)}, + {0.375f, 0.5625f, Float(data.glyphCacheArraySize - 1)}, + + {0.5f, 0.5f, Float(data.glyphCacheArraySize*3/4)}, /* e */ + {0.5625f, 0.5f, Float(data.glyphCacheArraySize*3/4)}, + {0.5f, 0.5625f, Float(data.glyphCacheArraySize*3/4)}, + {0.5625f, 0.5625f, Float(data.glyphCacheArraySize*3/4)}, + + {0.75f, 0.5f, Float(data.glyphCacheArraySize - 1)}, /* f (a) */ + {0.8125f, 0.5f, Float(data.glyphCacheArraySize - 1)}, + {0.75f, 0.625f, Float(data.glyphCacheArraySize - 1)}, + {0.8125f, 0.625f, Float(data.glyphCacheArraySize - 1)}, + + {0.75f, 0.75f, 0.0f}, /* g (b) */ + {0.875f, 0.75f, 0.0f}, + {0.75f, 0.875f, 0.0f}, + {0.875f, 0.875f, 0.0f}, + + {0.5f, 0.75f, Float(data.glyphCacheArraySize/2)}, /* h (c) */ + {0.625f, 0.75f, Float(data.glyphCacheArraySize/2)}, + {0.5f, 0.8125f, Float(data.glyphCacheArraySize/2)}, + {0.625f, 0.8125f, Float(data.glyphCacheArraySize/2)}, + }; + if(data.glyphCacheArraySize == 1) CORRADE_COMPARE_AS( + renderer.vertexTextureCoordinates(), + Containers::stridedArrayView(expectedTextureCoordinates).slice(&Vector3::xy), + TestSuite::Compare::Container); + else CORRADE_COMPARE_AS( + renderer.vertexTextureArrayCoordinates(), + Containers::stridedArrayView(expectedTextureCoordinates), + TestSuite::Compare::Container); } -void RendererTest::addFontNotFoundInCache() { - CORRADE_SKIP_IF_NO_ASSERT(); +void RendererTest::clearResetCore() { + auto&& data = ClearResetCoreData[testCaseInstanceId()]; + setTestCaseDescription(data.name); struct: AbstractGlyphCache { using AbstractGlyphCache::AbstractGlyphCache; @@ -3967,48 +6704,184 @@ void RendererTest::addFontNotFoundInCache() { struct: AbstractFont { FontFeatures doFeatures() const override { return {}; } - bool doIsOpened() const override { return true; } - void doClose() override {} + 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 {}; } - } font1, font2, font3; - glyphCache.addFont(0, &font1); - /* font2 not */ - glyphCache.addFont(0, &font3); + + private: + bool _opened = false; + } font; + font.openFile("", 1.0f); + glyphCache.addFont(1, &font); struct: AbstractShaper { using AbstractShaper::AbstractShaper; - UnsignedInt doShape(Containers::StringView, UnsignedInt, UnsignedInt, Containers::ArrayView) override { - return 0; + UnsignedInt doShape(Containers::StringView, UnsignedInt begin, UnsignedInt end, Containers::ArrayView) override { + return end - begin; } - 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}; + 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; + } - RendererCore renderer{glyphCache}; + ShapeDirection direction = ShapeDirection::Unspecified; + } shaper{font}; - 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"); + 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::multipleBlocks() { - auto&& data = MultipleBlocksData[testCaseInstanceId()]; +void RendererTest::clearResetCoreAllocators() { + auto&& data = ClearResetCoreData[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}, {}}; + } glyphCache{PixelFormat::R8Unorm, {16, 16}}; struct: AbstractFont { FontFeatures doFeatures() const override { return {}; } @@ -4017,187 +6890,151 @@ void RendererTest::multipleBlocks() { Properties doOpenFile(Containers::StringView, Float size) override { _opened = true; - return {size, 2.0f*size, -1.0f*size, 4.0f*size, 0}; + /* 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& glyphs) override { - for(UnsignedInt& i: glyphs) - i = 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 {}; } - 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 */ - } + private: + bool _opened = false; + } font; + font.openFile("", 1.0f); + glyphCache.addFont(1, &font); struct: AbstractShaper { using AbstractShaper::AbstractShaper; - UnsignedInt doShape(Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView) override { - _text = text; - _begin = begin; - return end - begin; + 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] = _text[_begin + i]; + /* 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] = {}; - 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; + void doGlyphClustersInto(const Containers::StridedArrayView1D&) const override { + /* The data don't matter in this case */ } + } shaper{font}; - ShapeDirection doDirection() const override { - return direction; - } + struct Allocation { + Vector2 glyphPositions[20]; + UnsignedInt glyphIds[18]; /* deliberately smaller */ + UnsignedInt glyphClusters[20]; + Vector2 glyphAdvances[20]; - ShapeDirection direction; + Float runScales[4]; + UnsignedInt runEnds[3]; /* deliberately smaller */ - private: - Containers::StringView _text; - UnsignedInt _begin; - } shaper1{font1}, shaper2{font2}; + 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); - RendererCore renderer{glyphCache, data.flags}; + 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); - /* 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); + 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); - 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 */ + /* 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); - {-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 */ + /* 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); - {-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); + /* Other resetting behavior is sufficiently tested by clearResetCore() + already */ } -void RendererTest::clearResetCore() { - auto&& data = ClearResetCoreData[testCaseInstanceId()]; +void RendererTest::clearReset() { + auto&& data = ClearResetData[testCaseInstanceId()]; setTestCaseDescription(data.name); struct: AbstractGlyphCache { @@ -4253,14 +7090,9 @@ void RendererTest::clearResetCore() { 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}; + Renderer renderer{glyphCache, data.flags}; /* Clearing right from the initial state should be a no-op */ if(data.reset) @@ -4268,6 +7100,8 @@ void RendererTest::clearResetCore() { else renderer.clear(); CORRADE_COMPARE(renderer.glyphCapacity(), 0); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); CORRADE_COMPARE(renderer.glyphCount(), 0); CORRADE_COMPARE(renderer.runCapacity(), 0); CORRADE_COMPARE(renderer.runCount(), 0); @@ -4275,31 +7109,38 @@ void RendererTest::clearResetCore() { CORRADE_COMPARE(renderer.renderingGlyphCount(), 0); CORRADE_COMPARE(renderer.renderingRunCount(), 0); - shaper.direction = ShapeDirection::RightToLeft; + /* Set a non-default index type to verify it doesn't get reset with + reset(). All other cases of index type change after clear() are + otherwise tested in allocateDifferentIndexType() already. */ + renderer.setIndexType(MeshIndexType::UnsignedShort); + + /* Fill the renderer with something */ renderer - .setAlignment(Alignment::LineEnd) .setCursor({100.0f, 50.0f}) - .setLineAdvance(30.0f) .add(shaper, 1.0f, "ab\nc"); + CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); + CORRADE_COMPARE(renderer.renderingRunCount(), 1); if(data.renderAddOnly) { CORRADE_VERIFY(renderer.isRendering()); + /* Index and vertex buffers are allocated only when render() is + called */ + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); CORRADE_COMPARE(renderer.glyphCount(), 0); CORRADE_COMPARE(renderer.runCount(), 0); } else { renderer.render(); CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 3); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 3); 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 + /* Verify initial index values to be sure that the offset doesn't leak + to after clear() */ + CORRADE_COMPARE_AS(renderer.indices(), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11 }), TestSuite::Compare::Container); } CORRADE_COMPARE(renderer.glyphCapacity(), 3); @@ -4307,78 +7148,60 @@ void RendererTest::clearResetCore() { 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. */ + /* Reset should behave like RendererCore, plus resetting also the index / + vertex state */ if(data.reset) renderer.reset(); else renderer.clear(); - CORRADE_COMPARE(renderer.glyphCapacity(), data.expectedBuiltinGlyphAllocatorCapacity); + CORRADE_COMPARE(renderer.glyphCapacity(), 3); + /* Index and vertex buffers are allocated only when render() is called. For + the builtin allocator however, if glyph positions and clusters aren't + needed, the vertex and glyph data share the same allocation and thus get + allocated in add() already. */ + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.renderAddOnly ? 0 : 3); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), data.renderAddOnly && data.flags >= RendererFlag::GlyphPositionsClusters ? 0 : 3); 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); } + CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedShort); - /* 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; + /* Rendering should work the same way after a clear / reset. The Renderer + wrappers delegate to RendererCore, which is tested in clearResetCore() + already, so just verify that the extra state isn't leaking in any + way. */ renderer - .setAlignment(Alignment::LineBegin) + /* Using the same alignment as in clearReserCore() to have the same + output rect */ + .setAlignment(Alignment::LineRight) .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.glyphIndexCapacity(), 3); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 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 + CORRADE_COMPARE(renderer.runCount(), 1); + CORRADE_COMPARE_AS(renderer.indices(), Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6, 6, 5, 7, + 8, 9, 10, 10, 9, 11 }), TestSuite::Compare::Container); } -void RendererTest::clearResetCoreAllocators() { - auto&& data = ClearResetCoreData[testCaseInstanceId()]; +void RendererTest::clearResetAllocators() { + auto&& data = ClearResetData[testCaseInstanceId()]; setTestCaseDescription(data.name); struct: AbstractGlyphCache { @@ -4443,14 +7266,25 @@ void RendererTest::clearResetCoreAllocators() { Float runScales[4]; UnsignedInt runEnds[3]; /* deliberately smaller */ - UnsignedInt expectedGlyphCount, expectedRunCount; - int glyphCalled = 0, runCalled = 0; + char indices[22*6*2 + 9]; /* deliberately not a multiple of 6 shorts */ + + Vector2 vertexPositions[20*4]; + Vector2 vertexTextureCoordinates[19*4 + 2]; /* deliberately smaller, gets rounded to multiple of 4 */ + + UnsignedInt expectedGlyphCount, + expectedRunCount, + expectedIndexSize, + expectedVertexCount; + int glyphCalled = 0, + runCalled = 0, + indexCalled = 0, + vertexCalled = 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){ + Renderer 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); @@ -4474,20 +7308,51 @@ void RendererTest::clearResetCoreAllocators() { runScales = allocation.runScales; runEnds = allocation.runEnds; ++allocation.runCalled; + }, &allocation, [](void* state, UnsignedInt size, Containers::ArrayView& indices) { + Allocation& allocation = *static_cast(state); + CORRADE_COMPARE(size, allocation.expectedIndexSize); + CORRADE_COMPARE(indices.size(), 0); + + indices = allocation.indices; + ++allocation.indexCalled; + }, &allocation, [](void* state, UnsignedInt vertexCount, Containers::StridedArrayView1D& vertexPositions, Containers::StridedArrayView1D& vertexTextureCoordinates) { + Allocation& allocation = *static_cast(state); + CORRADE_COMPARE(vertexCount, allocation.expectedVertexCount); + CORRADE_COMPARE(vertexPositions.size(), 0); + CORRADE_COMPARE(vertexTextureCoordinates.size(), 0); + + vertexPositions = allocation.vertexPositions; + vertexTextureCoordinates = allocation.vertexTextureCoordinates; + ++allocation.vertexCalled; }, &allocation, data.flags}; + /* Set an index type that isn't just 1-byte to verify there are no + calculations happening that would accidentally omit the type size */ + renderer.setIndexType(MeshIndexType::UnsignedShort); allocation.expectedGlyphCount = 3; allocation.expectedRunCount = 1; + allocation.expectedIndexSize = 3*6*2; + allocation.expectedVertexCount = 3*4; renderer.add(shaper, 1.0f, "abc"); CORRADE_COMPARE(renderer.renderingGlyphCount(), 3); CORRADE_COMPARE(renderer.renderingRunCount(), 1); if(data.renderAddOnly) { CORRADE_VERIFY(renderer.isRendering()); + /* Index and vertex buffers are allocated only when render() is called */ + CORRADE_COMPARE(allocation.indexCalled, 0); + CORRADE_COMPARE(allocation.vertexCalled, 0); + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 0); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 0); CORRADE_COMPARE(renderer.glyphCount(), 0); CORRADE_COMPARE(renderer.runCount(), 0); } else { renderer.render(); CORRADE_VERIFY(!renderer.isRendering()); + CORRADE_COMPARE(allocation.indexCalled, 1); + CORRADE_COMPARE(allocation.vertexCalled, 1); + /* Minimum of all returned view sizes */ + CORRADE_COMPARE(renderer.glyphIndexCapacity(), 22); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 19); CORRADE_COMPARE(renderer.glyphCount(), 3); CORRADE_COMPARE(renderer.runCount(), 1); } @@ -4506,35 +7371,50 @@ void RendererTest::clearResetCoreAllocators() { shorter. */ allocation.expectedGlyphCount = 0; allocation.expectedRunCount = 0; + allocation.expectedIndexSize = 0; + allocation.expectedVertexCount = 0; if(data.reset) renderer.reset(); else renderer.clear(); CORRADE_COMPARE(allocation.glyphCalled, 2); CORRADE_COMPARE(allocation.runCalled, 2); + /* The index allocator doesn't get called because it doesn't make sense to + repopulate it with the exact same contents on every clear() */ + CORRADE_COMPARE(allocation.indexCalled, data.renderAddOnly ? 0 : 1); + CORRADE_COMPARE(allocation.vertexCalled, data.renderAddOnly ? 1 : 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.glyphVertexCapacity(), 19); + /* Stays untouched */ + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.renderAddOnly ? 0 : 22); 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 */ + allocators, except for index allocator, get called again */ if(data.reset) renderer.reset(); else renderer.clear(); CORRADE_COMPARE(allocation.glyphCalled, 3); CORRADE_COMPARE(allocation.runCalled, 3); + CORRADE_COMPARE(allocation.indexCalled, data.renderAddOnly ? 0 : 1); + CORRADE_COMPARE(allocation.vertexCalled, data.renderAddOnly ? 2 : 3); /* Minimum of all returned view sizes */ CORRADE_COMPARE(renderer.glyphCapacity(), 18); + CORRADE_COMPARE(renderer.glyphVertexCapacity(), 19); + /* Stays untouched */ + CORRADE_COMPARE(renderer.glyphIndexCapacity(), data.renderAddOnly ? 0 : 22); CORRADE_COMPARE(renderer.runCapacity(), 3); - /* Other resetting behavior is sufficiently tested by clearResetCore() - already */ + /* Other resetting behavior is sufficiently tested by clearReset() and + clearResetCore() already. Index type (and contents) update after clear + is tested in allocateDifferentIndexType(). */ } #ifdef MAGNUM_TARGET_GL diff --git a/src/Magnum/Text/Text.h b/src/Magnum/Text/Text.h index d2b7d76af..c7dbe3c0c 100644 --- a/src/Magnum/Text/Text.h +++ b/src/Magnum/Text/Text.h @@ -57,6 +57,7 @@ enum class Script: UnsignedInt; class FeatureRange; class RendererCore; +class Renderer; #ifdef MAGNUM_TARGET_GL class DistanceFieldGlyphCacheGL;