Browse Source

Text: new RendererGL class.

This is a replacement for the existing AbstractRenderer and Renderer2D /
Renderer3D classes, with no STL or other shittiness like excessive
allocations, and with a much better feature set. Deprecation of the old
APIs is going to happen next.

Compared to the old implementation it doesn't make use of the
complicated buffer mapping because -- unlike in 2012 -- such behavior
was since deemed questionable as the driver (or whichever translation
layer like ANGLE or Zink or Apple's GL-over-Metal) may still make a copy
anyway and doing so prevents buffer orphaning. So it's right now just
plain setData() / setSubData() calls. *If* it becomes some sort of a
bottleneck (which I doubt), I may reconsider, or add something else like
double buffering.
pull/674/head
Vladimír Vondruš 1 year ago
parent
commit
69f2da2423
  1. 6
      src/Magnum/Text/CMakeLists.txt
  2. 3
      src/Magnum/Text/Implementation/rendererState.h
  3. 5
      src/Magnum/Text/Renderer.cpp
  4. 11
      src/Magnum/Text/Renderer.h
  5. 306
      src/Magnum/Text/RendererGL.cpp
  6. 245
      src/Magnum/Text/RendererGL.h
  7. 26
      src/Magnum/Text/Test/CMakeLists.txt
  8. 768
      src/Magnum/Text/Test/RendererGLTest.cpp
  9. 74
      src/Magnum/Text/Test/RendererGL_Test.cpp
  10. BIN
      src/Magnum/Text/Test/render-nothing.tga
  11. BIN
      src/Magnum/Text/Test/render.tga
  12. 1
      src/Magnum/Text/Text.h

6
src/Magnum/Text/CMakeLists.txt

@ -66,10 +66,12 @@ set(MagnumText_PRIVATE_HEADERS
if(MAGNUM_TARGET_GL)
list(APPEND MagnumText_GracefulAssert_SRCS
DistanceFieldGlyphCacheGL.cpp
GlyphCacheGL.cpp)
GlyphCacheGL.cpp
RendererGL.cpp)
list(APPEND MagnumText_HEADERS
DistanceFieldGlyphCacheGL.h
GlyphCacheGL.h)
GlyphCacheGL.h
RendererGL.h)
if(MAGNUM_BUILD_DEPRECATED)
list(APPEND MagnumText_HEADERS
DistanceFieldGlyphCache.h

3
src/Magnum/Text/Implementation/rendererState.h

@ -150,7 +150,8 @@ struct Renderer::State: RendererCore::AllocatorState {
namespace Implementation {
/* Not used in the state structs above but needed by Renderer */
/* Not used in the state structs above but needed by both Renderer and
RendererGL */
struct Vertex {
Vector2 position;
Vector2 textureCoordinates;

5
src/Magnum/Text/Renderer.cpp

@ -930,6 +930,8 @@ Renderer::State::State(const AbstractGlyphCache& glyphCache, void(*glyphAllocato
Renderer::Renderer(const AbstractGlyphCache& glyphCache, void(*glyphAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D<Vector2>&, Containers::StridedArrayView1D<UnsignedInt>&, Containers::StridedArrayView1D<UnsignedInt>*, Containers::StridedArrayView1D<Vector2>&), void* glyphAllocatorState, void(*runAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D<Float>&, Containers::StridedArrayView1D<UnsignedInt>&), void* runAllocatorState, void(*indexAllocator)(void*, UnsignedInt, Containers::ArrayView<char>&), void* indexAllocatorState, void(*vertexAllocator)(void*, UnsignedInt, Containers::StridedArrayView1D<Vector2>&, Containers::StridedArrayView1D<Vector2>&), void* vertexAllocatorState, RendererFlags flags): RendererCore{Containers::pointer<State>(glyphCache, glyphAllocator, glyphAllocatorState, runAllocator, runAllocatorState, indexAllocator, indexAllocatorState, vertexAllocator, vertexAllocatorState, flags)} {}
Renderer::Renderer(Containers::Pointer<State>&& state): RendererCore{Utility::move(state)} {}
Renderer::Renderer(Renderer&&) noexcept = default;
Renderer::~Renderer() = default;
@ -937,7 +939,8 @@ Renderer::~Renderer() = default;
Renderer& Renderer::operator=(Renderer&&) noexcept = default;
RendererFlags Renderer::flags() const {
return RendererFlags{UnsignedByte(_state->flags)};
/* Subclasses inherit and add their own flags, mask them away */
return RendererFlags{UnsignedByte(_state->flags)} & RendererFlags{0x1};
}
namespace {

11
src/Magnum/Text/Renderer.h

@ -68,8 +68,9 @@ enum class RendererCoreFlag: UnsignedByte {
*/
GlyphClusters = 1 << 0,
/* Additions to this enum have to be propagated to RendererFlag and the
mask in RendererCore::flag() */
/* Additions to this enum have to be propagated to RendererFlag,
RendererGLFlag and the masks in RendererCore::flags() and
Renderer::flags() */
};
/**
@ -651,6 +652,9 @@ enum class RendererFlag: UnsignedByte {
* memory so this flag includes both clusters and positions.
*/
GlyphPositionsClusters = Int(RendererCoreFlag::GlyphClusters)
/* Additions to this enum have to be propagated to RendererGLFlag and the
mask in Renderer::flags() */
};
/**
@ -1062,6 +1066,9 @@ class MAGNUM_TEXT_EXPORT Renderer: public RendererCore {
#endif
struct State;
/* Delegated to by RendererGL constructors */
explicit MAGNUM_TEXT_LOCAL Renderer(Containers::Pointer<State>&& state);
private:
/* While the allocators get just size to grow by, these functions get
the total count */

306
src/Magnum/Text/RendererGL.cpp

@ -0,0 +1,306 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024, 2025
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "RendererGL.h"
#include <Corrade/Containers/EnumSet.hpp>
#include <Corrade/Containers/StringView.h>
#include "Magnum/Shaders/GenericGL.h" /* no link-time dependency here */
#include "Magnum/Text/AbstractGlyphCache.h"
#include "Magnum/Text/Implementation/rendererState.h"
/* Somehow on GCC 4.8 to 7 the {} passed as a default argument for
ArrayView<const FeatureRange> causes "error: elements of array 'const class
Magnum::Text::FeatureRange [0]' have incomplete type". GCC 9 is fine, no
idea about version 8, but including the definition for it as well to be
safe. Similar problem happens with MSVC STL, where the initializer_list is
implemented as a (begin, end) pair and size() is a difference of those two
pointers. Which needs to know the type size to calculate the actual element
count. */
#if (defined(CORRADE_TARGET_GCC) && __GNUC__ <= 8) || defined(CORRADE_TARGET_DINKUMWARE)
#include "Magnum/Text/Feature.h"
#endif
namespace Magnum { namespace Text {
Debug& operator<<(Debug& debug, const RendererGLFlag value) {
debug << "Text::RendererGLFlag" << Debug::nospace;
switch(value) {
/* LCOV_EXCL_START */
#define _c(v) case RendererGLFlag::v: return debug << "::" #v;
_c(GlyphPositionsClusters)
#undef _c
/* LCOV_EXCL_STOP */
}
return debug << "(" << Debug::nospace << Debug::hex << UnsignedByte(value) << Debug::nospace << ")";
}
Debug& operator<<(Debug& debug, const RendererGLFlags value) {
return Containers::enumSetDebugOutput(debug, value, "Text::RendererGLFlags{}", {
RendererGLFlag::GlyphPositionsClusters
});
}
struct RendererGL::State: Renderer::State {
explicit State(const AbstractGlyphCache& glyphCache, RendererGLFlags flags);
GL::Buffer indices{GL::Buffer::TargetHint::ElementArray},
vertices{GL::Buffer::TargetHint::Array};
GL::Mesh mesh;
/* Because querying GL buffer size is not possible on all platforms and it
may be slow, track the size here. It's used to know whether the buffer
should be reuploaded as a whole or can be partially updated, updated in
both reserve() and render(). */
UnsignedInt bufferGlyphCapacity = 0;
};
RendererGL::State::State(const AbstractGlyphCache& glyphCache, RendererGLFlags flags): Renderer::State{glyphCache, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, RendererFlags{UnsignedByte(flags)}} {
#ifdef MAGNUM_TARGET_GLES2
CORRADE_ASSERT(glyphCache.size().z() == 1,
"Text::RendererGL: array glyph caches are not supported in OpenGL ES 2.0 and WebGL 1 builds", );
#endif
/* As documented in RendererGL::setIndexType(), use of 8-bit indices is
discouraged on contemporary GPUs */
indexType = minIndexType = MeshIndexType::UnsignedShort;
/* Set up the mesh with the initial index type and zero primitives to draw.
The count gets updated on each renderer(), index buffer properties each
time the index type changes. */
mesh.setIndexBuffer(indices, 0, indexType)
.setCount(0);
#ifndef MAGNUM_TARGET_GLES2
if(glyphCache.size().z() != 1) {
mesh.addVertexBuffer(vertices, 0,
Shaders::GenericGL2D::Position{},
Shaders::GenericGL2D::TextureArrayCoordinates{});
} else
#endif
{
mesh.addVertexBuffer(vertices, 0,
Shaders::GenericGL2D::Position{},
Shaders::GenericGL2D::TextureCoordinates{});
}
}
RendererGL::RendererGL(const AbstractGlyphCache& glyphCache, RendererGLFlags flags): Renderer{Containers::pointer<State>(glyphCache, flags)} {}
RendererGL::RendererGL(RendererGL&&) noexcept = default;
RendererGL::~RendererGL() = default;
RendererGL& RendererGL::operator=(RendererGL&&) noexcept = default;
RendererGLFlags RendererGL::flags() const {
return RendererGLFlags{UnsignedByte(_state->flags)};
}
GL::Mesh& RendererGL::mesh() {
return static_cast<State&>(*_state).mesh;
}
const GL::Mesh& RendererGL::mesh() const {
return static_cast<const State&>(*_state).mesh;
}
RendererGL& RendererGL::setIndexType(MeshIndexType atLeast) {
State& state = static_cast<State&>(*_state);
Renderer::setIndexType(atLeast);
/* Upload indices anew if the type is different from before. In this case
it's also most likely that the size is bigger than before, so do it as
a setData() call instead of having a specialized setSubData() code path
if the total size shrinks.
Besides the type, the capacity should not change compared to when the
buffer was last updated in reserve() or render(). (Which only holds for
builtin allocators, but RendererGL so far allows only builtin allocators
so that's fine. It however does *not* hold for `state.indexData`, as
that can stay larger if the index type becomes smaller, so verifying
against `state.glyphPositions` instead.) */
CORRADE_INTERNAL_ASSERT(state.bufferGlyphCapacity == state.glyphPositions.size());
if(GL::meshIndexType(state.indexType) != state.mesh.indexType()) {
state.indices.setData(state.indexData);
state.mesh.setIndexBuffer(state.indices, 0, state.indexType);
}
return *this;
}
RendererGL& RendererGL::clear() {
Renderer::clear();
static_cast<State&>(*_state).mesh.setCount(0);
return *this;
}
RendererGL& RendererGL::reset() {
Renderer::reset();
static_cast<State&>(*_state).mesh.setCount(0);
return *this;
}
RendererGL& RendererGL::reserve(const UnsignedInt glyphCapacity, const UnsignedInt runCapacity) {
State& state = static_cast<State&>(*_state);
Renderer::reserve(glyphCapacity, runCapacity);
/* Upload indices anew if the capacity is bigger than before */
if(state.bufferGlyphCapacity < glyphCapacity) {
state.indices.setData(state.indexData);
/* Update the mesh index buffer reference if the type changed */
if(GL::meshIndexType(state.indexType) != state.mesh.indexType())
state.mesh.setIndexBuffer(state.indices, 0, state.indexType);
/* If the capacity isn't bigger, the index type shouldn't have changed
either and so no upload needs to be done. It can change only if the new
capacity is too larger to fit the type used, or in a setIndexType()
call, but there we handle the reupload directly. */
} else CORRADE_INTERNAL_ASSERT(GL::meshIndexType(state.indexType) == state.mesh.indexType());
/* Resize the vertex buffer and reupload its contents if the capacity is
bigger than before */
if(state.bufferGlyphCapacity < glyphCapacity) {
const UnsignedInt glyphSize = 4*(
#ifndef MAGNUM_TARGET_GLES2
state.glyphCache.size().z() != 1 ?
sizeof(Implementation::VertexArray) :
#endif
sizeof(Implementation::Vertex));
/* The assumption in this case is that the capacity is bigger than the
actually rendered glyph count, otherwise we'd have it all resized
and uploaded in render() already. Thus we have to do a bigger
setData() allocation first and then upload just a portion with
setSubData(). */
CORRADE_INTERNAL_ASSERT(glyphCapacity > state.glyphCount);
state.vertices
.setData({nullptr, glyphCapacity*glyphSize})
.setSubData(0, state.vertexData.prefix(state.glyphCount*glyphSize));
}
/* Remember the currently used capacity if it grew. It can happen that
reserve() is called with a smaller capacity, or with just runCapacity
being larger, so this shouldn't reset that and cause needless reupload
next time. */
state.bufferGlyphCapacity = Math::max(state.bufferGlyphCapacity, glyphCapacity);
return *this;
}
Containers::Pair<Range2D, Range1Dui> RendererGL::render() {
State& state = static_cast<State&>(*_state);
const Containers::Pair<Range2D, Range1Dui> out = Renderer::render();
/* Upload indices anew if the glyph count is bigger than before */
if(state.bufferGlyphCapacity < state.glyphCount) {
state.indices.setData(state.indexData);
/* Update the mesh index buffer reference if the type changed */
if(GL::meshIndexType(state.indexType) != state.mesh.indexType())
state.mesh.setIndexBuffer(state.indices, 0, state.indexType);
/* If the glyph count isn't bigger, the index type shouldn't have changed
either. Same reasoning as in reserve() above. */
} else CORRADE_INTERNAL_ASSERT(GL::meshIndexType(state.indexType) == state.mesh.indexType());
/* Upload vertices fully anew if the glyph count is bigger than before */
const UnsignedInt glyphSize = 4*(
#ifndef MAGNUM_TARGET_GLES2
state.glyphCache.size().z() != 1 ?
sizeof(Implementation::VertexArray) :
#endif
sizeof(Implementation::Vertex));
if(state.bufferGlyphCapacity < state.glyphCount) {
/* Unlike in render(), it's just setData() alone, with the assumption
that the render() caused the capacity to grow to fit exactly all
glyphs, and so we upload everything. (Which only holds for builtin
vertex allocators, but RendererGL so far allows only builtin
allocators so that's fine.) */
CORRADE_INTERNAL_ASSERT(
state.vertexPositions.size() == state.glyphCount*4 &&
state.vertexTextureCoordinates.size() == state.glyphCount*4);
state.vertices.setData(state.vertexData.prefix(state.glyphCount*glyphSize));
/* Otherwise upload just what was rendered new */
} else {
const Range1Dui glyphRange = glyphsForRuns(out.second());
state.vertices.setSubData(glyphRange.min()*glyphSize, state.vertexData.slice(glyphRange.min()*glyphSize, glyphRange.max()*glyphSize));
}
/* Remember the currently used capacity if it grew */
state.bufferGlyphCapacity = Math::max(state.bufferGlyphCapacity, state.glyphCount);
/* Set the mesh index count to exactly what was rendered in total */
state.mesh.setCount(state.glyphCount*6);
return out;
}
RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const Containers::ArrayView<const FeatureRange> features) {
return static_cast<RendererGL&>(Renderer::add(shaper, size, text, begin, end, features));
}
RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end) {
return static_cast<RendererGL&>(Renderer::add(shaper, size, text, begin, end));
}
RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const UnsignedInt begin, const UnsignedInt end, const std::initializer_list<FeatureRange> features) {
return static_cast<RendererGL&>(Renderer::add(shaper, size, text, begin, end, features));
}
RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const Containers::ArrayView<const FeatureRange> features) {
return static_cast<RendererGL&>(Renderer::add(shaper, size, text, features));
}
RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text) {
return static_cast<RendererGL&>(Renderer::add(shaper, size, text));
}
RendererGL& RendererGL::add(AbstractShaper& shaper, const Float size, const Containers::StringView text, const std::initializer_list<FeatureRange> features) {
return static_cast<RendererGL&>(Renderer::add(shaper, size, text, features));
}
Containers::Pair<Range2D, Range1Dui> RendererGL::render(AbstractShaper& shaper, const Float size, const Containers::StringView text, const Containers::ArrayView<const FeatureRange> features) {
/* Compared to Renderer::render() this calls our render() instead of
Renderer::render() */
add(shaper, size, text, features);
return render();
}
Containers::Pair<Range2D, Range1Dui> RendererGL::render(AbstractShaper& shaper, const Float size, const Containers::StringView text) {
return render(shaper, size, text, {});
}
Containers::Pair<Range2D, Range1Dui> RendererGL::render(AbstractShaper& shaper, const Float size, const Containers::StringView text, const std::initializer_list<FeatureRange> features) {
return render(shaper, size, text, Containers::arrayView(features));
}
}}

245
src/Magnum/Text/RendererGL.h

@ -0,0 +1,245 @@
#ifndef Magnum_Text_RendererGL_h
#define Magnum_Text_RendererGL_h
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024, 2025
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
/** @file
* @brief Class @ref Magnum::Text::RendererGL, enum @ref Magnum::Text::RendererGLFlag, enum set @ref Magnum::Text::RendererGLFlags
* @m_since_latest
*/
#include "Magnum/configure.h"
#ifdef MAGNUM_TARGET_GL
#include "Magnum/Text/Renderer.h"
namespace Magnum { namespace Text {
/**
@brief OpenGL text renderer flag
@m_since_latest
A superset of @ref RendererFlag.
@note This enum is available only if Magnum is compiled with
@ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features
for more information.
@see @ref RendererGLFlags, @ref RendererGL
*/
/* Currently is the same as RendererFlag, but is made to a dedicated type to
not cause a breaking change once GL-specific flags are introduced, such as
buffer mapping */
enum class RendererGLFlag: UnsignedByte {
/** @copydoc RendererFlag::GlyphPositionsClusters */
GlyphPositionsClusters = UnsignedByte(RendererFlag::GlyphPositionsClusters)
};
/**
@debugoperatorenum{RendererGLFlag}
@m_since_latest
@note This function is available only if Magnum is compiled with
@ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features
for more information.
*/
MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, RendererGLFlag value);
/**
@brief OpenGL text renderer flags
@m_since_latest
@note This enum set is available only if Magnum is compiled with
@ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features
for more information.
@see @ref RendererGL
*/
typedef Containers::EnumSet<RendererGLFlag> RendererGLFlags;
CORRADE_ENUMSET_OPERATORS(RendererGLFlags)
/**
@debugoperatorenum{RendererGLFlags}
@m_since_latest
@note This function is available only if Magnum is compiled with
@ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features
for more information.
*/
MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, RendererGLFlags value);
/**
@brief OpenGL text renderer
@m_since_latest
@note This class is available only if Magnum is compiled with
@ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features
for more information.
*/
class MAGNUM_TEXT_EXPORT RendererGL: public Renderer {
public:
/**
* @brief Construct
* @param glyphCache Glyph cache to use
* @param flags Opt-in feature flags
*
* Unlike with the @ref Renderer base, the OpenGL implementation needs
* to have a complete control over memory layout and allocation and
* thus it isn't possible to supply custom allocators. If you want the
* control, use @ref Renderer with custom index and vertex allocators
* and fill a @ref GL::Mesh instance with the data manually.
*/
explicit RendererGL(const AbstractGlyphCache& glyphCache, RendererGLFlags flags = {});
/**
* @brief Construct without creating the internal state and the OpenGL objects
*
* The constructed instance is equivalent to moved-from state, i.e. no
* APIs can be safely called on the object. Useful in cases where you
* will overwrite the instance later anyway. Move another object over
* it to make it useful.
*
* This function can be safely used for constructing (and later
* destructing) objects even without any OpenGL context being active.
* However note that this is a low-level and a potentially dangerous
* API, see the documentation of @ref NoCreate for alternatives.
*/
explicit RendererGL(NoCreateT) noexcept: Renderer{NoCreate} {}
/** @brief Copying is not allowed */
RendererGL(RendererGL&) = delete;
/**
* @brief Move constructor
*
* Performs a destructive move, i.e. the original object isn't usable
* afterwards anymore.
*/
RendererGL(RendererGL&&) noexcept;
~RendererGL();
/** @brief Copying is not allowed */
RendererGL& operator=(RendererGL&) = delete;
/** @brief Move assignment */
RendererGL& operator=(RendererGL&&) noexcept;
/** @brief Flags */
RendererGLFlags flags() const;
/** @brief Mesh containing the rendered index and vertex data */
GL::Mesh& mesh();
const GL::Mesh& mesh() const; /**< @overload */
/**
* @brief Set index type
* @return Reference to self (for method chaining)
*
* Calls @ref Renderer::setIndexType() and updates @ref mesh() with the
* rendered index data, if different from before. Compared to
* @ref Renderer, the default index type is
* @ref MeshIndexType::UnsignedShort, not
* @ref MeshIndexType::UnsignedByte, as use of 8-bit indices is
* discouraged on contemporary GPUs.
*/
RendererGL& setIndexType(MeshIndexType atLeast);
/**
* @brief Reserve capacity for given glyph count
* @return Reference to self (for method chaining)
*
* Calls @ref Renderer::reserve() and updates @ref mesh() with the
* rendered index data, if different from before.
*/
RendererGL& reserve(UnsignedInt glyphCapacity, UnsignedInt runCapacity);
/**
* @brief Clear rendered glyphs, runs and vertices
* @return Reference to self (for method chaining)
*
* Calls @ref Renderer::clear() and additionally also sets @ref mesh()
* index count to @cpp 0 @ce.
*/
RendererGL& clear();
/**
* @brief Reset internal renderer state
* @return Reference to self (for method chaining)
*
* Calls @ref Renderer::reset(), and additionally also sets @ref mesh()
* index count to @cpp 0 @ce.
*/
RendererGL& reset();
/**
* @brief Wrap up rendering of all text added so far
*
* Calls @ref Renderer::render(), updates @ref mesh() with the newly
* rendered vertex data and potentially updates also the index data, if
* different from before.
*/
Containers::Pair<Range2D, Range1Dui> render();
/* Overloads to remove a WTF factor from method chaining order, and to
ensure our render() is called instead of Render::render() */
#ifndef DOXYGEN_GENERATING_OUTPUT
RendererGL& setCursor(const Vector2& cursor) {
return static_cast<RendererGL&>(Renderer::setCursor(cursor));
}
RendererGL& setAlignment(Alignment alignment) {
return static_cast<RendererGL&>(Renderer::setAlignment(alignment));
}
RendererGL& setLineAdvance(Float advance) {
return static_cast<RendererGL&>(Renderer::setLineAdvance(advance));
}
RendererGL& setLayoutDirection(LayoutDirection direction) {
return static_cast<RendererGL&>(Renderer::setLayoutDirection(direction));
}
RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, Containers::ArrayView<const FeatureRange> features);
RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end);
RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, UnsignedInt begin, UnsignedInt end, std::initializer_list<FeatureRange> features);
RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView<const FeatureRange> features);
RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text);
RendererGL& add(AbstractShaper& shaper, Float size, Containers::StringView text, std::initializer_list<FeatureRange> features);
Containers::Pair<Range2D, Range1Dui> render(AbstractShaper& shaper, Float size, Containers::StringView text, Containers::ArrayView<const FeatureRange> features);
Containers::Pair<Range2D, Range1Dui> render(AbstractShaper& shaper, Float size, Containers::StringView text);
Containers::Pair<Range2D, Range1Dui> render(AbstractShaper& shaper, Float size, Containers::StringView text, std::initializer_list<FeatureRange> features);
#endif
private:
struct State;
};
}}
#else
#error this header is available only in the OpenGL build
#endif
#endif

26
src/Magnum/Text/Test/CMakeLists.txt

@ -87,6 +87,7 @@ corrade_add_test(TextScriptTest ScriptTest.cpp LIBRARIES MagnumTextTestLib)
if(MAGNUM_TARGET_GL)
corrade_add_test(TextGlyphCacheGL_Test GlyphCacheGL_Test.cpp LIBRARIES MagnumText)
corrade_add_test(TextDistanceFieldGlyphCacheGL_Test DistanceFieldGlyphCacheGL_Test.cpp LIBRARIES MagnumText)
corrade_add_test(TextRendererGL_Test RendererGL_Test.cpp LIBRARIES MagnumText)
if(MAGNUM_BUILD_GL_TESTS)
corrade_add_test(TextDistanceFieldGlyphCacheGLTest DistanceFieldGlyphCacheGLTest.cpp
@ -120,9 +121,32 @@ if(MAGNUM_TARGET_GL)
MagnumTextTestLib
MagnumOpenGLTester
MagnumDebugTools)
corrade_add_test(TextRendererGLTest RendererGLTest.cpp
LIBRARIES
MagnumDebugTools
MagnumShaders
MagnumTextTestLib
MagnumOpenGLTester)
MagnumOpenGLTester
FILES
render.tga
render-nothing.tga)
target_include_directories(TextRendererGLTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>)
if(MAGNUM_BUILD_PLUGINS_STATIC)
if(MAGNUM_WITH_ANYIMAGEIMPORTER)
target_link_libraries(TextRendererGLTest PRIVATE AnyImageImporter)
endif()
if(MAGNUM_WITH_TGAIMPORTER)
target_link_libraries(TextRendererGLTest PRIVATE TgaImporter)
endif()
else()
# So the plugins get properly built when building the test
if(MAGNUM_WITH_ANYIMAGEIMPORTER)
add_dependencies(TextRendererGLTest AnyImageImporter)
endif()
if(MAGNUM_WITH_TGAIMPORTER)
add_dependencies(TextRendererGLTest TgaImporter)
endif()
endif()
endif()
endif()

768
src/Magnum/Text/Test/RendererGLTest.cpp

@ -27,33 +27,799 @@
#include <string>
#include <tuple>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/Optional.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/String.h>
#include <Corrade/Containers/StringView.h>
#include <Corrade/PluginManager/Manager.h>
#include <Corrade/TestSuite/Compare/Container.h>
#include <Corrade/Utility/Path.h>
#include "Magnum/Image.h"
#include "Magnum/ImageView.h"
#include "Magnum/Mesh.h"
#include "Magnum/PixelFormat.h"
#include "Magnum/Math/Color.h"
#include "Magnum/Math/Matrix3.h"
#include "Magnum/DebugTools/CompareImage.h"
#include "Magnum/GL/Context.h"
#include "Magnum/GL/Extensions.h"
#include "Magnum/GL/Framebuffer.h"
#include "Magnum/GL/OpenGLTester.h"
#include "Magnum/GL/Renderbuffer.h"
#include "Magnum/GL/RenderbufferFormat.h"
#include "Magnum/Shaders/VectorGL.h"
#include "Magnum/Text/AbstractFont.h"
#include "Magnum/Text/AbstractShaper.h"
#include "Magnum/Text/GlyphCacheGL.h"
#include "Magnum/Text/Renderer.h"
#include "Magnum/Text/RendererGL.h"
#include "Magnum/Trade/AbstractImporter.h"
#include "configure.h"
namespace Magnum { namespace Text { namespace Test { namespace {
struct RendererGLTest: GL::OpenGLTester {
explicit RendererGLTest();
void construct();
#ifdef MAGNUM_TARGET_GLES2
void constructArrayGlyphCacheNotSupported();
#endif
void constructCopy();
void constructMove();
/* Compared to Renderer & RendererCore, RendererGL doesn't expose any extra
getters / setters for a properties() test */
void renderSetup();
void renderTeardown();
void renderClearReset();
void renderIndexTypeChanged();
void renderMesh();
void renderMeshIndexType();
void mutableText();
private:
PluginManager::Manager<Trade::AbstractImporter> _manager{"nonexistent"};
GL::Renderbuffer _color{NoCreate};
GL::Framebuffer _framebuffer{NoCreate};
};
using namespace Containers::Literals;
using namespace Math::Literals;
const struct {
const char* name;
Containers::Optional<MeshIndexType> indexType;
Int glyphCacheArraySize;
RendererGLFlags flags;
MeshIndexType expectedIndexType;
} ConstructData[]{
{"",
{}, 1, {}, MeshIndexType::UnsignedShort},
{"UnsignedByte indices",
MeshIndexType::UnsignedByte, 1, {}, MeshIndexType::UnsignedByte},
{"UnsignedInt indices",
MeshIndexType::UnsignedInt, 1, {}, MeshIndexType::UnsignedInt},
{"glyph positions and clusters",
{}, 1, RendererGLFlag::GlyphPositionsClusters, MeshIndexType::UnsignedShort},
#ifndef MAGNUM_TARGET_GLES2
{"array glyph cache",
{}, 5, {}, MeshIndexType::UnsignedShort},
{"array glyph cache, glyph positions and clusters",
{}, 5, RendererGLFlag::GlyphPositionsClusters, MeshIndexType::UnsignedShort},
#endif
};
const struct {
const char* name;
Int glyphCacheArraySize;
RendererGLFlags flags;
UnsignedInt reserveBefore, reserveAfter;
Containers::Optional<MeshIndexType> indexTypeBefore, indexTypeAfter;
bool clear, reset;
MeshIndexType expectedIndexType;
} RenderClearResetData[]{
{"",
1, {}, 0, 0, {}, {}, false, false,
MeshIndexType::UnsignedShort},
{"glyph positions and clusters",
1, RendererGLFlag::GlyphPositionsClusters, 0, 0, {}, {}, false, false, MeshIndexType::UnsignedShort},
#ifndef MAGNUM_TARGET_GLES2
{"array glyph cache",
5, {}, 0, 0, {}, {}, false, false,
MeshIndexType::UnsignedShort},
{"array glyph cache, glyph positions and clusters",
5, RendererGLFlag::GlyphPositionsClusters, 0, 0, {}, {}, false, false,
MeshIndexType::UnsignedShort},
#endif
/* These test just index buffer generation, so no cache- or glyph-related
variants */
{"UnsignedByte indices",
1, {}, 0, 0, MeshIndexType::UnsignedByte, {}, false, false,
MeshIndexType::UnsignedByte},
{"explicit default UnsignedShort indices",
1, {}, 0, 0, MeshIndexType::UnsignedShort, {}, false, false,
MeshIndexType::UnsignedShort},
{"UnsignedInt indices",
1, {}, 0, 0, MeshIndexType::UnsignedInt, {}, false, false,
MeshIndexType::UnsignedInt},
{"reserve exactly upfront",
1, {}, 5, 0, {}, {}, false, false,
MeshIndexType::UnsignedShort},
{"reserve partially upfront",
1, {}, 3, 0, {}, {}, false, false,
MeshIndexType::UnsignedShort},
{"reserve more upfront",
1, {}, 16385, 0, {}, {}, false, false,
MeshIndexType::UnsignedInt},
{"reserve again after render with the same",
1, {}, 0, 5, {}, {}, false, false,
MeshIndexType::UnsignedShort},
{"reserve again after render with less",
1, {}, 0, 3, {}, {}, false, false,
MeshIndexType::UnsignedShort},
{"reserve again after render with more",
/* Reserve a bigger size to ensure it doesn't get aliased with the old
memory, preserving the original contents by accident and hiding a
potential bug where it doesn't get correctly reuploaded */
1, {}, 0, 1024*1024, {}, {}, false, false,
MeshIndexType::UnsignedInt},
{"reserve all upfront and then change indices to UnsignedByte",
1, {}, 5, 0, MeshIndexType::UnsignedByte, {}, false, false,
MeshIndexType::UnsignedByte},
{"reserve all upfront and then explicitly use default UnsignedShort indices",
1, {}, 5, 0, MeshIndexType::UnsignedShort, {}, false, false,
MeshIndexType::UnsignedShort},
{"reserve all upfront and then change indices to UnsignedInt",
1, {}, 5, 0, MeshIndexType::UnsignedInt, {}, false, false,
MeshIndexType::UnsignedInt},
{"change indices to UnsignedByte after render",
1, {}, 0, 0, {}, MeshIndexType::UnsignedByte, false, false,
MeshIndexType::UnsignedByte},
{"explicitly set default UnsignedShort after render",
1, {}, 0, 0, {}, MeshIndexType::UnsignedShort, false, false,
MeshIndexType::UnsignedShort},
{"change indices to UnsignedInt after render",
1, {}, 0, 0, {}, MeshIndexType::UnsignedInt, false, false,
MeshIndexType::UnsignedInt},
{"clear and rerender",
1, {}, 0, 0, {}, {}, true, false,
MeshIndexType::UnsignedShort},
{"clear and rerender, UnsignedInt indices",
1, {}, 0, 0, MeshIndexType::UnsignedInt, {}, true, false,
MeshIndexType::UnsignedInt},
{"reset and rerender",
1, {}, 0, 0, {}, {}, true, true,
MeshIndexType::UnsignedShort},
};
RendererGLTest::RendererGLTest() {
addInstancedTests({&RendererGLTest::construct},
Containers::arraySize(ConstructData));
#ifdef MAGNUM_TARGET_GLES2
addTests({&RendererGLTest::constructArrayGlyphCacheNotSupported});
#endif
addTests({&RendererGLTest::constructCopy,
&RendererGLTest::constructMove});
addInstancedTests({&RendererGLTest::renderClearReset},
Containers::arraySize(RenderClearResetData),
&RendererGLTest::renderSetup,
&RendererGLTest::renderTeardown);
addTests({&RendererGLTest::renderIndexTypeChanged},
&RendererGLTest::renderSetup,
&RendererGLTest::renderTeardown);
addTests({&RendererGLTest::renderMesh,
&RendererGLTest::renderMeshIndexType,
&RendererGLTest::mutableText});
/* Load the plugins directly from the build tree. Otherwise they're either
static and already loaded or not present in the build tree */
#ifdef ANYIMAGEIMPORTER_PLUGIN_FILENAME
CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(ANYIMAGEIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded);
#endif
#ifdef TGAIMPORTER_PLUGIN_FILENAME
CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(TGAIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded);
#endif
}
void RendererGLTest::construct() {
auto&& data = ConstructData[testCaseInstanceId()];
setTestCaseDescription(data.name);
struct: AbstractGlyphCache {
using AbstractGlyphCache::AbstractGlyphCache;
GlyphCacheFeatures doFeatures() const override { return {}; }
} glyphCache{PixelFormat::R8Unorm, {16, 16, data.glyphCacheArraySize}};
RendererGL renderer{glyphCache, data.flags};
if(data.indexType)
renderer.setIndexType(*data.indexType);
MAGNUM_VERIFY_NO_GL_ERROR();
CORRADE_COMPARE(renderer.flags(), data.flags);
CORRADE_COMPARE(renderer.indexType(), data.expectedIndexType);
CORRADE_COMPARE(renderer.mesh().count(), 0);
CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(data.expectedIndexType));
}
#ifdef MAGNUM_TARGET_GLES2
void RendererGLTest::constructArrayGlyphCacheNotSupported() {
CORRADE_SKIP_IF_NO_ASSERT();
struct: AbstractGlyphCache {
using AbstractGlyphCache::AbstractGlyphCache;
GlyphCacheFeatures doFeatures() const override { return {}; }
} glyphCache{PixelFormat::R8Unorm, {16, 16, 5}};
Containers::String out;
Error redirectError{&out};
RendererGL{glyphCache};
CORRADE_COMPARE(out, "Text::RendererGL: array glyph caches are not supported in OpenGL ES 2.0 and WebGL 1 builds\n");
}
#endif
void RendererGLTest::constructCopy() {
CORRADE_VERIFY(!std::is_copy_constructible<RendererGL>{});
CORRADE_VERIFY(!std::is_copy_assignable<RendererGL>{});
}
void RendererGLTest::constructMove() {
struct: AbstractGlyphCache {
using AbstractGlyphCache::AbstractGlyphCache;
GlyphCacheFeatures doFeatures() const override { return {}; }
} glyphCache{PixelFormat::R8Unorm, {16, 16}},
anotherGlyphCache{PixelFormat::RGBA8Unorm, {4, 4}};
/* Verify that both the Renderer and the RendererGL state is
transferred */
RendererGL a{glyphCache, RendererGLFlags{0x80}};
UnsignedInt meshId = a.mesh().id();
a.setIndexType(MeshIndexType::UnsignedInt);
RendererGL b = Utility::move(a);
CORRADE_COMPARE(&b.glyphCache(), &glyphCache);
CORRADE_COMPARE(b.flags(), RendererGLFlags{0x80});
CORRADE_COMPARE(b.indexType(), MeshIndexType::UnsignedInt);
CORRADE_COMPARE(b.mesh().id(), meshId);
RendererGL c{anotherGlyphCache};
c = Utility::move(b);
CORRADE_COMPARE(&c.glyphCache(), &glyphCache);
CORRADE_COMPARE(c.flags(), RendererGLFlags{0x80});
CORRADE_COMPARE(c.indexType(), MeshIndexType::UnsignedInt);
CORRADE_COMPARE(c.mesh().id(), meshId);
CORRADE_VERIFY(std::is_nothrow_move_constructible<RendererGL>::value);
CORRADE_VERIFY(std::is_nothrow_move_assignable<RendererGL>::value);
}
constexpr Vector2i RenderSize{8, 8};
void RendererGLTest::renderSetup() {
/* Pick a color that's directly representable on RGBA4 as well to reduce
artifacts */
GL::Renderer::setClearColor(0x111111_rgbf);
GL::Renderer::enable(GL::Renderer::Feature::FaceCulling);
_color = GL::Renderbuffer{};
_color.setStorage(
#if !defined(MAGNUM_TARGET_GLES2) || !defined(MAGNUM_TARGET_WEBGL)
GL::RenderbufferFormat::RGBA8,
#else
GL::RenderbufferFormat::RGBA4,
#endif
RenderSize);
_framebuffer = GL::Framebuffer{{{}, RenderSize}};
_framebuffer
.attachRenderbuffer(GL::Framebuffer::ColorAttachment{0}, _color)
.clear(GL::FramebufferClear::Color)
.bind();
}
void RendererGLTest::renderTeardown() {
_framebuffer = GL::Framebuffer{NoCreate};
_color = GL::Renderbuffer{NoCreate};
}
void RendererGLTest::renderClearReset() {
auto&& data = RenderClearResetData[testCaseInstanceId()];
setTestCaseDescription(data.name);
/* Override the default padding to zero to make testing easier, also use
nearest neighbor filtering for predictable output */
GlyphCacheGL cache{NoCreate};
#ifndef MAGNUM_TARGET_GLES2
GlyphCacheArrayGL cacheArray{NoCreate};
if(data.glyphCacheArraySize != 1) {
cacheArray = GlyphCacheArrayGL{PixelFormat::R8Unorm, {2, 2, data.glyphCacheArraySize}, {}};
cacheArray.texture()
.setMinificationFilter(SamplerFilter::Nearest)
.setMagnificationFilter(SamplerFilter::Nearest);
} else
#endif
{
cache = GlyphCacheGL{PixelFormat::R8Unorm, {2, 2}, {}};
cache.texture()
.setMinificationFilter(SamplerFilter::Nearest)
.setMagnificationFilter(SamplerFilter::Nearest);
}
/* For type-independent access below */
AbstractGlyphCache& glyphCache =
#ifndef MAGNUM_TARGET_GLES2
data.glyphCacheArraySize != 1 ? static_cast<AbstractGlyphCache&>(cacheArray) :
#endif
cache;
Shaders::VectorGL2D shader{Shaders::VectorGL2D::Configuration{}
#ifndef MAGNUM_TARGET_GLES2
.setFlags(data.glyphCacheArraySize != 1 ?
Shaders::VectorGL2D::Flag::TextureArrays :
Shaders::VectorGL2D::Flags{})
#endif
};
shader.setTransformationProjectionMatrix(Matrix3::projection(Vector2{RenderSize}));
#ifndef MAGNUM_TARGET_GLES2
if(data.glyphCacheArraySize != 1) {
shader.bindVectorTexture(cacheArray.texture());
} else
#endif
{
shader.bindVectorTexture(cache.texture());
}
struct: AbstractFont {
FontFeatures doFeatures() const override { return {}; }
bool doIsOpened() const override { return _opened; }
void doClose() override { _opened = false; }
Properties doOpenFile(Containers::StringView, Float) override {
_opened = true;
/* The size is used to scale advances, ascent & descent is used to
align the block. Line height is used for multi-line text which
we don't test here, glyph count is overriden in addFont()
below. */
return {1.0f, 2.0f, -1.0f, 10000.0f, 0};
}
void doGlyphIdsInto(const Containers::StridedArrayView1D<const char32_t>&, const Containers::StridedArrayView1D<UnsignedInt>&) override {}
Vector2 doGlyphSize(UnsignedInt) override { return {}; }
Vector2 doGlyphAdvance(UnsignedInt) override { return {}; }
Containers::Pointer<AbstractShaper> doCreateShaper() override { return {}; }
private:
bool _opened = false;
} font;
font.openFile("", 1.0f);
UnsignedInt fontId = glyphCache.addFont(4, &font);
/* Shuffled order to not have their IDs match the clusters, other than that
the simplest possible contents to make it easy to verify that the data
get correctly uploaded. All corner cases are verified in RendererTest
already. */
glyphCache.addGlyph(fontId, 3, {}, /* c, white square */
data.glyphCacheArraySize/2,
Range2Di::fromSize({1, 0}, {1, 1}));
glyphCache.addGlyph(fontId, 1, {}, /* a / d, light gray square */
data.glyphCacheArraySize - 1,
Range2Di::fromSize({1, 1}, {1, 1}));
glyphCache.addGlyph(fontId, 2, {}, /* b / e, dark gray rect */
0,
Range2Di::fromSize({0, 0}, {1, 2}));
{
const Containers::StridedArrayView3D<UnsignedByte> pixels = glyphCache.image().pixels<UnsignedByte>();
pixels[data.glyphCacheArraySize/2][0][1] = 0xff;
pixels[0][0][0] = 0x33;
pixels[0][1][0] = 0x33;
pixels[data.glyphCacheArraySize - 1][1][1] = 0x99;
}
glyphCache.flushImage({{}, glyphCache.size()});
MAGNUM_VERIFY_NO_GL_ERROR();
struct: AbstractShaper {
using AbstractShaper::AbstractShaper;
UnsignedInt doShape(Containers::StringView, UnsignedInt begin, UnsignedInt end, Containers::ArrayView<const FeatureRange>) override {
return end - begin;
}
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>& ids) const override {
for(UnsignedInt i = 0; i != ids.size(); ++i)
ids[i] = i + 1;
}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>& offsets, const Containers::StridedArrayView1D<Vector2>& advances) const override {
/* Just the simplest possible, rigorously tested in RendererTest
already */
for(UnsignedInt i = 0; i != offsets.size(); ++i) {
advances[i] = {2.0f, 0.0f};
offsets[i] = {0.0f, i == 2 ? 1.0f : 0.0f};
}
}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>& clusters) const override {
/* Just to verify that the clusters get populated with meaningful
data */
for(UnsignedInt i = 0; i != clusters.size(); ++i)
clusters[i] = 10 + i*3;
}
} shaper{font};
RendererGL renderer{glyphCache, data.flags};
/* Rendering with nothing inside should result in nothing */
shader.draw(renderer.mesh());
MAGNUM_VERIFY_NO_GL_ERROR();
if(!(_manager.loadState("AnyImageImporter") & PluginManager::LoadState::Loaded) ||
!(_manager.loadState("TgaImporter") & PluginManager::LoadState::Loaded))
CORRADE_SKIP("AnyImageImporter / TgaImporter plugins not found.");
CORRADE_COMPARE_WITH(
/* Use just one channel, the others are always the same */
_framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels<Color4ub>().slice(&Color4ub::r),
Utility::Path::join(TEXT_TEST_DIR, "render-nothing.tga"),
(DebugTools::CompareImageToFile{_manager, 0.0f, 0.0f}));
/* This uploads indices if called */
if(data.reserveBefore) {
renderer.reserve(data.reserveBefore, 0);
MAGNUM_VERIFY_NO_GL_ERROR();
CORRADE_COMPARE(renderer.mesh().count(), 0);
}
/* This may reupload indices if reserve() was called */
if(data.indexTypeBefore) {
renderer.setIndexType(*data.indexTypeBefore);
MAGNUM_VERIFY_NO_GL_ERROR();
CORRADE_COMPARE(renderer.indexType(), *data.indexTypeBefore);
CORRADE_COMPARE(renderer.mesh().count(), 0);
CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(*data.indexTypeBefore));
}
for(Int i = 0, iMax = 1 + (data.reset || data.clear); i != iMax; ++i) {
if(data.clear)
CORRADE_ITERATION("after clear");
else if(data.reset)
CORRADE_ITERATION("after reset");
/* This uploads indices if reserve() wasn't called */
renderer
.setAlignment(Alignment::LineLeft)
.setCursor({-3.0f, 1.0f})
.render(shaper, 1.0f, "abc");
/* This uploads indices if reserve() wasn't called or was too little */
renderer
.setAlignment(Alignment::LineRight)
.setCursor({5.0f, -3.0f})
.render(shaper, 2.0f, "de");
/* This may reupload indices if called */
if(data.indexTypeAfter) {
renderer.setIndexType(*data.indexTypeAfter);
MAGNUM_VERIFY_NO_GL_ERROR();
CORRADE_COMPARE(renderer.indexType(), *data.indexTypeAfter);
CORRADE_COMPARE(renderer.mesh().count(), 5*6);
CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(*data.indexTypeAfter));
}
/* This may reupload indices and vertices if called */
if(data.reserveAfter) {
renderer.reserve(data.reserveAfter, 0);
MAGNUM_VERIFY_NO_GL_ERROR();
CORRADE_COMPARE(renderer.mesh().count(), 5*6);
}
MAGNUM_VERIFY_NO_GL_ERROR();
CORRADE_COMPARE(renderer.indexType(), data.expectedIndexType);
CORRADE_COMPARE(renderer.mesh().count(), 5*6);
CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(data.expectedIndexType));
/* If glyph positions and clusters are enabled, verify they got filled as
well. Again just to be sure that the operation is done at all,
thoroughly tested in RendererTest already. */
if(data.flags & RendererGLFlag::GlyphPositionsClusters) {
CORRADE_COMPARE_AS(renderer.glyphPositions(), Containers::arrayView<Vector2>({
{-3.0f, 1.0f}, /* a */
{-1.0f, 1.0f}, /* b */
{ 1.0f, 2.0f}, /* c */
{-3.0f, -3.0f}, /* d */
{ 1.0f, -3.0f}, /* e */
}), TestSuite::Compare::Container);
CORRADE_COMPARE_AS(renderer.glyphClusters(), Containers::arrayView({
10u, 13u, 16u,
10u, 13u
}), TestSuite::Compare::Container);
}
/* Verify the index and vertex data are generated as expected */
if(data.expectedIndexType == MeshIndexType::UnsignedByte)
CORRADE_COMPARE_AS(renderer.indices<UnsignedByte>(), Containers::arrayView<UnsignedByte>({
0, 1, 2, 2, 1, 3,
4, 5, 6, 6, 5, 7,
8, 9, 10, 10, 9, 11,
12, 13, 14, 14, 13, 15,
16, 17, 18, 18, 17, 19
}), TestSuite::Compare::Container);
else if(data.expectedIndexType == MeshIndexType::UnsignedShort)
CORRADE_COMPARE_AS(renderer.indices<UnsignedShort>(), Containers::arrayView<UnsignedShort>({
0, 1, 2, 2, 1, 3,
4, 5, 6, 6, 5, 7,
8, 9, 10, 10, 9, 11,
12, 13, 14, 14, 13, 15,
16, 17, 18, 18, 17, 19
}), TestSuite::Compare::Container);
else if(data.expectedIndexType == MeshIndexType::UnsignedInt)
CORRADE_COMPARE_AS(renderer.indices<UnsignedInt>(), Containers::arrayView<UnsignedInt>({
0, 1, 2, 2, 1, 3,
4, 5, 6, 6, 5, 7,
8, 9, 10, 10, 9, 11,
12, 13, 14, 14, 13, 15,
16, 17, 18, 18, 17, 19
}), TestSuite::Compare::Container);
else CORRADE_INTERNAL_ASSERT_UNREACHABLE();
CORRADE_COMPARE_AS(renderer.vertexPositions(), Containers::arrayView<Vector2>({
{-3.0f, 1.0f}, /* a */
{-2.0f, 1.0f},
{-3.0f, 2.0f},
{-2.0f, 2.0f},
{-1.0f, 1.0f}, /* b, rect */
{ 0.0f, 1.0f},
{-1.0f, 3.0f},
{ 0.0f, 3.0f},
{ 1.0f, 2.0f}, /* c */
{ 2.0f, 2.0f},
{ 1.0f, 3.0f},
{ 2.0f, 3.0f},
{-3.0f, -3.0f}, /* d */
{-1.0f, -3.0f},
{-3.0f, -1.0f},
{-1.0f, -1.0f},
{ 1.0f, -3.0f}, /* e, rect */
{ 3.0f, -3.0f},
{ 1.0f, 1.0f},
{ 3.0f, 1.0f},
}), TestSuite::Compare::Container);
Vector3 expectedTextureCoordinates[]{
{0.5f, 0.5f, Float(data.glyphCacheArraySize - 1)}, /* a */
{1.0f, 0.5f, Float(data.glyphCacheArraySize - 1)},
{0.5f, 1.0f, Float(data.glyphCacheArraySize - 1)},
{1.0f, 1.0f, Float(data.glyphCacheArraySize - 1)},
{0.0f, 0.0f, 0.0f}, /* b, rect */
{0.5f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f},
{0.5f, 1.0f, 0.0f},
{0.5f, 0.0f, Float(data.glyphCacheArraySize/2)}, /* c */
{1.0f, 0.0f, Float(data.glyphCacheArraySize/2)},
{0.5f, 0.5f, Float(data.glyphCacheArraySize/2)},
{1.0f, 0.5f, Float(data.glyphCacheArraySize/2)},
{0.5f, 0.5f, Float(data.glyphCacheArraySize - 1)}, /* d; same as a */
{1.0f, 0.5f, Float(data.glyphCacheArraySize - 1)},
{0.5f, 1.0f, Float(data.glyphCacheArraySize - 1)},
{1.0f, 1.0f, Float(data.glyphCacheArraySize - 1)},
{0.0f, 0.0f, 0.0f}, /* e, rect; same as b */
{0.5f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f},
{0.5f, 1.0f, 0.0f},
};
#ifndef MAGNUM_TARGET_GLES2
if(data.glyphCacheArraySize != 1) {
CORRADE_COMPARE_AS(renderer.vertexTextureArrayCoordinates(),
Containers::arrayView(expectedTextureCoordinates),
TestSuite::Compare::Container);
} else
#endif
{
CORRADE_COMPARE_AS(renderer.vertexTextureCoordinates(),
Containers::stridedArrayView(expectedTextureCoordinates).slice(&Vector3::xy),
TestSuite::Compare::Container);
}
/* Draw the generated mesh */
_framebuffer.clear(GL::FramebufferClear::Color);
shader.draw(renderer.mesh());
MAGNUM_VERIFY_NO_GL_ERROR();
/* light gray square, dark gray rect, white square on top left
large light gray square, large dark gray rect on bottom right */
CORRADE_COMPARE_WITH(
/* Use just one channel, the others are always the same */
_framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels<Color4ub>().slice(&Color4ub::r),
Utility::Path::join(TEXT_TEST_DIR, "render.tga"),
(DebugTools::CompareImageToFile{_manager, 0.0f, 0.0f}));
/* If resetting or clearing, there's another iteration of all above.
Verify that it calls correct parent reset or clear by checking
whether the cursor stays as before or not. */
if(data.reset || data.clear) {
if(data.reset) {
renderer.reset();
CORRADE_COMPARE(renderer.mesh().count(), 0);
CORRADE_COMPARE(renderer.cursor(), Vector2{});
} else {
renderer.clear();
CORRADE_COMPARE(renderer.cursor(), (Vector2{5.0f, -3.0f}));
}
/* The index type should stay even after clear / reset */
CORRADE_COMPARE(renderer.indexType(), data.expectedIndexType);
CORRADE_COMPARE(renderer.mesh().count(), 0);
CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(data.expectedIndexType));
/* Rendering after a reset or clear should result in nothing
again */
_framebuffer.clear(GL::FramebufferClear::Color);
shader.draw(renderer.mesh());
MAGNUM_VERIFY_NO_GL_ERROR();
CORRADE_COMPARE_WITH(
/* Use just one channel, the others are always the same */
_framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels<Color4ub>().slice(&Color4ub::r),
Utility::Path::join(TEXT_TEST_DIR, "render-nothing.tga"),
(DebugTools::CompareImageToFile{_manager, 0.0f, 0.0f}));
}
}
/* Clearing twice in a row should not result in anything different */
if(data.reset || data.clear) {
if(data.reset) {
renderer.reset();
CORRADE_COMPARE(renderer.mesh().count(), 0);
CORRADE_COMPARE(renderer.cursor(), Vector2{});
} else {
renderer.clear();
CORRADE_COMPARE(renderer.cursor(), (Vector2{5.0f, -3.0f}));
}
/* The index type should stay even after clear / reset */
CORRADE_COMPARE(renderer.indexType(), data.expectedIndexType);
CORRADE_COMPARE(renderer.mesh().count(), 0);
CORRADE_COMPARE(renderer.mesh().indexType(), GL::meshIndexType(data.expectedIndexType));
}
}
void RendererGLTest::renderIndexTypeChanged() {
/* Verifies that an index type change happening inside render() due to
there being too many glyphs is correctly propagated to the GL mesh. A
trimmed-down version of renderClearReset() that verifies just the image
output, because that's the only place where it can be detected. */
GlyphCacheGL glyphCache{PixelFormat::R8Unorm, {2, 2}, {}};
glyphCache.texture()
.setMinificationFilter(SamplerFilter::Nearest)
.setMagnificationFilter(SamplerFilter::Nearest);
struct: AbstractFont {
FontFeatures doFeatures() const override { return {}; }
bool doIsOpened() const override { return _opened; }
void doClose() override { _opened = false; }
Properties doOpenFile(Containers::StringView, Float) override {
_opened = true;
/* Compared to renderClearReset(), the line height is 0 so we can
render the 256 glyph prefix on the same spot without having to
adjust the cursor to place the next line correctly */
return {1.0f, 2.0f, -1.0f, 0.0f, 0};
}
void doGlyphIdsInto(const Containers::StridedArrayView1D<const char32_t>&, const Containers::StridedArrayView1D<UnsignedInt>&) override {}
Vector2 doGlyphSize(UnsignedInt) override { return {}; }
Vector2 doGlyphAdvance(UnsignedInt) override { return {}; }
Containers::Pointer<AbstractShaper> doCreateShaper() override { return {}; }
private:
bool _opened = false;
} font;
font.openFile("", 1.0f);
/* We have many glyphs from the initial prefix, only the first three
resolve to a valid one */
UnsignedInt fontId = glyphCache.addFont(260, &font);
glyphCache.addGlyph(fontId, 3, {},
Range2Di::fromSize({1, 0}, {1, 1}));
glyphCache.addGlyph(fontId, 1, {},
Range2Di::fromSize({1, 1}, {1, 1}));
glyphCache.addGlyph(fontId, 2, {},
Range2Di::fromSize({0, 0}, {1, 2}));
{
const Containers::StridedArrayView2D<UnsignedByte> pixels = glyphCache.image().pixels<UnsignedByte>()[0];
pixels[0][1] = 0xff;
pixels[0][0] = 0x33;
pixels[1][0] = 0x33;
pixels[1][1] = 0x99;
}
glyphCache.flushImage({{}, glyphCache.size()});
MAGNUM_VERIFY_NO_GL_ERROR();
struct: AbstractShaper {
using AbstractShaper::AbstractShaper;
UnsignedInt doShape(Containers::StringView, UnsignedInt begin, UnsignedInt end, Containers::ArrayView<const FeatureRange>) override {
return end - begin;
}
void doGlyphIdsInto(const Containers::StridedArrayView1D<UnsignedInt>& ids) const override {
for(UnsignedInt i = 0; i != ids.size(); ++i)
ids[i] = i + 1;
}
void doGlyphOffsetsAdvancesInto(const Containers::StridedArrayView1D<Vector2>& offsets, const Containers::StridedArrayView1D<Vector2>& advances) const override {
for(UnsignedInt i = 0; i != offsets.size(); ++i) {
advances[i] = {2.0f, 0.0f};
offsets[i] = {0.0f, i == 2 ? 1.0f : 0.0f};
}
}
void doGlyphClustersInto(const Containers::StridedArrayView1D<UnsignedInt>&) const override {}
} shaper{font};
RendererGL renderer{glyphCache};
/* Set a small index type to only have to render 256 glyphs to make it
change, not 16k */
renderer.setIndexType(MeshIndexType::UnsignedByte);
/* 16*16 glyphs. Index type doesn't change yet, only after render(). */
renderer
.setAlignment(Alignment::LineLeft)
.setCursor({-3.0f, 1.0f})
.add(shaper, 1.0f, "0123456789abcdef"_s*16);
CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedByte);
CORRADE_COMPARE(renderer.mesh().indexType(), GL::MeshIndexType::UnsignedByte);
/* This should then cause the index type change, and the GL mesh should
adapt to it */
renderer.render(shaper, 1.0f, "\nabc");
CORRADE_COMPARE(renderer.indexType(), MeshIndexType::UnsignedShort);
CORRADE_COMPARE(renderer.mesh().indexType(), GL::MeshIndexType::UnsignedShort);
/* Just to match the image made in renderClearReset(), nothing else. There
should be 256 + 5 glyphs in total. */
renderer
.setAlignment(Alignment::LineRight)
.setCursor({5.0f, -3.0f})
.render(shaper, 2.0f, "de");
CORRADE_COMPARE(renderer.glyphCount(), 256 + 5);
/* Draw just the suffix from the mesh, not everything */
renderer.mesh()
.setIndexOffset(256*6)
.setCount(5*6);
Shaders::VectorGL2D shader;
shader.setTransformationProjectionMatrix(Matrix3::projection(Vector2{RenderSize}))
.bindVectorTexture(glyphCache.texture())
.draw(renderer.mesh());
MAGNUM_VERIFY_NO_GL_ERROR();
CORRADE_COMPARE_WITH(
/* Use just one channel, the others are always the same */
_framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels<Color4ub>().slice(&Color4ub::r),
Utility::Path::join(TEXT_TEST_DIR, "render.tga"),
(DebugTools::CompareImageToFile{_manager, 0.0f, 0.0f}));
}
struct TestShaper: AbstractShaper {

74
src/Magnum/Text/Test/RendererGL_Test.cpp

@ -0,0 +1,74 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024, 2025
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <Corrade/Containers/String.h>
#include <Corrade/TestSuite/Tester.h>
#include "Magnum/Text/RendererGL.h"
namespace Magnum { namespace Text { namespace Test { namespace {
struct RendererGL_Test: TestSuite::Tester {
explicit RendererGL_Test();
void debugFlag();
void debugFlags();
void constructNoCreate();
};
RendererGL_Test::RendererGL_Test() {
addTests({&RendererGL_Test::debugFlag,
&RendererGL_Test::debugFlags,
&RendererGL_Test::constructNoCreate});
}
void RendererGL_Test::debugFlag() {
Containers::String out;
Debug{&out} << RendererGLFlag::GlyphPositionsClusters << RendererGLFlag(0xca);
CORRADE_COMPARE(out, "Text::RendererGLFlag::GlyphPositionsClusters Text::RendererGLFlag(0xca)\n");
}
void RendererGL_Test::debugFlags() {
Containers::String out;
Debug{&out} << (RendererGLFlag::GlyphPositionsClusters|RendererGLFlag(0xf0)) << RendererGLFlags{};
CORRADE_COMPARE(out, "Text::RendererGLFlag::GlyphPositionsClusters|Text::RendererGLFlag(0xf0) Text::RendererGLFlags{}\n");
}
void RendererGL_Test::constructNoCreate() {
RendererGL renderer{NoCreate};
/* Shouldn't crash */
CORRADE_VERIFY(true);
/* Implicit construction is not allowed */
CORRADE_VERIFY(!std::is_convertible<NoCreateT, RendererGL>::value);
}
}}}}
CORRADE_TEST_MAIN(Magnum::Text::Test::RendererGL_Test)

BIN
src/Magnum/Text/Test/render-nothing.tga

Binary file not shown.

BIN
src/Magnum/Text/Test/render.tga

Binary file not shown.

1
src/Magnum/Text/Text.h

@ -73,6 +73,7 @@ class AbstractRenderer;
template<UnsignedInt> class BasicRenderer;
typedef BasicRenderer<2> Renderer2D;
typedef BasicRenderer<3> Renderer3D;
class RendererGL;
#endif
}}

Loading…
Cancel
Save