From 6707534ce65375a3b79598f6d08f125071ee02c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 4 Oct 2023 16:49:31 +0200 Subject: [PATCH] Text: rework AbstractGlyphCache for better flexibility and efficiency. The class now supports incremental filling, multiple fonts, texture arrays, removes all reliance on STL containers and is finally properly documented. To avoid complete breakage of every use, as much as possible was kept as deprecated APIs -- in particular the reserve() with the nasty std::vectors, the insert() that assumes a 2D cache and a single font and textureSize() that returns a 2D vector. Those behave the same as before, but will assert if the cache is an array or contains more than one font. On the other hand, begin() / end() access with std::unordered_map iterators (ew!) was removed as the internals simply aren't a hashmap anymore. The image() that returned an Image2D is now used to fill the glyph cache instead of querying its potentially processed contents, and returns a MutableImageView3D. I considered keeping it and adding sourceImage() instead, but such naming turned out to be too inconsistent. For querying processed image data (such as with the distance field cache) there's a new processedImage() query, guarded by new GlyphCacheFeature bits -- if both ImageProcessing and ProcessedImageDownload is set, it can be used to retrieve the processed image (so, similar as ImageDownload was before), and if neither is set, the cache contents are queryable directly through image(), without needing any special support from the GPU API. Existing code is updated only in the minimal way possible to ensure that no serious breakage was introduced by reimplementing the deprecated APIs on top of the new backend. Porting away from deprecated APIs will be done in next commits. The GlyphCache and DistanceFieldGlyphCache have their public API kept intact for now, as a similar rework will be needed for them as well. Additionally, the MagnumFont and MagnumFontConverter plugins aren't compiling yet as they require substantial changes to deal with the new glyph cache features. That is not the case with other plugins in the magnum-plugins repository tho, for those the backwards compatibility "just works". On the other hand, since layout of the AbstractGlyphChange changed, I'm bumping the AbstractFont plugin interface version to force-trigger a rebuild of dependent projects. Because I ran a stale magnum-player binary, it worked without crashing or GL errors but just didn't show ANY text whatsoever due to ABI differences, and I wasted some precious minutes before realizing that a simple rebuild would fix it. --- doc/changelog.dox | 41 + doc/snippets/MagnumText-gl.cpp | 7 + doc/snippets/MagnumText.cpp | 92 + src/Magnum/Text/AbstractFont.cpp | 1 + src/Magnum/Text/AbstractFont.h | 2 +- src/Magnum/Text/AbstractGlyphCache.cpp | 478 ++++- src/Magnum/Text/AbstractGlyphCache.h | 786 +++++++- src/Magnum/Text/DistanceFieldGlyphCache.cpp | 62 +- src/Magnum/Text/DistanceFieldGlyphCache.h | 5 + src/Magnum/Text/GlyphCache.cpp | 45 +- src/Magnum/Text/GlyphCache.h | 14 +- .../Text/Test/AbstractFontConverterTest.cpp | 1 + src/Magnum/Text/Test/AbstractFontTest.cpp | 1 + .../Text/Test/AbstractGlyphCacheTest.cpp | 1590 ++++++++++++++++- src/Magnum/Text/Test/CMakeLists.txt | 10 +- .../Test/DistanceFieldGlyphCacheGLTest.cpp | 72 +- src/Magnum/Text/Test/GlyphCacheGLTest.cpp | 112 +- 17 files changed, 3054 insertions(+), 265 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index f75fdcec7..50fa8dbb6 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -1248,6 +1248,35 @@ See also: - @cpp Shaders::PhongGL::setLightColor(const Magnum::Color4&) @ce is deprecated in favor of @ref Shaders::PhongGL::setLightColors() with a single item --- it's short enough to not warrant the existence of a dedicated overload +- The @ref Text library API was reworked for more features, better efficiency + and no dependencies on the STL: + - @cpp Text::GlyphCacheFeature::ImageDownload @ce is deprecated in favor + of @ref Text::GlyphCacheFeature::ProcessedImageDownload, as there's now + a possibility to get the image directly for glyph caches that don't + have @ref Text::GlyphCacheFeature::ImageProcessing + - The @cpp Text::AbstractGlyphCache::AbstractGlyphCache(const Vector2i&, const Vector2i&) @ce + constructor is deprecated in favor of + @ref Text::AbstractGlyphCache::AbstractGlyphCache(PixelFormat, const Vector2i&, const Vector2i&) + that specifies a concrete format for the cache image. + - The @cpp Text::AbstractGlyphCache::reserve() @ce function is deprecated + in favor of operating directly on a @ref TextureTools::AtlasLandfill + instance through @ref Text::AbstractGlyphCache::atlas(). It also forced + the caller to use a @ref std::vector, which wasn't nice at all. The old + API can only be called on 2D glyph caches now, and only when there's no + font added yet. + - @cpp Text::AbstractGlyphCache::insert() @ce function is deprecated in + favor of @ref Text::AbstractGlyphCache::addFont() and + @relativeref{Text::AbstractGlyphCache,addGlyph()} that better expose + multi-font and 2D array support. The old API can only be called on 2D + glyph caches now. + - @cpp Text::AbstractGlyphCache::textureSize() @ce is deprecated in favor + of @ref Text::AbstractGlyphCache::size() which returns a 3D size in + order to support texture arrays as well. The old API can only be called + on 2D glyph caches now. + - @cpp Text::AbstractGlyphCache::setImage() @ce is deprecated, copy glyph + data to slices of @ref Text::AbstractGlyphCache::image() instead and + call @relativeref{Text::AbstractGlyphCache,flushImage()} instead. The + old API can only be called on 2D glyph caches now. - The @cpp TextureTools::atlas() @ce utility is deprecated in favor of @ref TextureTools::AtlasLandfill, which has a vastly better packing efficiency, supports incremental packing and doesn't force the caller to @@ -1598,6 +1627,18 @@ See also: and @ref Shaders::DistanceFieldVectorGL is removed, as its benefits were rather questionable --- on the contrary, it made subclass implementation more verbose and less clear +- The @ref Text library API was reworked for more features, better efficiency + and no dependencies on the STL: + - The @cpp Text::AbstractGlyphCache::begin() @ce / @cpp end() @ce access + via @ref std::unordered_map iterators is removed. Use + @ref Text::AbstractGlyphCache::glyph() for accessing properties of a + particular glyph, for accessing all glyph data then + @relativeref{Text::AbstractGlyphCache,glyphIdsInto()}, + @relativeref{Text::AbstractGlyphCache,glyphOffsets() const} and + @relativeref{Text::AbstractGlyphCache,glyphRectangles() const}. + - The @ref Text::AbstractGlyphCache::image() query now returns a + @ref MutableImageView3D instead of an @ref Image2D in order to support + incremental population and texture arrays - The @ref Trade::AnimationTrackTarget enum was extended from 8 to 16 bits to provide more room for custom targets, consistently with @ref Trade::MeshAttribute. diff --git a/doc/snippets/MagnumText-gl.cpp b/doc/snippets/MagnumText-gl.cpp index 91f7b2350..8236367a9 100644 --- a/doc/snippets/MagnumText-gl.cpp +++ b/doc/snippets/MagnumText-gl.cpp @@ -26,6 +26,7 @@ #include #include +#include "Magnum/PixelFormat.h" #include "Magnum/Math/Color.h" #include "Magnum/Math/Matrix3.h" #include "Magnum/Shaders/VectorGL.h" @@ -61,6 +62,12 @@ font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" /* [AbstractFont-usage] */ } +{ +/* [AbstractGlyphCache-filling-construct] */ +Text::GlyphCache cache{Vector2i{512}}; +/* [AbstractGlyphCache-filling-construct] */ +} + { /* -Wnonnull in GCC 11+ "helpfully" says "this is null" if I don't initialize the font pointer. I don't care, I just want you to check compilation errors, diff --git a/doc/snippets/MagnumText.cpp b/doc/snippets/MagnumText.cpp index 3c521ed68..4dc9ea552 100644 --- a/doc/snippets/MagnumText.cpp +++ b/doc/snippets/MagnumText.cpp @@ -31,17 +31,24 @@ #include #include #include +#include #include #include /** @todo remove once file callbacks are -free */ +#include #include +#include #include #include #include "Magnum/FileCallback.h" +#include "Magnum/ImageView.h" #include "Magnum/Math/Color.h" #include "Magnum/Math/Matrix3.h" +#include "Magnum/Math/Range.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractFontConverter.h" +#include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/TextureTools/Atlas.h" #define DOXYGEN_ELLIPSIS(...) __VA_ARGS__ @@ -162,4 +169,89 @@ font->setFileCallback([](const std::string& filename, /* [AbstractFont-setFileCallback-template] */ } +{ +struct: Text::AbstractGlyphCache { + using Text::AbstractGlyphCache::AbstractGlyphCache; + + Text::GlyphCacheFeatures doFeatures() const override { return {}; } +} cache{Vector2i{256}}; +/* [AbstractGlyphCache-filling-images] */ +Containers::ArrayView images = DOXYGEN_ELLIPSIS({}); +/* [AbstractGlyphCache-filling-images] */ + +/* [AbstractGlyphCache-filling-font] */ +UnsignedInt fontId = cache.addFont(images.size()); +/* [AbstractGlyphCache-filling-font] */ + +/* [AbstractGlyphCache-filling-atlas] */ +Containers::Array offsets{NoInit, images.size()}; + +cache.atlas().clearFlags( + TextureTools::AtlasLandfillFlag::RotatePortrait| + TextureTools::AtlasLandfillFlag::RotateLandscape); +CORRADE_INTERNAL_ASSERT(cache.atlas().add( + stridedArrayView(images).slice(&ImageView2D::size), + offsets)); +/* [AbstractGlyphCache-filling-atlas] */ + +/* [AbstractGlyphCache-filling-glyphs] */ +/* The glyph cache is just 2D, so copying to the first slice */ +Containers::StridedArrayView3D dst = cache.image().pixels()[0]; +Range2Di updated; +for(UnsignedInt fontGlyphId = 0; fontGlyphId != images.size(); ++fontGlyphId) { + Range2Di rectangle = Range2Di::fromSize(offsets[fontGlyphId], + images[fontGlyphId].size()); + cache.addGlyph(fontId, fontGlyphId, {}, rectangle); + + /* Copy assuming all input images have the same pixel format */ + Containers::StridedArrayView3D src = images[fontGlyphId].pixels(); + Utility::copy(src, dst.sliceSize({ + std::size_t(offsets[fontGlyphId].y()), + std::size_t(offsets[fontGlyphId].x()), + 0}, src.size())); + + /* Maintain a range that was updated in the glyph cache */ + updated = Math::join(updated, rectangle); +} + +/* Reflect the image data update to the actual GPU-side texture */ +cache.flushImage(updated); +/* [AbstractGlyphCache-filling-glyphs] */ +} + +{ +struct: Text::AbstractGlyphCache { + using Text::AbstractGlyphCache::AbstractGlyphCache; + + Text::GlyphCacheFeatures doFeatures() const override { return {}; } +} cacheInstance{Vector2i{256}}; +/* [AbstractGlyphCache-querying] */ +Containers::Pointer font = DOXYGEN_ELLIPSIS({}); +Text::AbstractGlyphCache& cache = DOXYGEN_ELLIPSIS(cacheInstance); + +Containers::ArrayView fontGlyphIds = DOXYGEN_ELLIPSIS({}); + +Containers::Optional fontId = cache.findFont(font.get()); +DOXYGEN_ELLIPSIS() +for(std::size_t i = 0; i != fontGlyphIds.size(); ++i) { + Containers::Triple glyph = + cache.glyph(*fontId, fontGlyphIds[i]); + DOXYGEN_ELLIPSIS(static_cast(glyph);) +} +/* [AbstractGlyphCache-querying] */ + +/* [AbstractGlyphCache-querying-batch] */ +Containers::Array glyphIds{NoInit, fontGlyphIds.size()}; +cache.glyphIdsInto(*fontId, fontGlyphIds, glyphIds); + +Containers::StridedArrayView1D offsets = cache.glyphOffsets(); +Containers::StridedArrayView1D rects = cache.glyphRectangles(); +for(std::size_t i = 0; i != fontGlyphIds.size(); ++i) { + Vector2i offset = offsets[glyphIds[i]]; + Range2Di rectangle = rects[glyphIds[i]]; + DOXYGEN_ELLIPSIS(static_cast(offset); static_cast(rectangle);) +} +/* [AbstractGlyphCache-querying-batch] */ +} + } diff --git a/src/Magnum/Text/AbstractFont.cpp b/src/Magnum/Text/AbstractFont.cpp index 45b539683..d96f3ac15 100644 --- a/src/Magnum/Text/AbstractFont.cpp +++ b/src/Magnum/Text/AbstractFont.cpp @@ -39,6 +39,7 @@ #include "Magnum/FileCallback.h" #include "Magnum/Math/Functions.h" +#include "Magnum/Math/Range.h" #include "Magnum/Text/AbstractGlyphCache.h" #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT diff --git a/src/Magnum/Text/AbstractFont.h b/src/Magnum/Text/AbstractFont.h index 027459d14..b81838c97 100644 --- a/src/Magnum/Text/AbstractFont.h +++ b/src/Magnum/Text/AbstractFont.h @@ -747,7 +747,7 @@ updated interface string. */ /* Silly indentation to make the string appear in pluginInterface() docs */ #define MAGNUM_TEXT_ABSTRACTFONT_PLUGIN_INTERFACE /* [interface] */ \ -"cz.mosra.magnum.Text.AbstractFont/0.3.2" +"cz.mosra.magnum.Text.AbstractFont/0.3.3" /* [interface] */ #ifndef DOXYGEN_GENERATING_OUTPUT diff --git a/src/Magnum/Text/AbstractGlyphCache.cpp b/src/Magnum/Text/AbstractGlyphCache.cpp index 523a56863..22b82250f 100644 --- a/src/Magnum/Text/AbstractGlyphCache.cpp +++ b/src/Magnum/Text/AbstractGlyphCache.cpp @@ -25,34 +25,254 @@ #include "AbstractGlyphCache.h" +#include +#include +#include +#include +#include #include -#include /** @todo remove once std::vector is gone */ #include "Magnum/Image.h" #include "Magnum/ImageView.h" #include "Magnum/PixelFormat.h" +#include "Magnum/Math/Range.h" #include "Magnum/TextureTools/Atlas.h" +#ifdef MAGNUM_BUILD_DEPRECATED +#include +#include +#endif + namespace Magnum { namespace Text { -AbstractGlyphCache::AbstractGlyphCache(const Vector2i& size, const Vector2i& padding): _size{size}, _padding{padding} { - /* Default "Not Found" glyph. Can't do just `.insert({0, {}})` because - that's ambiguous in C++17, due to a new insert(node_type&&) overload. */ - glyphs.insert({0, std::pair{}}); +Debug& operator<<(Debug& debug, const GlyphCacheFeature value) { + debug << "Text::GlyphCacheFeature" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case GlyphCacheFeature::v: return debug << "::" #v; + _c(ImageProcessing) + _c(ProcessedImageDownload) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedByte(value)) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const GlyphCacheFeatures value) { + return Containers::enumSetDebugOutput(debug, value, "Text::GlyphCacheFeatures{}", { + /* ProcessedImageDownload is a superset of ImageProcessing, has to be + first */ + GlyphCacheFeature::ProcessedImageDownload, + GlyphCacheFeature::ImageProcessing + }); +} + +struct AbstractGlyphCache::State { + explicit State(PixelFormat format, const Vector3i& size, const Vector2i& padding): image{format, size, Containers::Array{ValueInit, 4*((pixelFormatSize(format)*size.x() + 3)/4)*size.y()*size.z()}}, atlas{size}, padding{padding} { + /* Flags are currently cleared as well, will be enabled back in a later + step once the behavior is specified (with negative ranges) and + Math::join() is fixed to handle those correctly. */ + atlas.setPadding(padding) + .clearFlags(TextureTools::AtlasLandfillFlag::RotatePortrait| + TextureTools::AtlasLandfillFlag::RotateLandscape); + } + + Image3D image; + TextureTools::AtlasLandfill atlas; + + Vector2i padding; + + /* First element is glyph position relative to a point on the baseline, + second layer in the texture atlas, third a region in the atlas + slice. Index of the item is ID of the glyph in the cache, refered to + from the fontGlyphMapping array. Index 0 is reserved for an invalid + glyph. */ + Containers::Array> glyphs; + /* `fontRanges[i]` to `fontRanges[i + 1]` is the range in + `fontGlyphMapping` containing a mapping for glyphs from font `i`, + `fontGlyphMapping[fontRanges[i]] + j` is then mapping from glyph ID `j` + from font `i` to index in the `glyphs` array, or is 0 if given + glyph isn't present in the cache (which then maps to the invalid + glyph). */ + struct Font { + UnsignedInt offset; + /* 4 bytes free on 64b, but not so critical I think */ + const AbstractFont* pointer; + }; + Containers::Array fonts; + /* With an assumption that majority of font glyphs get put into a cache, + this achieves O(1) mapping from a font ID + font-specific glyph ID pair + to a cache-global glyph ID with far less overhead than a hashmap would, + and much less memory used as well compared to storing a key, value and a + hash for each mapping entry. + + Another assumption is that there's no more than 64k glyphs in total, + which makes the mapping save half memory compared to storing 32-bit + ints. 64K glyphs is enough to fill a 4K texture with 16x16 glyphs, which + seems enough for now. It however might get reached at some point in + practice, in which case the type would simply get changed to a 32-bit + one (and the assertion in addGlyph() then removed). */ + Containers::Array fontGlyphMapping; +}; + +AbstractGlyphCache::AbstractGlyphCache(const PixelFormat format, const Vector3i& size, const Vector2i& padding) { + CORRADE_ASSERT(size.product(), + "Text::AbstractGlyphCache: expected non-zero size, got" << Debug::packed << size, ); + + /* Creating the state only after the assert as the AtlasLandfill would + assert on zero size as well */ + _state.emplace(format, size, padding); + + /* Default invalid glyph -- empty / zero-area */ + arrayAppend(_state->glyphs, InPlaceInit); + + /* There are no fonts yet */ + arrayAppend(_state->fonts, InPlaceInit, 0u, nullptr); } +AbstractGlyphCache::AbstractGlyphCache(const PixelFormat format, const Vector3i& size): AbstractGlyphCache{format, size, {}} {} + +AbstractGlyphCache::AbstractGlyphCache(const PixelFormat format, const Vector2i& size, const Vector2i& padding): AbstractGlyphCache{format, Vector3i{size, 1}, padding} {} + +AbstractGlyphCache::AbstractGlyphCache(const PixelFormat format, const Vector2i& size): AbstractGlyphCache{format, size, {}} {} + +#ifdef MAGNUM_BUILD_DEPRECATED +AbstractGlyphCache::AbstractGlyphCache(const Vector2i& size, const Vector2i& padding): AbstractGlyphCache{PixelFormat::R8Unorm, size, padding} {} + +AbstractGlyphCache::AbstractGlyphCache(const Vector2i& size): AbstractGlyphCache{PixelFormat::R8Unorm, size, {}} {} +#endif + +AbstractGlyphCache::AbstractGlyphCache(AbstractGlyphCache&&) noexcept = default; + AbstractGlyphCache::~AbstractGlyphCache() = default; +AbstractGlyphCache& AbstractGlyphCache::operator=(AbstractGlyphCache&&) noexcept = default; + +PixelFormat AbstractGlyphCache::format() const { + return _state->image.format(); +} + +Vector3i AbstractGlyphCache::size() const { + return _state->image.size(); +} + +#ifdef MAGNUM_BUILD_DEPRECATED +Vector2i AbstractGlyphCache::textureSize() const { + CORRADE_ASSERT(_state->image.size().z() == 1, + "Text::AbstractGlyphCache::textureSize(): can't be used on an array glyph cache", {}); + return _state->image.size().xy(); +} +#endif + +Vector2i AbstractGlyphCache::padding() const { + return _state->padding; +} + +UnsignedInt AbstractGlyphCache::fontCount() const { + return _state->fonts.size() - 1; +} + +UnsignedInt AbstractGlyphCache::glyphCount() const { + return _state->glyphs.size(); +} + +TextureTools::AtlasLandfill& AbstractGlyphCache::atlas() { + return _state->atlas; +} + +const TextureTools::AtlasLandfill& AbstractGlyphCache::atlas() const { + return _state->atlas; +} + +void AbstractGlyphCache::setInvalidGlyph(const Vector2i& offset, const Int layer, const Range2Di& rectangle) { + State& state = *_state; + /** @todo expand once rotations (and thus negative rectangle sizes) are + supported */ + const Range2Di rectanglePadded = rectangle.padded(state.padding); + #ifndef CORRADE_NO_ASSERT + const Range2Dui rectangleu{rectangle}; + const Range2Dui rectanglePaddedu{rectanglePadded}; + #endif + CORRADE_ASSERT(UnsignedInt(layer) < UnsignedInt(state.image.size().z()) && (rectangleu.min() <= rectangleu.max()).all() && (rectanglePaddedu.min() <= Vector2ui{state.image.size().xy()}).all() && (rectanglePaddedu.max() <= Vector2ui{state.image.size().xy()}).all(), + "Text::AbstractGlyphCache::setInvalidGlyph(): layer" << layer << "and rectangle" << Debug::packed << rectangle << "out of range for size" << Debug::packed << state.image.size() << "and padding" << Debug::packed << state.padding, ); + + state.glyphs[0] = {offset - _state->padding, layer, rectanglePadded}; +} + +void AbstractGlyphCache::setInvalidGlyph(const Vector2i& offset, const Range2Di& rectangle) { + CORRADE_ASSERT(_state->image.size().z() == 1, + "Text::AbstractGlyphCache::setInvalidGlyph(): use the layer overload for an array glyph cache", ); + setInvalidGlyph(offset, 0, rectangle); +} + +UnsignedInt AbstractGlyphCache::addFont(const UnsignedInt glyphCount, const AbstractFont* const pointer) { + State& state = *_state; + + #ifndef CORRADE_NO_ASSERT + for(UnsignedInt i = 0; i != state.fonts.size() - 1; ++i) + CORRADE_ASSERT(!pointer || state.fonts[i].pointer != pointer, + "Text::AbstractGlyphCache::addFont(): pointer" << pointer << "already used for font" << i, {}); + #endif + + /* The last item in the font array now becomes the new font (and its offset + should be the size of the fontGlyphMapping array), assign the pointer to + it. Add a new item after which is the end offset sentinel. */ + CORRADE_INTERNAL_ASSERT(state.fontGlyphMapping.size() == state.fonts.back().offset); + state.fonts.back().pointer = pointer; + arrayAppend(state.fonts, InPlaceInit, + state.fonts.back().offset + glyphCount, + nullptr); + + /** @todo er, some arrayAppend with ValueInit + count? there's already + arrayResize() with the same signature */ + for(UnsignedShort& i: arrayAppend(state.fontGlyphMapping, NoInit, glyphCount)) + i = 0; + return state.fonts.size() - 2; +} + +UnsignedInt AbstractGlyphCache::fontGlyphCount(const UnsignedInt fontId) const { + const State& state = *_state; + CORRADE_ASSERT(fontId < state.fonts.size() - 1, + "Text::AbstractGlyphCache::fontGlyphCount(): index" << fontId << "out of range for" << state.fonts.size() - 1 << "fonts", {}); + return state.fonts[fontId + 1].offset - state.fonts[fontId].offset; +} + +const AbstractFont* AbstractGlyphCache::fontPointer(const UnsignedInt fontId) const { + const State& state = *_state; + CORRADE_ASSERT(fontId < state.fonts.size() - 1, + "Text::AbstractGlyphCache::fontPointer(): index" << fontId << "out of range for" << state.fonts.size() - 1 << "fonts", {}); + return state.fonts[fontId].pointer; +} + +Containers::Optional AbstractGlyphCache::findFont(const AbstractFont* pointer) const { + CORRADE_ASSERT(pointer, + "Text::AbstractGlyphCache::findFont(): expected a non-null pointer", {}); + const State& state = *_state; + for(UnsignedInt i = 0; i != state.fonts.size() - 1; ++i) + if(state.fonts[i].pointer == pointer) return i; + return {}; +} + +#ifdef MAGNUM_BUILD_DEPRECATED std::vector AbstractGlyphCache::reserve(const std::vector& sizes) { - CORRADE_ASSERT((glyphs.size() == 1 && glyphs.at(0) == std::pair()), + State& state = *_state; + CORRADE_ASSERT(state.image.size().z() == 1, + "Text::AbstractGlyphCache::reserve(): can't be used on an array glyph cache", {}); + /* This is technically possible now, but we just don't bother for the + compatibility API as it would need to be additionally tested */ + CORRADE_ASSERT(state.fonts.size() == 1, "Text::AbstractGlyphCache::reserve(): reserving space in non-empty cache is not yet implemented", {}); - glyphs.reserve(glyphs.size() + sizes.size()); - /* Rotations are not yet supported */ - TextureTools::AtlasLandfill atlas{_size}; - atlas.setPadding(_padding) - .clearFlags(TextureTools::AtlasLandfillFlag::RotatePortrait| - TextureTools::AtlasLandfillFlag::RotateLandscape); + /* Append an empty font range just to prevent reserve() from being called + again */ + arrayAppend(state.fonts, InPlaceInit, 0u, nullptr); + + /* Disable rotations in the atlas as the old API doesn't expect them */ + const TextureTools::AtlasLandfillFlags previousFlags = state.atlas.flags(); + state.atlas.clearFlags(TextureTools::AtlasLandfillFlag::RotatePortrait| + TextureTools::AtlasLandfillFlag::RotateLandscape); /* Create the output array. Because the new atlas packer doesn't accept zero sizes, change those to be (1, 1) instead. A new interface will @@ -62,14 +282,19 @@ std::vector AbstractGlyphCache::reserve(const std::vector& s for(std::size_t i = 0; i != sizes.size(); ++i) out[i].max() = Math::max(sizes[i], Vector2i{1}); + const bool succeeded = state.atlas.add( + Containers::stridedArrayView(out).slice(&Range2Di::max), + Containers::stridedArrayView(out).slice(&Range2Di::min)); + + /* Restore previous flags back */ + state.atlas.setFlags(previousFlags); + /* The error message matches what the old TextureTools::atlas() did. Not great, but that's the interface we should stay compatible with. */ - if(!atlas.add(Containers::stridedArrayView(out).slice(&Range2Di::max), - Containers::stridedArrayView(out).slice(&Range2Di::min))) - { + if(!succeeded) { Error{} << "Text::AbstractGlyphCache::reserve(): requested atlas size" - << _size << "is too small to fit" << sizes.size() - << "textures. Generated atlas will be empty."; + << state.image.size().xy() << "is too small to fit" + << sizes.size() << "textures. Generated atlas will be empty."; return {}; } @@ -80,33 +305,224 @@ std::vector AbstractGlyphCache::reserve(const std::vector& s return out; } +#endif + +UnsignedInt AbstractGlyphCache::addGlyph(const UnsignedInt fontId, const UnsignedInt fontGlyphId, const Vector2i& offset, const Int layer, const Range2Di& rectangle) { + State& state = *_state; + CORRADE_ASSERT(fontId < state.fonts.size() - 1, + "Text::AbstractGlyphCache::addGlyph(): index" << fontId << "out of range for" << state.fonts.size() - 1 << "fonts", {}); + const UnsignedInt fontOffset = state.fonts[fontId].offset; + CORRADE_ASSERT(fontGlyphId < state.fonts[fontId + 1].offset - fontOffset, + "Text::AbstractGlyphCache::addGlyph(): index" << fontGlyphId << "out of range for" << state.fonts[fontId + 1].offset - fontOffset << "glyphs in font" << fontId, {}); + CORRADE_ASSERT(!state.fontGlyphMapping[fontOffset + fontGlyphId], + "Text::AbstractGlyphCache::addGlyph(): glyph" << fontGlyphId << "in font" << fontId << "already added at index" << state.fontGlyphMapping[fontOffset + fontGlyphId], {}); + /** @todo expand once rotations (and thus negative rectangle sizes) are + supported */ + const Range2Di rectanglePadded = rectangle.padded(state.padding); + #ifndef CORRADE_NO_ASSERT + const Range2Dui rectangleu{rectangle}; + const Range2Dui rectanglePaddedu{rectanglePadded}; + #endif + CORRADE_ASSERT(UnsignedInt(layer) < UnsignedInt(state.image.size().z()) && (rectangleu.min() <= rectangleu.max()).all() && (rectanglePaddedu.min() <= Vector2ui{state.image.size().xy()}).all() && (rectanglePaddedu.max() <= Vector2ui{state.image.size().xy()}).all(), + "Text::AbstractGlyphCache::addGlyph(): layer" << layer << "and rectangle" << Debug::packed << rectangle << "out of range for size" << Debug::packed << state.image.size() << "and padding" << Debug::packed << state.padding, {}); + + const UnsignedInt glyphId = state.glyphs.size(); + /* The fontGlyphMapping entries are 16-bit to save memory, can't have IDs + beyond that. See its documentation for more reasoning. */ + CORRADE_ASSERT(glyphId < 65536, + "Text::AbstractGlyphCache::addGlyph(): only at most 65536 glyphs can be added", {}); + state.fontGlyphMapping[fontOffset + fontGlyphId] = glyphId; + arrayAppend(state.glyphs, InPlaceInit, offset - _state->padding, layer, rectangle.padded(_state->padding)); + return glyphId; +} + +UnsignedInt AbstractGlyphCache::addGlyph(const UnsignedInt fontId, const UnsignedInt fontGlyphId, const Vector2i& offset, const Range2Di& rectangle) { + CORRADE_ASSERT(_state->image.size().z() == 1, + "Text::AbstractGlyphCache::addGlyph(): use the layer overload for an array glyph cache", {}); + return addGlyph(fontId, fontGlyphId, offset, 0, rectangle); +} -void AbstractGlyphCache::insert(const UnsignedInt glyph, const Vector2i& position, const Range2Di& rectangle) { - const std::pair glyphData = {position-_padding, rectangle.padded(_padding)}; +#ifdef MAGNUM_BUILD_DEPRECATED +void AbstractGlyphCache::insert(const UnsignedInt glyph, const Vector2i& offset, const Range2Di& rectangle) { + State& state = *_state; + CORRADE_ASSERT(state.image.size().z() == 1, + "Text::AbstractGlyphCache::insert(): can't be used on an array glyph cache", ); + CORRADE_ASSERT(state.fonts.size() <= 2, + "Text::AbstractGlyphCache::insert(): can't be used on a multi-font glyph cache", ); /* Overwriting "Not Found" glyph */ - if(glyph == 0) glyphs[0] = glyphData; + if(glyph == 0) { + setInvalidGlyph(offset, rectangle); + + /* Inserting new glyph. Add the first ever font and ajust the font range if + needed. */ + } else { + if(state.fonts.size() == 1) + arrayAppend(_state->fonts, InPlaceInit, 0u, nullptr); + if(glyph >= state.fonts[1].offset) { + arrayResize(state.fontGlyphMapping, glyph + 1); + state.fonts[1].offset = glyph + 1; + } + + addGlyph(0, glyph, offset, rectangle); + } +} +#endif + +MutableImageView3D AbstractGlyphCache::image() { + return _state->image; +} + +ImageView3D AbstractGlyphCache::image() const { + return _state->image; +} + +void AbstractGlyphCache::flushImage(const Range3Di& range) { + State& state = *_state; + #ifndef CORRADE_NO_ASSERT + const Range3Dui rangeu{range}; + #endif + CORRADE_ASSERT((rangeu.min() <= rangeu.max()).all() && (rangeu.max() <= Vector3ui{state.image.size()}).all(), + "Text::AbstractGlyphCache::flushImage():" << Debug::packed << range << "out of range for size" << Debug::packed << state.image.size(), ); + + /** @todo ugh have slicing on images directly already */ + PixelStorage storage; + storage.setRowLength(state.image.size().x()) + .setSkip(range.min()); + /* Set image height only if it's an array glyph cache, as otherwise it'd + cause errors on ES2 that doesn't support this pixel storage state */ + if(state.image.size().z() != 1) + storage.setImageHeight(state.image.size().y()); + doSetImage(range.min(), ImageView3D{ + storage, + state.image.format(), + range.size(), + state.image.data()}); +} + +void AbstractGlyphCache::flushImage(Int layer, const Range2Di& range) { + flushImage({{range.min(), layer}, + {range.max(), layer + 1}}); +} + +void AbstractGlyphCache::flushImage(const Range2Di& range) { + CORRADE_ASSERT(_state->image.size().z() == 1, + "Text::AbstractGlyphCache::flushImage(): use the 3D or layer overload for an array glyph cache", ); + flushImage(0, range); +} + +void AbstractGlyphCache::doSetImage(const Vector3i& offset, const ImageView3D& image) { + if(_state->image.size().z() == 1) + /** @todo ugh have slicing on images directly already */ + return doSetImage(offset.xy(), ImageView2D{image.storage(), image.format(), image.size().xy(), image.data()}); - /* Inserting new glyph */ - else CORRADE_INTERNAL_ASSERT_OUTPUT(glyphs.insert({glyph, glyphData}).second); + CORRADE_ASSERT_UNREACHABLE("Text::AbstractGlyphCache::image(): not implemented by derived class", ); } +void AbstractGlyphCache::doSetImage(const Vector2i&, const ImageView2D&) { + CORRADE_ASSERT_UNREACHABLE("Text::AbstractGlyphCache::image(): not implemented by derived class", ); +} + +#ifdef MAGNUM_BUILD_DEPRECATED void AbstractGlyphCache::setImage(const Vector2i& offset, const ImageView2D& image) { - CORRADE_ASSERT((offset >= Vector2i{} && offset + image.size() <= _size).all(), - "Text::AbstractGlyphCache::setImage():" << Range2Di::fromSize(offset, image.size()) << "out of range for texture size" << _size, ); + State& state = *_state; + CORRADE_ASSERT(state.image.size().z() == 1, + "Text::AbstractGlyphCache::setImage(): can't be used on an array glyph cache", ); + CORRADE_ASSERT((offset >= Vector2i{} && offset + image.size() <= state.image.size().xy()).all(), + "Text::AbstractGlyphCache::setImage():" << Range2Di::fromSize(offset, image.size()) << "out of range for glyph cache of size" << state.image.size().xy(), ); + CORRADE_ASSERT(image.format() == state.image.format(), + "Text::AbstractGlyphCache::setImage(): expected" << state.image.format() << "but got" << image.format(), ); + const Containers::StridedArrayView3D src = image.pixels(); + Utility::copy(src, + state.image.pixels()[0].sliceSize({std::size_t(offset.y()), + std::size_t(offset.x()), + 0}, src.size())); + flushImage(Range2Di::fromSize(offset, image.size())); +} +#endif - doSetImage(offset, image); +Image3D AbstractGlyphCache::processedImage() { + CORRADE_ASSERT(features() >= GlyphCacheFeature::ProcessedImageDownload, + "Text::AbstractGlyphCache::processedImage(): feature not supported", Image3D{PixelFormat::R8Unorm}); + + return doProcessedImage(); +} + +Image3D AbstractGlyphCache::doProcessedImage() { + CORRADE_ASSERT_UNREACHABLE("Text::AbstractGlyphCache::processedImage(): feature advertised but not implemented", Image3D{PixelFormat::R8Unorm}); } -Image2D AbstractGlyphCache::image() { - CORRADE_ASSERT(features() & GlyphCacheFeature::ImageDownload, - "Text::AbstractGlyphCache::image(): feature not supported", Image2D{PixelFormat::R8Unorm}); +UnsignedInt AbstractGlyphCache::glyphId(const UnsignedInt fontId, const UnsignedInt fontGlyphId) const { + const State& state = *_state; + CORRADE_DEBUG_ASSERT(fontId < state.fonts.size() - 1, + "Text::AbstractGlyphCache::glyphId(): index" << fontId << "out of range for" << state.fonts.size() - 1 << "fonts", {}); + const UnsignedInt fontOffset = state.fonts[fontId].offset; + CORRADE_DEBUG_ASSERT(fontGlyphId < state.fonts[fontId + 1].offset - fontOffset, + "Text::AbstractGlyphCache::glyphId(): index" << fontGlyphId << "out of range for" << state.fonts[fontId + 1].offset - fontOffset << "glyphs in font" << fontId, {}); + return state.fontGlyphMapping[fontOffset + fontGlyphId]; +} + +void AbstractGlyphCache::glyphIdsInto(const UnsignedInt fontId, const Containers::StridedArrayView1D& fontGlyphIds, const Containers::StridedArrayView1D& glyphIds) const { + CORRADE_ASSERT(fontGlyphIds.size() == glyphIds.size(), + "Text::AbstractGlyphCache::glyphIdsInto(): expected fontGlyphIds and glyphIds views to have the same size but got" << fontGlyphIds.size() << "and" << glyphIds.size(), ); + const State& state = *_state; + CORRADE_ASSERT(fontId < state.fonts.size() - 1, + "Text::AbstractGlyphCache::glyphIdsInto(): index" << fontId << "out of range for" << state.fonts.size() - 1 << "fonts", ); + const UnsignedInt fontOffset = state.fonts[fontId].offset; + #ifndef CORRADE_NO_DEBUG_ASSERT + const UnsignedInt fontGlyphCount = state.fonts[fontId + 1].offset - fontOffset; + #endif + + for(std::size_t i = 0; i != fontGlyphIds.size(); ++i) { + const UnsignedInt fontGlyphId = fontGlyphIds[i]; + CORRADE_DEBUG_ASSERT(fontGlyphId < fontGlyphCount, + "Text::AbstractGlyphCache::glyphIdsInto(): glyph" << i << "index" << fontGlyphId << "out of range for" << fontGlyphCount << "glyphs in font" << fontId, ); + glyphIds[i] = state.fontGlyphMapping[fontOffset + fontGlyphId]; + } +} + +void AbstractGlyphCache::glyphIdsInto(const UnsignedInt fontId, const std::initializer_list fontGlyphIds, const Containers::StridedArrayView1D& glyphIds) const { + return glyphIdsInto(fontId, Containers::arrayView(fontGlyphIds), glyphIds); +} + +Containers::StridedArrayView1D AbstractGlyphCache::glyphOffsets() const { + return stridedArrayView(_state->glyphs).slice(&Containers::Triple::first); +} + +Containers::StridedArrayView1D AbstractGlyphCache::glyphLayers() const { + return stridedArrayView(_state->glyphs).slice(&Containers::Triple::second); +} + +Containers::StridedArrayView1D AbstractGlyphCache::glyphRectangles() const { + return stridedArrayView(_state->glyphs).slice(&Containers::Triple::third); +} + +Containers::Triple AbstractGlyphCache::glyph(const UnsignedInt fontId, const UnsignedInt fontGlyphId) const { + const State& state = *_state; + CORRADE_DEBUG_ASSERT(fontId < state.fonts.size() - 1, + "Text::AbstractGlyphCache::glyph(): index" << fontId << "out of range for" << state.fonts.size() - 1 << "fonts", {}); + const UnsignedInt fontOffset = state.fonts[fontId].offset; + CORRADE_DEBUG_ASSERT(fontGlyphId < state.fonts[fontId + 1].offset - fontOffset, + "Text::AbstractGlyphCache::glyph(): index" << fontGlyphId << "out of range for" << state.fonts[fontId + 1].offset - fontOffset << "glyphs in font" << fontId, {}); + return state.glyphs[state.fontGlyphMapping[fontOffset + fontGlyphId]]; +} - return doImage(); +Containers::Triple AbstractGlyphCache::glyph(const UnsignedInt glyphId) const { + const State& state = *_state; + CORRADE_DEBUG_ASSERT(glyphId < state.glyphs.size(), + "Text::AbstractGlyphCache::glyph(): index" << glyphId << "out of range for" << state.glyphs.size() << "glyphs", {}); + return state.glyphs[glyphId]; } -Image2D AbstractGlyphCache::doImage() { - CORRADE_ASSERT_UNREACHABLE("Text::AbstractGlyphCache::image(): feature advertised but not implemented", Image2D{PixelFormat::R8Unorm}); +#ifdef MAGNUM_BUILD_DEPRECATED +std::pair AbstractGlyphCache::operator[](const UnsignedInt glyphId) const { + const State& state = *_state; + CORRADE_ASSERT(state.image.size().z() == 1, + "Text::AbstractGlyphCache::operator[](): can't be used on an array glyph cache", {}); + const Containers::Triple out = + glyphId && glyphId < state.fonts[1].offset ? glyph(0, glyphId) : glyph(0); + return {out.first(), out.third()}; } +#endif }} diff --git a/src/Magnum/Text/AbstractGlyphCache.h b/src/Magnum/Text/AbstractGlyphCache.h index edc551c61..5fbc1c910 100644 --- a/src/Magnum/Text/AbstractGlyphCache.h +++ b/src/Magnum/Text/AbstractGlyphCache.h @@ -26,16 +26,22 @@ */ /** @file - * @brief Class @ref Magnum::Text::AbstractGlyphCache + * @brief Class @ref Magnum::Text::AbstractGlyphCache, enum @ref Magnum::Text::GlyphCacheFeature, enum set @ref Magnum::Text::GlyphCacheFeatures * @m_since{2019,10} */ -#include -#include +#include +#include #include "Magnum/Magnum.h" -#include "Magnum/Math/Range.h" +#include "Magnum/Text/Text.h" #include "Magnum/Text/visibility.h" +#include "Magnum/TextureTools/TextureTools.h" + +#ifdef MAGNUM_BUILD_DEPRECATED +#include +#include +#endif namespace Magnum { namespace Text { @@ -47,14 +53,35 @@ namespace Magnum { namespace Text { */ enum class GlyphCacheFeature: UnsignedByte { /** - * Ability to download glyph cache data using - * @ref AbstractGlyphCache::image(). May not be supported by glyph caches - * on embedded platforms that don't have an ability to get texture data - * back from a GPU. + * The glyph cache processes the input image, potentially to a different + * size or format. + * @m_since_latest + */ + ImageProcessing = 1 << 0, + + /** + * Ability to download processed image data using + * @ref AbstractGlyphCache::processedImage(). May not be supported by glyph + * caches on embedded platforms that don't have an ability to get texture + * data back from a GPU. Implies @ref GlyphCacheFeature::ImageProcessing. + * Glyph caches without @ref GlyphCacheFeature::ImageProcessing have the + * image accessible always through @ref AbstractGlyphCache::image(). + * @m_since_latest */ - ImageDownload = 1 << 0 + ProcessedImageDownload = ImageProcessing|(1 << 1), + + #ifdef MAGNUM_BUILD_DEPRECATED + /** + * @m_deprecated_since_latest Use + * @ref GlyphCacheFeature::ProcessedImageDownload instead. + */ + ImageDownload CORRADE_DEPRECATED_ENUM("use ProcessedImageDownload instead") = ProcessedImageDownload + #endif }; +/** @debugoperatorenum{GlyphCacheFeature} */ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, GlyphCacheFeature value); + /** @brief Set of features supported by a glyph cache @m_since{2019,10} @@ -65,141 +92,742 @@ typedef Containers::EnumSet GlyphCacheFeatures; CORRADE_ENUMSET_OPERATORS(GlyphCacheFeatures) +/** @debugoperatorenum{GlyphCacheFeatures} */ +MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& output, GlyphCacheFeatures value); + /** @brief Base for glyph caches @m_since{2019,10} -An API-agnostic base for glyph caches. See @ref GlyphCache and -@ref DistanceFieldGlyphCache for concrete implementations. +A GPU-API-agnostic base for glyph caches, supporting multiple fonts and both 2D +and 2D array textures. See the @ref GlyphCache and @ref DistanceFieldGlyphCache +subclasses for concrete OpenGL implementations. The base class provides a +common interface for adding fonts, glyph properties, uploading glyph data and +retrieving glyph properties back. + +@section Text-AbstractGlyphCache-filling Filling the glyph cache + +A glyph cache is constructed through the concrete GPU-API-specific subclasses, +namely the @ref GlyphCache and @ref DistanceFieldGlyphCache classes mentioned +above. Depending on the use case and platform capabilities it's also possible +to create glyph caches with 2D texture arrays, for example when large alphabets +are used or when the cache may get filled on-demand with many additional +glyphs. + +A glyph cache is created in an appropriate @ref PixelFormat and a size. +@ref PixelFormat::R8Unorm is the usual choice, @ref PixelFormat::RGBA8Unorm is +useful for emoji fonts or when arbitrary icon data are put into the cache. + +@snippet MagnumText-gl.cpp AbstractGlyphCache-filling-construct + +The rest of this section describes low level usage of the glyph cache filling +APIs, which are useful mainly when implementing an @ref AbstractFont itself or +when adding arbitrary other image data to the cache. When using the glyph cache +with an existing @ref AbstractFont instance, often the high level use involves +just calling @ref AbstractFont::fillGlyphCache(), which does all of the +following on its own. + +Let's say we want to fill the glyph cache with a custom set of images that +don't necessarily need to be a font per se. Assuming the input images are +stored in a simple array, and the goal is to put them all together into the +cache and reference them later simply by their array indices. + +@snippet MagnumText.cpp AbstractGlyphCache-filling-images + +@subsection Text-AbstractGlyphCache-filling-font Adding a font + +As the cache supports multiple fonts, each glyph added to it needs to be +associated with a particular font. The @ref addFont() function takes an upper +bound on *glyph IDs* used in the font and optionally an identifier to associate +it with a concrete @ref AbstractFont instance to look it up later with +@ref findFont(). It returns a *font ID* that's subsequently used for adding and +querying glyphs. In our case the glyph IDs are simply indices into the array, +so the upper bound is the array size: + +@snippet MagnumText.cpp AbstractGlyphCache-filling-font + +@subsection Text-AbstractGlyphCache-filling-atlas Reserving space in the glyph atlas + +Each glyph cache contains an instance of @ref TextureTools::AtlasLandfill +packer, which is used to layout glyph data into the cache texture as well as +maintain the remaining free space when more glyphs get added. In this case +we'll ask it for best offsets corresponding to the input image sizes, and as we +created the cache as 2D, we can get 2D offsets back. To keep the example +simple, rotations are disabled as well, see the atlas packer class docs for +information about how to deal with them and achieve potentially better packing +efficiency. + +@snippet MagnumText.cpp AbstractGlyphCache-filling-atlas + +In case the layouting fails, triggering the assertion, the cache size was +picked too small or there was already enough glyphs added that the new ones +didn't fit. The solution is then to either increase the cache size, turn the +cache into an array, or create a second cache for the new data. Depending on +the input sizes it's also possible to achieve a better packing efficiency by +toggling various @ref TextureTools::AtlasLandfillFlag values --- you can for +example create temporary instances aside, attempt packing with them, and then +for filling the glyph cache itself pick the set of flags that resulted in the +smallest @ref TextureTools::AtlasLandfill::filledSize(). + +@subsection Text-AbstractGlyphCache-filling-glyphs Adding glyphs + +With the `offsets` array filled, everything is ready for adding images into the +cache with @ref addGlyph() and copying their data to respective locations +in the cache @ref image(). Together with input image sizes, the `offsets` are +used to form @relativeref{Magnum,Range2Di} instances. What is left at zeros in +this case is the third *glyph offset* argument, which describes how the glyph +image is positioned relative to the text layouting cursor (used for example for +letters *j* or *q* that reach below the baseline). + +@snippet MagnumText.cpp AbstractGlyphCache-filling-glyphs + +Important is to call @ref flushImage() at the end, which makes the glyph cache +update its actual GPU-side texture based on what area of the image was updated. +In case of @ref DistanceFieldGlyphCache for example it also triggers distance +field generation for given area. + +@subsection Text-AbstractGlyphCache-filling-incremental Incremental population + +As long as the cache size allows, it's possible to add more fonts with +additional glyphs. It's also possible to call @ref addGlyph() for any font that +was added previously, as long as the added glyph ID is within corresponding +@ref fontGlyphCount() bounds and given glyph ID wasn't added yet. That allows +for example a use case where the glyph cache is initially empty and glyphs are +rasterized to it only as needed by actually rendered text, which is especially +useful with large alphabets or when the set of used fonts is large. + +However, note that packing the atlas with all glyphs from all fonts just once +will always result in a more optimal layout of the glyph data than adding the +glyphs incrementally. + +@subsection Text-AbstractGlyphCache-filling-invalid-glyph Setting a custom invalid glyph + +By default, to denote an invalid glyph, i.e. a glyph that isn't present in the +cache, a zero-area rectangle is used. This can be overriden with +@ref setInvalidGlyph(). Its usage is similar to @ref addGlyph(), in particular +you may want to reserve its space together with other glyphs to achieve best +packing. + +Additionally, glyph ID @cpp 0 @ce in fonts is usually reserved for invalid +font-specific glyphs as well. The cache-global invalid glyph can thus be either +a special one or you can make it alias some other font-specific invalid glyph, +by calling @ref setInvalidGlyph() with the same arguments as @ref addGlyph() +for a font-specific glyph ID @cpp 0 @ce. + +@section Text-AbstractGlyphCache-querying Querying glyph properties and glyph data + +A glyph cache can be queried for ID of a particular font with @ref findFont(), +passing the @ref AbstractFont pointer to it. If no font with such pointer was +added, the function returns @ref Containers::NullOpt. Then, for a particular +font ID a font-specific glyph ID can be queried with @ref glyphId(). If no such +glyph was added yet, the function returns @cpp 0 @ce, i.e. the invalid glyph. +The @ref glyph() function then directly returns data for given glyph, or the +invalid glyph data in case the glyph wasn't found. + +@snippet MagnumText.cpp AbstractGlyphCache-querying + +As text rendering is potentially happening very often, batch +@ref glyphIdsInto(), @ref glyphOffsets(), @ref glyphLayers() and +@ref glyphRectangles() APIs are provided as well to trim down the amount of +function calls and redundant lookups: + +@snippet MagnumText.cpp AbstractGlyphCache-querying-batch + +For invalid glyphs it's the caller choice to either use the invalid glyph +as-is (as done above), leading to blank spaces in the text, or remember the +font-specific glyph IDs that resolved to @cpp 0 @ce with @ref glyphId(), +rasterize them to the cache in the next round, and then update the rendered +text again. @section Text-AbstractGlyphCache-subclassing Subclassing -The subclass needs to implement the @ref doSetImage() function and manage the -glyph cache image. The public @ref setImage() function already does checking -for rectangle bounds so it's not needed to do it again on the implementation -side. +A subclass needs to implement the @ref doFeatures() and @ref doSetImage() +functions. If the subclass does additional processing of the glyph cache image, +it should advertise that with @ref GlyphCacheFeature::ImageProcessing. If it's +desirable to populate the processed image directly and/or download it, it +should provide an appropriate setter and/or advertise +@ref GlyphCacheFeature::ProcessedImageDownload as well and implement +@ref doProcessedImage(). + +The public @ref flushImage() function already does checking for rectangle +bounds so it's not needed to do it again inside @ref doSetImage(). */ class MAGNUM_TEXT_EXPORT AbstractGlyphCache { public: /** - * @brief Constructor - * @param size Glyph cache texture size - * @param padding Padding around every glyph + * @brief Construct a 2D array glyph cache + * @param format Source image format + * @param size Source image size in pixels + * @param padding Padding around every glyph in pixels + * @m_since_latest + * + * The @p size is expected to be non-zero. + * @see @ref AbstractGlyphCache(PixelFormat, const Vector2i&, const Vector2i&) + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + explicit AbstractGlyphCache(PixelFormat format, const Vector3i& size, const Vector2i& padding = {}); + #else + /* To not need to include Vector */ + explicit AbstractGlyphCache(PixelFormat format, const Vector3i& size, const Vector2i& padding); + explicit AbstractGlyphCache(PixelFormat format, const Vector3i& size); + #endif + + /** + * @brief Construct a 2D glyph cache + * @param format Source image format + * @param size Source image size in pixels + * @param padding Padding around every glyph in pixels + * @m_since_latest + * + * Equivalent to calling + * @ref AbstractGlyphCache(PixelFormat, const Vector3i&, const Vector2i&) + * with depth set to @cpp 1 @ce. + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + explicit AbstractGlyphCache(PixelFormat format, const Vector2i& size, const Vector2i& padding = {}); + #else + /* To not need to include Vector */ + explicit AbstractGlyphCache(PixelFormat format, const Vector2i& size, const Vector2i& padding); + explicit AbstractGlyphCache(PixelFormat format, const Vector2i& size); + #endif + + #ifdef MAGNUM_BUILD_DEPRECATED + /** + * @brief Construct a 2D glyph cache + * @param size Source image size in pixels + * @param padding Padding around every glyph in pixels + * + * Calls @ref AbstractGlyphCache(PixelFormat, const Vector2i&, const Vector2i&) + * with @p format set to @ref PixelFormat::R8Unorm. + * @m_deprecated_since_latest Use @ref AbstractGlyphCache(PixelFormat, const Vector2i&, const Vector2i&) + * instead. */ + #ifdef DOXYGEN_GENERATING_OUTPUT explicit AbstractGlyphCache(const Vector2i& size, const Vector2i& padding = {}); + #else + /* To not need to include Vector */ + CORRADE_DEPRECATED("use AbstractGlyphCache(PixelFormat, const Vector2i&, const Vector2i&) instead") explicit AbstractGlyphCache(const Vector2i& size, const Vector2i& padding); + CORRADE_DEPRECATED("use AbstractGlyphCache(PixelFormat, const Vector2i&, const Vector2i&) instead") explicit AbstractGlyphCache(const Vector2i& size); + #endif + #endif + + /** @brief Copying is not allowed */ + AbstractGlyphCache(const AbstractGlyphCache&) = delete; + + /** + * @brief Move constructor + * @m_since_latest + * + * Performs a destructive move, i.e. the original object isn't usable + * afterwards anymore. + */ + AbstractGlyphCache(AbstractGlyphCache&&) noexcept; virtual ~AbstractGlyphCache(); + /** @brief Copying is not allowed */ + AbstractGlyphCache& operator=(const AbstractGlyphCache&) = delete; + + /** + * @brief Move assignment + * @m_since_latest + */ + AbstractGlyphCache& operator=(AbstractGlyphCache&&) noexcept; + /** @brief Features supported by this glyph cache implementation */ GlyphCacheFeatures features() const { return doFeatures(); } - /** @brief Glyph cache texture size */ - Vector2i textureSize() const { return _size; } + /** + * @brief Glyph cache texture format + * @m_since_latest + * + * Corresponds to the format of the image view returned from + * @ref image(). + */ + PixelFormat format() const; + + /** + * @brief Glyph cache texture size + * @m_since_latest + * + * Corresponds to the size of the image view returned from + * @ref image(). + */ + Vector3i size() const; + + #ifdef MAGNUM_BUILD_DEPRECATED + /** + * @brief 2D glyph cache texture size + * + * Can be called only if @ref size() depth is @cpp 1 @ce. + * @m_deprecated_since_latest Use @ref size() instead. + */ + CORRADE_DEPRECATED("use size() instead") Vector2i textureSize() const; + #endif /** @brief Glyph padding */ - Vector2i padding() const { return _padding; } + Vector2i padding() const; - /** @brief Count of glyphs in the cache */ - std::size_t glyphCount() const { return glyphs.size(); } + /** + * @brief Count of fonts in the cache + * @m_since_latest + * + * @see @ref addFont(), @ref glyphCount() + */ + UnsignedInt fontCount() const; /** - * @brief Parameters of given glyph - * @param glyph Glyph ID + * @brief Count of all glyphs added to the cache * - * First tuple element is glyph position relative to point on baseline, - * second element is glyph region in texture atlas. + * The returned count is a sum across all fonts present in the cache. + * It's not possible to query count of added glyphs for a just single + * font, the @ref fontGlyphCount() query returns an upper bound for a + * font-specific glyph ID. + * @see @ref addGlyph(), @ref fontCount() + */ + UnsignedInt glyphCount() const; + + /** + * @brief Atlas packer instance + * @m_since_latest * - * Returned values include padding. + * Meant to be used to reserve space in the atlas texture for + * to-be-added glyphs. After that call @ref addGlyph() to associate the + * reserved space with actual glyph properties, copy the corresponding + * glyph data to appropriate sub-ranges in @ref image() and reflect + * the updates to the GPU-side data with @ref flushImage(). * - * If no glyph is found, glyph @cpp 0 @ce is returned, which is by - * default on zero position and has zero region in texture atlas. You - * can reset it to some meaningful value in @ref insert(). - * @see @ref padding() + * The atlas packer is initially configured to match @ref size() and + * @ref padding() and the + * @ref TextureTools::AtlasLandfillFlag::RotatePortrait and + * @relativeref{TextureTools::AtlasLandfillFlag,RotateLandscape} flags + * are cleared. Everything else is left at defaults. See the class + * documentation for more information. */ - std::pair operator[](UnsignedInt glyph) const { - auto it = glyphs.find(glyph); - return it == glyphs.end() ? glyphs.at(0) : it->second; - } + TextureTools::AtlasLandfill& atlas(); - /** @brief Iterator access to cache data */ - std::unordered_map>::const_iterator begin() const { - return glyphs.begin(); - } + /** + * @overload + * @m_since_latest + */ + const TextureTools::AtlasLandfill& atlas() const; - /** @brief Iterator access to cache data */ - std::unordered_map>::const_iterator end() const { - return glyphs.end(); - } + /** + * @brief Set a cache-global invalid glyph + * @param offset Offset of the rendered glyph relative to a + * point on the baseline + * @param layer Layer in the atlas + * @param rectangle Rectangle in the atlas without padding applied + * @return Cache-global glyph ID + * @m_since_latest + * + * Defines properties of glyph with ID @cpp 0 @ce, i.e. a cache-global + * invalid glyph. By default the glyph is empty. + * + * The @p layer and @p rectangle is expected to be in bounds for + * @ref size(). Usually the @p rectangle would match an offset + size + * reserved earlier in the @ref atlas() packer, but doesn't have to. If + * not, it's the caller responsibility to ensure the atlas packer has + * up-to-date information about used area in the atlas in case + * incremental filling of the cache is desired. + * + * Copy the corresponding glyph data to appropriate sub-ranges in + * @ref image(). After the glyphs are copied, call @ref flushImage() to + * reflect the updates to the GPU-side data. + */ + void setInvalidGlyph(const Vector2i& offset, Int layer, const Range2Di& rectangle); /** - * @brief Layout glyphs with given sizes to the cache + * @brief Set a cache-global invalid glyph in a 2D glyph cache + * @m_since_latest * - * Returns non-overlapping regions in cache texture to store glyphs. - * The reserved space is reused on next call to @ref reserve() if no - * glyph was stored there, use @ref insert() to store actual glyph on - * given position and @ref setImage() to upload glyph image. + * Equivalent to calling @ref setInvalidGlyph(const Vector2i&, Int, const Range2Di&) + * with @p layer set to @cpp 0 @ce. Can be called only if @ref size() + * depth is @cpp 1 @ce. + */ + void setInvalidGlyph(const Vector2i& offset, const Range2Di& rectangle); + + /** + * @brief Add a font + * @param glyphCount Upper bound on glyph IDs present in the font, + * or the value of @ref AbstractFont::glyphCount() + * @param pointer Optional unique font identifier for later + * lookup via @ref findFont(). Use @cpp nullptr @ce if not + * associated with any particular font instance. + * @m_since_latest * - * Glyph @p sizes are expected to be without padding. + * Returns font ID that's subsequently used to identify the font in + * @ref addGlyph() and @ref glyph(). The @p pointer is expected to be + * either @cpp nullptr @ce or unique across all added fonts but apart + * from that isn't accessed in any way. + */ + UnsignedInt addFont(UnsignedInt glyphCount, const AbstractFont* pointer = nullptr); + + /** + * @brief Upper bound on glyph IDs present in given font + * @param fontId Font ID returned by @ref addFont() + * @m_since_latest + * + * The @p fontId is expected to be less than @ref fontCount(). Note + * that this query doesn't return an actual number of glyphs added for + * given font but an upper bound on their IDs. + */ + UnsignedInt fontGlyphCount(UnsignedInt fontId) const; + + /** + * @brief Unique font identifier + * @param fontId Font ID returned by @ref addFont() + * @m_since_latest + * + * The @p fontId is expected to be less than @ref fontCount(). The + * returned pointer isn't guaranteed to point to anything meaningful. + */ + const AbstractFont* fontPointer(UnsignedInt fontId) const; + + /** + * @brief Find a font ID for a unique font identifier + * @m_since_latest * - * @attention Cache size must be large enough to contain all rendered - * glyphs. - * @see @ref padding() + * The @p pointer is expected to not be @cpp nullptr @ce, as there can + * be multiple fonts with no associated identifier. If no font is found + * for given identifier, returns @ref Containers::NullOpt. The lookup + * is done with an @f$ \mathcal{O}(n) @f$ complexity with @f$ n @f$ + * being @ref fontCount(). */ - std::vector reserve(const std::vector& sizes); + Containers::Optional findFont(const AbstractFont* pointer) const; + #ifdef MAGNUM_BUILD_DEPRECATED /** - * @brief Insert a glyph to the cache + * @brief Reserve space for given glyph sizes in the cache + * + * Calls @ref addFont() with glyph count set to size of the @p sizes + * vector and then @ref TextureTools::AtlasLandfill::add() to reserve + * the sizes. For backwards compatibility only, can be called just + * once. + * @m_deprecated_since_latest Use @ref atlas() and + * @ref TextureTools::AtlasLandfill::add() instead. + */ + CORRADE_DEPRECATED("use atlas() and TextureTools::AtlasLandfill::add() instead") std::vector reserve(const std::vector& sizes); + #endif + + /** + * @brief Add a glyph + * @param fontId Font ID returned by @ref addFont() + * @param fontGlyphId Glyph ID in given font + * @param offset Offset of the rendered glyph relative to a + * point on the baseline + * @param layer Layer in the atlas + * @param rectangle Rectangle in the atlas without padding applied + * @return Cache-global glyph ID + * @m_since_latest + * + * The @p fontId is expected to be less than @ref fontCount(), + * @p fontGlyphId then less than the glyph count passed in the + * @ref addFont() call and an ID that haven't been added yet, and + * @p layer and @p rectangle in bounds for @ref size(). Usually the + * @p rectangle would match an offset + size reserved earlier in the + * @ref atlas() packer, but doesn't have to. If not, it's the caller + * responsibility to ensure the atlas packer has up-to-date information + * about used area in the atlas in case incremental filling of the + * cache is desired. + * + * Copy the corresponding glyph data to appropriate sub-ranges in + * @ref image(). After the glyphs are copied, call @ref flushImage() to + * reflect the updates to the GPU-side data. + * + * The returned glyph ID can be passed directly to @ref glyph() to + * retrieve its properties, the same ID can be also queried by passing + * the @p fontId and @p fontGlyphId to @ref glyphId(). Due to how the + * internal glyph ID mapping is implemented, there can be at most 65536 + * glyphs added including the implicit invalid one. + */ + UnsignedInt addGlyph(UnsignedInt fontId, UnsignedInt fontGlyphId, const Vector2i& offset, Int layer, const Range2Di& rectangle); + + /** + * @brief Add a glyph to a 2D glyph cache + * + * Equivalent to calling @ref addGlyph(UnsignedInt, UnsignedInt, const Vector2i&, Int, const Range2Di&) + * with @p layer set to @cpp 0 @ce. Can be called only if @ref size() + * depth is @cpp 1 @ce. + */ + UnsignedInt addGlyph(UnsignedInt fontId, UnsignedInt fontGlyphId, const Vector2i& offset, const Range2Di& rectangle); + + #ifdef MAGNUM_BUILD_DEPRECATED + /** + * @brief Add a glyph * @param glyph Glyph ID - * @param position Position relative to point on baseline + * @param offset Offset relative to point on baseline * @param rectangle Region in texture atlas * - * You can obtain unused non-overlapping regions with @ref reserve(). - * You can't overwrite already inserted glyph, however you can reset - * glyph @cpp 0 @ce to some meaningful value. + * Calls either @ref setInvalidGlyph() or @ref addGlyph() with + * @p fontId set to @cpp 0 @ce. If no font is added yet, adds it, if + * it's added expands its glyph count as necessary. Cannot be called if + * there's more than one font. + * @m_deprecated_since_latest Use @ref setInvalidGlyph(), + * @ref addFont() and @ref addGlyph() instead. + */ + CORRADE_DEPRECATED("use addFont() and addGlyph() instead") void insert(UnsignedInt glyph, const Vector2i& offset, const Range2Di& rectangle); + #endif + + /** + * @brief Glyph cache image + * @m_since_latest * - * Glyph parameters are expected to be without padding. + * The view is of @ref format() and @ref size(), and is initially + * zero-filled. For every @ref addGlyph() copy the corresponding glyph + * data to appropriate sub-ranges of the image. After the glyphs are + * copied, call @ref flushImage() to reflect the updates to the + * GPU-side data. * - * Use @ref setImage() to upload an image corresponding to the glyphs. - * @see @ref padding(), @ref AbstractFont::fillGlyphCache() + * If the glyph cache has @ref GlyphCacheFeature::ImageProcessing set, + * the actual image used for rendering is different. Use + * @ref processedImage() to download it. + * @see @ref ImageView::pixels(), @ref Utility::copy(), + * @ref processedImage() */ - void insert(UnsignedInt glyph, const Vector2i& position, const Range2Di& rectangle); + MutableImageView3D image(); + /** + * @overload + * @m_since_latest + */ + ImageView3D image() const; + + /** + * @brief Flush glyph cache image updates + * @m_since_latest + * + * Call after copying glyph data to @ref image() in order to reflect + * the updates to the GPU-side data. The @p layer and @p range is + * expected to be in bounds for @ref size(). You can use + * @ref Math::join() on rectangles passed to @ref addGlyph() to + * calculate the area that spans all glyphs that were added. + */ + void flushImage(const Range3Di& range); + + /** + * @overload + * @m_since_latest + */ + void flushImage(Int layer, const Range2Di& range); + + /** + * @brief Flush 2D glyph cache image updates + * @m_since_latest + * + * Equivalent to calling @ref flushImage(const Range3Di&) with depth + * offset @cpp 0 @ce and depth size @cpp 1 @ce. Can be called only if + * @ref size() depth is @cpp 1 @ce. + */ + void flushImage(const Range2Di& range); + + #ifdef MAGNUM_BUILD_DEPRECATED /** * @brief Set cache image * * Uploads image for one or more glyphs to given offset in cache - * texture. The @p offset and @ref ImageView::size() are expected to be - * in bounds for @ref textureSize(). - * @see @ref AbstractFont::fillGlyphCache() + * texture and calls @ref flushImage(). The @p offset and + * @ref ImageView::size() are expected to be in bounds for @ref size(). + * Can be called only if @ref size() depth is @cpp 1 @ce. + * @m_deprecated_since_latest Copy the glyph data to slices of + * @ref image() instead and call @ref flushImage() afterwards. */ - void setImage(const Vector2i& offset, const ImageView2D& image); + CORRADE_DEPRECATED("copy data to image() instead") void setImage(const Vector2i& offset, const ImageView2D& image); + #endif /** - * @brief Download cache image + * @brief Download processed cache image + * @m_since_latest * - * Downloads the cache texture back. Calls @ref doImage(). Available - * only if @ref GlyphCacheFeature::ImageDownload is supported. + * If the glyph cache has @ref GlyphCacheFeature::ImageProcessing set, + * the actual image used for rendering is different from @ref image() + * and has potentially a different size or format. Expects that + * @ref GlyphCacheFeature::ProcessedImageDownload is supported. For a + * glyph cache without @ref GlyphCacheFeature::ImageProcessing you can + * get the image directly through @ref image(). * @see @ref features() */ - Image2D image(); + Image3D processedImage(); + + /** + * @brief Query a cache-global glyph ID from a font-local glyph ID + * @param fontId Font ID returned by @ref addFont() + * @param fontGlyphId Glyph ID in given font + * @m_since_latest + * + * The @p fontId is expected to be less than @ref fontCount(), + * @p fontGlyphId then less than the glyph count passed in the + * @ref addFont() call. The returned ID can be then used to index the + * @ref glyphOffsets() const, @ref glyphLayers() const and + * @ref glyphRectangles() const views, alternatively you can use + * @ref glyph(UnsignedInt, UnsignedInt) const to get properties of a + * single glyph directly. + * + * If @ref addGlyph() wasn't called for given + * @p fontId and @p fontGlyphId yet, returns @cpp 0 @ce, i.e. the + * cache-global invalid glyph index. + * + * The lookup is done with an @f$ \mathcal{O}(1) @f$ complexity. + * @see @ref glyphIdsInto() + */ + UnsignedInt glyphId(UnsignedInt fontId, UnsignedInt fontGlyphId) const; + + /** + * @brief Query cache-global glyph IDs from font-local glyph IDs + * @param[in] fontId Font ID returned by @ref addFont() + * @param[in] fontGlyphIds Glyph IDs in given font + * @param[out] glyphIds Resulting cache-global glyph IDs + * + * A batch variant of @ref glyphId(), mainly meant to be used to index + * the @ref glyphOffsets() const, @ref glyphLayers() const and + * @ref glyphRectangles() const views. + * + * The @p fontId is expected to be less than @ref fontCount(), all + * @p fontGlyphIds items then less than the glyph count passed in the + * @ref addFont() call. The @p fontGlyphIds and @p glyphIds views are + * expected to have the same size. Glyphs for which @ref addGlyph() + * wasn't called yet have the corresponding @p glyphIds item set to + * @cpp 0 @ce. + * + * The lookup is done with an @f$ \mathcal{O}(n) @f$ complexity with + * @f$ n @f$ being size of the @p fontGlyphIds array. + */ + void glyphIdsInto(UnsignedInt fontId, const Containers::StridedArrayView1D& fontGlyphIds, const Containers::StridedArrayView1D& glyphIds) const; + + /** + * @overload + * @m_since_latest + */ + void glyphIdsInto(UnsignedInt fontId, std::initializer_list fontGlyphIds, const Containers::StridedArrayView1D& glyphIds) const; + + /** + * @brief Positions of all glyphs in the cache relative to a point on the baseline + * @m_since_latest + * + * The offsets are including @ref padding(). Size of the returned view + * is the same as @ref glyphCount(). Use @ref glyphId() or + * @ref glyphIdsInto() to map from per-font glyph IDs to indices in + * this array. The first item is the position of the cache-global + * invalid glyph. + * + * The returned view is only guaranteed to be valid until the next + * @ref addGlyph() call. + * @see @ref glyphLayers() const, @ref glyphRectangles() const, + * @ref glyph() + */ + Containers::StridedArrayView1D glyphOffsets() const; + + /** + * @brief Layers of all glyphs in the cache atlas + * @m_since_latest + * + * Size of the returned view is the same as @ref glyphCount(). Use + * @ref glyphId() or @ref glyphIdsInto() to map from per-font glyph IDs + * to indices in this array. The first item is the layer of the + * cache-global invalid glyph. All values are guaranteed to be less + * than @ref size() depth. + * + * The returned view is only guaranteed to be valid until the next + * @ref addGlyph() call. + * @see @ref glyphOffsets() const, @ref glyphRectangles() const, + * @ref glyph() + */ + Containers::StridedArrayView1D glyphLayers() const; + + /** + * @brief Rectangles of all glyphs in the cache atlas + * @m_since_latest + * + * The rectangles are including @ref padding(). Size of the returned + * view is the same as @ref glyphCount(). Use @ref glyphId() or + * @ref glyphIdsInto() to map from per-font glyph IDs to indices in + * this array. The first item is the layer of the cache-global invalid + * glyph. All values are guaranteed to fit into @ref size() width and + * height. + * + * The returned view is only guaranteed to be valid until the next + * @ref addGlyph() call. + * @see @ref glyphOffsets() const, @ref glyphLayers() const, + * @ref glyph() + */ + Containers::StridedArrayView1D glyphRectangles() const; + + /** + * @brief Properties of given glyph ID in given font + * @param fontId Font ID returned by @ref addFont() + * @param fontGlyphId Glyph ID in given font + * @m_since_latest + * + * Returns offset of the rendered glyph relative to a point on the + * baseline, layer and rectangle in the atlas. The offset and rectangle + * are including @ref padding(). The @p fontId is expected to be less + * than @ref fontCount(), @p fontGlyphId then less than the glyph count + * passed in the @ref addFont() call. + * + * The lookup is done with an @f$ \mathcal{O}(1) @f$ complexity. + */ + Containers::Triple glyph(UnsignedInt fontId, UnsignedInt fontGlyphId) const; + + /** + * @brief Properties of given cache-global glyph ID + * @param glyphId Cache-global glyph ID + * @m_since_latest + * + * Returns offset of the rendered glyph relative to a point on the + * baseline, layer and rectangle in the atlas. The offset and rectangle + * are including @ref padding(). The @p glyphId is expected to be less + * than @ref glyphCount(). + */ + Containers::Triple glyph(UnsignedInt glyphId) const; + + #ifdef MAGNUM_BUILD_DEPRECATED + /** + * @brief Properties of given glyph + * @param glyphId Glyph ID + * + * Calls @ref glyph() with @p fontId set to @cpp 0 @ce, returns its + * output with the layer index ignored. + * @m_deprecated_since_latest Use @ref glyph() instead. + */ + CORRADE_DEPRECATED("use glyph() instead") std::pair operator[](UnsignedInt glyphId) const; + #endif private: /** @brief Implementation for @ref features() */ virtual GlyphCacheFeatures doFeatures() const = 0; /** - * @brief Implementation for @ref setImage() + * @brief Set a 3D glyph cache image + * @m_since_latest * - * The @p offset and @ref ImageView::size() are guaranteed to be in - * bounds for @ref textureSize(). + * Called from @ref flushImage() with a slice of @ref image(). The + * @p offset and @ref ImageView::size() are guaranteed to be in bounds + * for @ref size(). For a glyph cache with @ref size() depth being + * @cpp 1 @ce default implementation delegates to + * @ref doSetImage(const Vector2i&, const ImageView2D&). Implement + * either this or the other overload. */ - virtual void doSetImage(const Vector2i& offset, const ImageView2D& image) = 0; + virtual void doSetImage(const Vector3i& offset, const ImageView3D& image); - /** @brief Implementation for @ref image() */ - virtual Image2D doImage(); + /** + * @brief Set a 2D glyph cache image + * + * Delegated to from the default implementation of + * @ref doSetImage(const Vector3i&, const ImageView3D&) if @ref size() + * depth is @cpp 1 @ce. The @p offset and @ref ImageView::size() are + * guaranteed to be in bounds for @ref size(). Implement either this or + * the other overload. + */ + virtual void doSetImage(const Vector2i& offset, const ImageView2D& image); + + /** + * @brief Implementation for @ref processedImage() + * @m_since_latest + */ + virtual Image3D doProcessedImage(); - Vector2i _size, _padding; - std::unordered_map> glyphs; + struct State; + Containers::Pointer _state; }; }} diff --git a/src/Magnum/Text/DistanceFieldGlyphCache.cpp b/src/Magnum/Text/DistanceFieldGlyphCache.cpp index 54fcc2ef2..883df1d55 100644 --- a/src/Magnum/Text/DistanceFieldGlyphCache.cpp +++ b/src/Magnum/Text/DistanceFieldGlyphCache.cpp @@ -25,13 +25,16 @@ #include "DistanceFieldGlyphCache.h" +#include "Magnum/Image.h" #include "Magnum/ImageView.h" +#include "Magnum/PixelFormat.h" #include "Magnum/GL/Context.h" #include "Magnum/GL/Extensions.h" #ifndef CORRADE_NO_ASSERT #include "Magnum/GL/PixelFormat.h" #endif #include "Magnum/GL/TextureFormat.h" +#include "Magnum/Math/Range.h" #include "Magnum/TextureTools/DistanceField.h" namespace Magnum { namespace Text { @@ -59,40 +62,48 @@ DistanceFieldGlyphCache::DistanceFieldGlyphCache(const Vector2i& sourceSize, con #endif } +GlyphCacheFeatures DistanceFieldGlyphCache::doFeatures() const { + return GlyphCacheFeature::ImageProcessing + #ifndef MAGNUM_TARGET_GLES + |GlyphCacheFeature::ProcessedImageDownload + #endif + ; +} + void DistanceFieldGlyphCache::doSetImage(const Vector2i& offset, const ImageView2D& image) { GL::Texture2D input; input.setWrapping(GL::SamplerWrapping::ClampToEdge) .setMinificationFilter(GL::SamplerFilter::Linear) .setMagnificationFilter(GL::SamplerFilter::Linear); - #ifndef CORRADE_NO_ASSERT - const GL::PixelFormat format = GL::pixelFormat(image.format()); + /* Upload the input texture and create a distance field from it */ + const Vector2 scale = Vector2{_size}/Vector2{textureSize()}; + + /* On ES2 without EXT_unpack_subimage and on WebGL 1 there's no possibility + to upload just a slice of the input, upload the whole image instead by + ignoring the PixelStorage properties of the input and also process it as + a whole. */ + #ifdef MAGNUM_TARGET_GLES2 + #ifndef MAGNUM_TARGET_WEBGL + if(!GL::Context::current().isExtensionSupported()) #endif - #if !(defined(MAGNUM_TARGET_GLES) && defined(MAGNUM_TARGET_GLES2)) - CORRADE_ASSERT(format == GL::PixelFormat::Red, - "Text::DistanceFieldGlyphCache::setImage(): expected" - << GL::PixelFormat::Red << "but got" << format, ); - input.setImage(0, GL::TextureFormat::R8, image); - #else + { + input.setImage(0, GL::textureFormat(image.format()), ImageView2D{image.format(), size().xy(), image.data()}); + _distanceField(input, texture(), {{}, size().xy()*scale}, size().xy()); + #ifdef MAGNUM_TARGET_WEBGL + static_cast(offset); + #endif + } #ifndef MAGNUM_TARGET_WEBGL - if(GL::Context::current().isExtensionSupported()) { - CORRADE_ASSERT(format == GL::PixelFormat::Red || format == GL::PixelFormat::Luminance, - "Text::DistanceFieldGlyphCache::setImage(): expected" - << GL::PixelFormat::Red << "but got" << format, ); - input.setImage(0, GL::TextureFormat::Red, ImageView2D{image.storage(), GL::PixelFormat::Red, GL::PixelType::UnsignedByte, image.size(), image.data()}); - } else + else #endif + #endif + #if !(defined(MAGNUM_TARGET_GLES2) && defined(MAGNUM_TARGET_WEBGL)) { - CORRADE_ASSERT(format == GL::PixelFormat::Luminance, - "Text::DistanceFieldGlyphCache::setImage(): expected" - << GL::PixelFormat::Luminance << "but got" << format, ); - input.setImage(0, GL::TextureFormat::Luminance, image); + input.setImage(0, GL::textureFormat(image.format()), image); + _distanceField(input, texture(), Range2Di::fromSize(offset*scale, image.size()*scale), image.size()); } #endif - - /* Create distance field from input texture */ - const Vector2 scale = Vector2{_size}/Vector2{textureSize()}; - _distanceField(input, texture(), Range2Di::fromSize(offset*scale, image.size()*scale), image.size()); } void DistanceFieldGlyphCache::setDistanceFieldImage(const Vector2i& offset, const ImageView2D& image) { @@ -122,4 +133,11 @@ void DistanceFieldGlyphCache::setDistanceFieldImage(const Vector2i& offset, cons texture().setSubImage(0, offset, image); } +#ifndef MAGNUM_TARGET_GLES +Image3D DistanceFieldGlyphCache::doProcessedImage() { + Image2D out = _texture.image(0, PixelFormat::R8Unorm); + return Image3D{out.format(), {out.size(), 1}, out.release()}; +} +#endif + }} diff --git a/src/Magnum/Text/DistanceFieldGlyphCache.h b/src/Magnum/Text/DistanceFieldGlyphCache.h index 356412ec2..198c132f1 100644 --- a/src/Magnum/Text/DistanceFieldGlyphCache.h +++ b/src/Magnum/Text/DistanceFieldGlyphCache.h @@ -112,8 +112,13 @@ class MAGNUM_TEXT_EXPORT DistanceFieldGlyphCache: public GlyphCache { void setDistanceFieldImage(const Vector2i& offset, const ImageView2D& image); private: + MAGNUM_TEXT_LOCAL GlyphCacheFeatures doFeatures() const override; MAGNUM_TEXT_LOCAL void doSetImage(const Vector2i& offset, const ImageView2D& image) override; + #ifndef MAGNUM_TARGET_GLES + MAGNUM_TEXT_LOCAL Image3D doProcessedImage() override; + #endif + Vector2i _size; TextureTools::DistanceField _distanceField; }; diff --git a/src/Magnum/Text/GlyphCache.cpp b/src/Magnum/Text/GlyphCache.cpp index 9c94a615b..0e0c03dea 100644 --- a/src/Magnum/Text/GlyphCache.cpp +++ b/src/Magnum/Text/GlyphCache.cpp @@ -25,17 +25,22 @@ #include "GlyphCache.h" -#include "Magnum/Image.h" #include "Magnum/PixelFormat.h" #include "Magnum/GL/Context.h" #include "Magnum/GL/Extensions.h" #include "Magnum/GL/TextureFormat.h" +#ifdef MAGNUM_TARGET_GLES2 +#include "Magnum/ImageView.h" +#endif + namespace Magnum { namespace Text { GlyphCache::GlyphCache(const GL::TextureFormat internalFormat, const Vector2i& size, const Vector2i& padding): GlyphCache{internalFormat, size, size, padding} {} -GlyphCache::GlyphCache(const GL::TextureFormat internalFormat, const Vector2i& originalSize, const Vector2i& size, const Vector2i& padding): AbstractGlyphCache{originalSize, padding} { +/* The unconditional Optional unwrap in here two may assert in rare cases. + Let's hope it doesn't in practice. */ +GlyphCache::GlyphCache(const GL::TextureFormat internalFormat, const Vector2i& originalSize, const Vector2i& size, const Vector2i& padding): AbstractGlyphCache{*GL::genericPixelFormat(internalFormat), originalSize, padding} { /* Initialize the texture */ _texture.setWrapping(GL::SamplerWrapping::ClampToEdge) .setMinificationFilter(GL::SamplerFilter::Linear) @@ -60,21 +65,31 @@ GlyphCache::GlyphCache(const Vector2i& originalSize, const Vector2i& size, const GlyphCache::~GlyphCache() = default; -GlyphCacheFeatures GlyphCache::doFeatures() const { - #ifndef MAGNUM_TARGET_GLES - return GlyphCacheFeature::ImageDownload; - #else - return {}; - #endif -} +GlyphCacheFeatures GlyphCache::doFeatures() const { return {}; } void GlyphCache::doSetImage(const Vector2i& offset, const ImageView2D& image) { - /** @todo some internalformat/format checking also here (if querying internal format is not slow) */ - _texture.setSubImage(0, offset, image); + /* On ES2 without EXT_unpack_subimage and on WebGL 1 there's no possibility + to upload just a slice of the input, upload the whole image instead by + ignoring the PixelStorage properties of the input */ + #ifdef MAGNUM_TARGET_GLES2 + #ifndef MAGNUM_TARGET_WEBGL + if(!GL::Context::current().isExtensionSupported()) + #endif + { + _texture.setSubImage(0, {}, ImageView2D{image.format(), size().xy(), image.data()}); + #ifdef MAGNUM_TARGET_WEBGL + static_cast(offset); + #endif + } + #ifndef MAGNUM_TARGET_WEBGL + else + #endif + #endif + #if !(defined(MAGNUM_TARGET_GLES2) && defined(MAGNUM_TARGET_WEBGL)) + { + _texture.setSubImage(0, offset, image); + } + #endif } -#ifndef MAGNUM_TARGET_GLES -Image2D GlyphCache::doImage() { return _texture.image(0, PixelFormat::R8Unorm); } -#endif - }} diff --git a/src/Magnum/Text/GlyphCache.h b/src/Magnum/Text/GlyphCache.h index 6f22960fb..291762b9b 100644 --- a/src/Magnum/Text/GlyphCache.h +++ b/src/Magnum/Text/GlyphCache.h @@ -114,14 +114,16 @@ class MAGNUM_TEXT_EXPORT GlyphCache: public AbstractGlyphCache { /** @brief Cache texture */ GL::Texture2D& texture() { return _texture; } + #ifdef DOXYGEN_GENERATING_OUTPUT private: - GlyphCacheFeatures MAGNUM_LOCAL doFeatures() const override; - void MAGNUM_LOCAL doSetImage(const Vector2i& offset, const ImageView2D& image) override; - #ifndef MAGNUM_TARGET_GLES - Image2D MAGNUM_LOCAL doImage() override; - #endif - + #else + protected: + #endif GL::Texture2D _texture; + + private: + MAGNUM_TEXT_LOCAL GlyphCacheFeatures doFeatures() const override; + MAGNUM_TEXT_LOCAL void doSetImage(const Vector2i& offset, const ImageView2D& image) override; }; }} diff --git a/src/Magnum/Text/Test/AbstractFontConverterTest.cpp b/src/Magnum/Text/Test/AbstractFontConverterTest.cpp index b693bce3a..a8addb1f2 100644 --- a/src/Magnum/Text/Test/AbstractFontConverterTest.cpp +++ b/src/Magnum/Text/Test/AbstractFontConverterTest.cpp @@ -34,6 +34,7 @@ #include #include +#include "Magnum/Math/Vector2.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractFontConverter.h" #include "Magnum/Text/AbstractGlyphCache.h" diff --git a/src/Magnum/Text/Test/AbstractFontTest.cpp b/src/Magnum/Text/Test/AbstractFontTest.cpp index 851f28b8d..ea4716c1a 100644 --- a/src/Magnum/Text/Test/AbstractFontTest.cpp +++ b/src/Magnum/Text/Test/AbstractFontTest.cpp @@ -36,6 +36,7 @@ #include #include "Magnum/FileCallback.h" +#include "Magnum/Math/Range.h" #include "Magnum/Math/Vector2.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" diff --git a/src/Magnum/Text/Test/AbstractGlyphCacheTest.cpp b/src/Magnum/Text/Test/AbstractGlyphCacheTest.cpp index b82198b8d..c31db91c6 100644 --- a/src/Magnum/Text/Test/AbstractGlyphCacheTest.cpp +++ b/src/Magnum/Text/Test/AbstractGlyphCacheTest.cpp @@ -24,51 +24,242 @@ */ #include -#include -#include /**< @todo drop once std::vector is gone */ +#include +#include #include /**< @todo drop once Debug is stream-free */ +#include #include #include #include +#include #include /**< @todo drop once Debug is stream-free */ #include "Magnum/Image.h" #include "Magnum/ImageView.h" #include "Magnum/PixelFormat.h" +#include "Magnum/DebugTools/CompareImage.h" +#include "Magnum/Math/Range.h" #include "Magnum/Text/AbstractGlyphCache.h" +#include "Magnum/TextureTools/Atlas.h" + +#ifdef MAGNUM_BUILD_DEPRECATED +#include +#include +#endif namespace Magnum { namespace Text { namespace Test { namespace { struct AbstractGlyphCacheTest: TestSuite::Tester { explicit AbstractGlyphCacheTest(); - void initialize(); - void access(); + void debugFeature(); + void debugFeatures(); + void debugFeaturesSupersets(); + + void construct(); + void constructNoPadding(); + void construct2D(); + void construct2DNoPadding(); + #ifdef MAGNUM_BUILD_DEPRECATED + void constructDeprecated(); + void constructDeprecatedNoPadding(); + #endif + void constructImageRowPadding(); + void constructZeroSize(); + + void constructCopy(); + void constructMove(); + + void features(); + + #ifdef MAGNUM_BUILD_DEPRECATED + void textureSizeNot2D(); + #endif + + void setInvalidGlyph(); + void setInvalidGlyph2D(); + void setInvalidGlyphOutOfRange(); + void setInvalidGlyphOutOfRangePadded(); + void setInvalidGlyph2DNot2D(); + + void addFont(); + void addFontDuplicatePointer(); + void fontOutOfRange(); + void findFontNullptr(); + + #ifdef MAGNUM_BUILD_DEPRECATED void reserve(); void reserveIncremental(); void reserveTooSmall(); + void reserveNot2D(); + #endif + + void addGlyph(); + void addGlyph2D(); + void addGlyphIndexOutOfRange(); + void addGlyphAlreadyAdded(); + void addGlyphOutOfRange(); + void addGlyphOutOfRangePadded(); + void addGlyphTooMany(); + void addGlyph2DNot2D(); + + #ifdef MAGNUM_BUILD_DEPRECATED + void insert(); + void insertNot2D(); + void insertMultiFont(); + #endif + + void flushImage(); + void flushImageWholeArea(); + void flushImageLayer(); + void flushImage2D(); + void flushImage2DPassthrough2D(); + void flushImageNotImplemented(); + void flushImagePassthrough2DNotImplemented(); + void flushImageOutOfRange(); + void flushImage2DNot2D(); + #ifdef MAGNUM_BUILD_DEPRECATED void setImage(); void setImageOutOfRange(); + void setImageInvalidFormat(); + void setImageNot2D(); + #endif - void image(); - void imageNotSupported(); - void imageNotImplemented(); + void processedImage(); + void processedImageNotSupported(); + void processedImageNotImplemented(); + + void access(); + void accessBatch(); + void accessInvalid(); + void accessBatchInvalid(); + #ifdef MAGNUM_BUILD_DEPRECATED + void accessDeprecated(); + void accessDeprecatedNot2D(); + #endif +}; + +const struct { + const char* name; + GlyphCacheFeatures features; +} ProcessedImageNotSupportedData[]{ + {"no processing", {}}, + {"no processed image download", GlyphCacheFeature::ImageProcessing}, }; AbstractGlyphCacheTest::AbstractGlyphCacheTest() { - addTests({&AbstractGlyphCacheTest::initialize, - &AbstractGlyphCacheTest::access, + addTests({&AbstractGlyphCacheTest::debugFeature, + &AbstractGlyphCacheTest::debugFeatures, + &AbstractGlyphCacheTest::debugFeaturesSupersets, + + &AbstractGlyphCacheTest::construct, + &AbstractGlyphCacheTest::constructNoPadding, + &AbstractGlyphCacheTest::construct2D, + &AbstractGlyphCacheTest::construct2DNoPadding, + #ifdef MAGNUM_BUILD_DEPRECATED + &AbstractGlyphCacheTest::constructDeprecated, + &AbstractGlyphCacheTest::constructDeprecatedNoPadding, + #endif + &AbstractGlyphCacheTest::constructImageRowPadding, + &AbstractGlyphCacheTest::constructZeroSize, + + &AbstractGlyphCacheTest::constructCopy, + &AbstractGlyphCacheTest::constructMove, + + &AbstractGlyphCacheTest::features, + + #ifdef MAGNUM_BUILD_DEPRECATED + &AbstractGlyphCacheTest::textureSizeNot2D, + #endif + + &AbstractGlyphCacheTest::setInvalidGlyph, + &AbstractGlyphCacheTest::setInvalidGlyph2D, + &AbstractGlyphCacheTest::setInvalidGlyphOutOfRange, + &AbstractGlyphCacheTest::setInvalidGlyphOutOfRangePadded, + &AbstractGlyphCacheTest::setInvalidGlyph2DNot2D, + + &AbstractGlyphCacheTest::addFont, + &AbstractGlyphCacheTest::addFontDuplicatePointer, + &AbstractGlyphCacheTest::fontOutOfRange, + &AbstractGlyphCacheTest::findFontNullptr, + + #ifdef MAGNUM_BUILD_DEPRECATED &AbstractGlyphCacheTest::reserve, &AbstractGlyphCacheTest::reserveIncremental, &AbstractGlyphCacheTest::reserveTooSmall, + &AbstractGlyphCacheTest::reserveNot2D, + #endif + + &AbstractGlyphCacheTest::addGlyph, + &AbstractGlyphCacheTest::addGlyph2D, + &AbstractGlyphCacheTest::addGlyphIndexOutOfRange, + &AbstractGlyphCacheTest::addGlyphAlreadyAdded, + &AbstractGlyphCacheTest::addGlyphOutOfRange, + &AbstractGlyphCacheTest::addGlyphOutOfRangePadded, + &AbstractGlyphCacheTest::addGlyphTooMany, + &AbstractGlyphCacheTest::addGlyph2DNot2D, + + #ifdef MAGNUM_BUILD_DEPRECATED + &AbstractGlyphCacheTest::insert, + &AbstractGlyphCacheTest::insertNot2D, + &AbstractGlyphCacheTest::insertMultiFont, + #endif + &AbstractGlyphCacheTest::flushImage, + &AbstractGlyphCacheTest::flushImageWholeArea, + &AbstractGlyphCacheTest::flushImageLayer, + &AbstractGlyphCacheTest::flushImage2D, + &AbstractGlyphCacheTest::flushImage2DPassthrough2D, + &AbstractGlyphCacheTest::flushImageNotImplemented, + &AbstractGlyphCacheTest::flushImagePassthrough2DNotImplemented, + &AbstractGlyphCacheTest::flushImageOutOfRange, + &AbstractGlyphCacheTest::flushImage2DNot2D, + + #ifdef MAGNUM_BUILD_DEPRECATED &AbstractGlyphCacheTest::setImage, &AbstractGlyphCacheTest::setImageOutOfRange, + &AbstractGlyphCacheTest::setImageInvalidFormat, + &AbstractGlyphCacheTest::setImageNot2D, + #endif + + &AbstractGlyphCacheTest::processedImage}); + + addInstancedTests({&AbstractGlyphCacheTest::processedImageNotSupported}, + Containers::arraySize(ProcessedImageNotSupportedData)); - &AbstractGlyphCacheTest::image, - &AbstractGlyphCacheTest::imageNotSupported, - &AbstractGlyphCacheTest::imageNotImplemented}); + addTests({&AbstractGlyphCacheTest::processedImageNotImplemented, + + &AbstractGlyphCacheTest::access, + &AbstractGlyphCacheTest::accessBatch, + &AbstractGlyphCacheTest::accessInvalid, + &AbstractGlyphCacheTest::accessBatchInvalid, + + #ifdef MAGNUM_BUILD_DEPRECATED + &AbstractGlyphCacheTest::accessDeprecated, + &AbstractGlyphCacheTest::accessDeprecatedNot2D, + #endif + }); +} + +void AbstractGlyphCacheTest::debugFeature() { + std::ostringstream out; + Debug{&out} << GlyphCacheFeature::ImageProcessing << GlyphCacheFeature(0xca); + CORRADE_COMPARE(out.str(), "Text::GlyphCacheFeature::ImageProcessing Text::GlyphCacheFeature(0xca)\n"); +} + +void AbstractGlyphCacheTest::debugFeatures() { + std::ostringstream out; + Debug{&out} << (GlyphCacheFeature::ImageProcessing|GlyphCacheFeature(0xf0)) << GlyphCacheFeatures{}; + CORRADE_COMPARE(out.str(), "Text::GlyphCacheFeature::ImageProcessing|Text::GlyphCacheFeature(0xf0) Text::GlyphCacheFeatures{}\n"); +} + +void AbstractGlyphCacheTest::debugFeaturesSupersets() { + /* ProcessedImageDownload is a superset of ImageProcessing, only one should + be printed */ + std::ostringstream out; + Debug{&out} << (GlyphCacheFeature::ImageProcessing|GlyphCacheFeature::ProcessedImageDownload); + CORRADE_COMPARE(out.str(), "Text::GlyphCacheFeature::ProcessedImageDownload\n"); } struct DummyGlyphCache: AbstractGlyphCache { @@ -78,49 +269,407 @@ struct DummyGlyphCache: AbstractGlyphCache { void doSetImage(const Vector2i&, const ImageView2D&) override {} }; -void AbstractGlyphCacheTest::initialize() { - DummyGlyphCache cache{{1024, 2048}, {23, 46}}; +void AbstractGlyphCacheTest::construct() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}, {2, 5}}; + CORRADE_COMPARE(cache.format(), PixelFormat::R32F); + CORRADE_COMPARE(cache.size(), (Vector3i{1024, 512, 3})); + CORRADE_COMPARE(cache.padding(), (Vector2i{2, 5})); + CORRADE_COMPARE(cache.fontCount(), 0); + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.atlas().size(), (Vector3i{1024, 512, 3})); + CORRADE_COMPARE(cache.atlas().filledSize(), (Vector3i{1024, 512, 0})); + CORRADE_COMPARE(cache.atlas().flags(), TextureTools::AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(cache.atlas().padding(), (Vector2i{2, 5})); + CORRADE_COMPARE(cache.image().format(), PixelFormat::R32F); + CORRADE_COMPARE(cache.image().size(), (Vector3i{1024, 512, 3})); - CORRADE_COMPARE(cache.textureSize(), (Vector2i{1024, 2048})); - CORRADE_COMPARE(cache.padding(), (Vector2i{23, 46})); + /* Invalid glyph is always present */ + CORRADE_COMPARE(cache.glyph(0), Containers::triple(Vector2i{}, 0, Range2Di{})); + CORRADE_COMPARE_AS(cache.glyphOffsets(), Containers::arrayView({ + Vector2i{} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(cache.glyphLayers(), Containers::arrayView({ + 0 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(cache.glyphRectangles(), Containers::arrayView({ + Range2Di{} + }), TestSuite::Compare::Container); + + /* Const overloads */ + const DummyGlyphCache& ccache = cache; + CORRADE_COMPARE(ccache.atlas().size(), (Vector3i{1024, 512, 3})); + CORRADE_COMPARE(ccache.atlas().filledSize(), (Vector3i{1024, 512, 0})); + CORRADE_COMPARE(ccache.atlas().flags(), TextureTools::AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(ccache.atlas().padding(), (Vector2i{2, 5})); + CORRADE_COMPARE(ccache.image().format(), PixelFormat::R32F); + CORRADE_COMPARE(ccache.image().size(), (Vector3i{1024, 512, 3})); } -void AbstractGlyphCacheTest::access() { - DummyGlyphCache cache{{100, 200}, {2, 3}}; - Vector2i position; - Range2Di rectangle; +void AbstractGlyphCacheTest::constructNoPadding() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + CORRADE_COMPARE(cache.format(), PixelFormat::R32F); + CORRADE_COMPARE(cache.size(), (Vector3i{1024, 512, 3})); + CORRADE_COMPARE(cache.padding(), Vector2i{}); + CORRADE_COMPARE(cache.fontCount(), 0); + /* Invalid glyph is always present */ + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.glyph(0), Containers::triple(Vector2i{}, 0, Range2Di{})); + CORRADE_COMPARE(cache.atlas().size(), (Vector3i{1024, 512, 3})); + CORRADE_COMPARE(cache.atlas().filledSize(), (Vector3i{1024, 512, 0})); + CORRADE_COMPARE(cache.atlas().flags(), TextureTools::AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(cache.atlas().padding(), Vector2i{}); + CORRADE_COMPARE(cache.image().format(), PixelFormat::R32F); + CORRADE_COMPARE(cache.image().size(), (Vector3i{1024, 512, 3})); + + /* Invalid glyph is always present, has zero size in this case as well */ + CORRADE_COMPARE(cache.glyph(0), Containers::triple(Vector2i{}, 0, Range2Di{})); + CORRADE_COMPARE_AS(cache.glyphOffsets(), Containers::arrayView({ + Vector2i{} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(cache.glyphLayers(), Containers::arrayView({ + 0 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(cache.glyphRectangles(), Containers::arrayView({ + Range2Di{} + }), TestSuite::Compare::Container); + + /* The rest shouldn't be any different */ +} - /* Default "Not Found" glyph */ +void AbstractGlyphCacheTest::construct2D() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}, {2, 5}}; + CORRADE_COMPARE(cache.format(), PixelFormat::R32F); + CORRADE_COMPARE(cache.size(), (Vector3i{1024, 512, 1})); + CORRADE_COMPARE(cache.padding(), (Vector2i{2, 5})); + CORRADE_COMPARE(cache.fontCount(), 0); + /* Invalid glyph is always present */ CORRADE_COMPARE(cache.glyphCount(), 1); - std::tie(position, rectangle) = cache[0]; - CORRADE_COMPARE(position, (Vector2i{0, 0})); - CORRADE_COMPARE(rectangle, (Range2Di{{0, 0}, {0, 0}})); + CORRADE_COMPARE(cache.atlas().size(), (Vector3i{1024, 512, 1})); + CORRADE_COMPARE(cache.atlas().filledSize(), (Vector3i{1024, 0, 1})); + CORRADE_COMPARE(cache.atlas().flags(), TextureTools::AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(cache.atlas().padding(), (Vector2i{2, 5})); - /* Overwrite the "Not Found" glyph */ - cache.insert(0, {3, 5}, {{10, 10}, {23, 45}}); + /* The rest shouldn't be any different */ +} + +void AbstractGlyphCacheTest::construct2DNoPadding() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}}; + CORRADE_COMPARE(cache.format(), PixelFormat::R32F); + CORRADE_COMPARE(cache.size(), (Vector3i{1024, 512, 1})); + CORRADE_COMPARE(cache.padding(), Vector2i{}); + CORRADE_COMPARE(cache.fontCount(), 0); + /* Invalid glyph is always present */ CORRADE_COMPARE(cache.glyphCount(), 1); - std::tie(position, rectangle) = cache[0]; - /* The position is with negative padding, rectangle with positive */ - CORRADE_COMPARE(position, (Vector2i{1, 2})); - CORRADE_COMPARE(rectangle, (Range2Di{{8, 7}, {25, 48}})); + CORRADE_COMPARE(cache.atlas().size(), (Vector3i{1024, 512, 1})); + CORRADE_COMPARE(cache.atlas().filledSize(), (Vector3i{1024, 0, 1})); + CORRADE_COMPARE(cache.atlas().flags(), TextureTools::AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(cache.atlas().padding(), Vector2i{}); - /* Querying available glyph */ - cache.insert(25, {3, 4}, {{15, 30}, {45, 35}}); - CORRADE_COMPARE(cache.glyphCount(), 2); - std::tie(position, rectangle) = cache[25]; - CORRADE_COMPARE(position, (Vector2i{1, 1})); - CORRADE_COMPARE(rectangle, (Range2Di{{13, 27}, {47, 38}})); + /* The rest shouldn't be any different */ +} + +#ifdef MAGNUM_BUILD_DEPRECATED +void AbstractGlyphCacheTest::constructDeprecated() { + /* Testing just the minimal set of getters as the deprecated constructor + should delegate */ + + CORRADE_IGNORE_DEPRECATED_PUSH + /* Not using the DummyGlyphCache as it'd warn about the deprecated + constructor even with the IGNORE macros */ + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + void doSetImage(const Vector2i&, const ImageView2D&) override {} + } cache{{1024, 512}, {2, 5}}; + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(cache.format(), PixelFormat::R8Unorm); + CORRADE_COMPARE(cache.size(), (Vector3i{1024, 512, 1})); + CORRADE_IGNORE_DEPRECATED_PUSH + CORRADE_COMPARE(cache.textureSize(), (Vector2i{1024, 512})); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(cache.padding(), (Vector2i{2, 5})); + CORRADE_COMPARE(cache.fontCount(), 0); + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.atlas().size(), (Vector3i{1024, 512, 1})); + CORRADE_COMPARE(cache.atlas().filledSize(), (Vector3i{1024, 0, 1})); + CORRADE_COMPARE(cache.atlas().flags(), TextureTools::AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(cache.atlas().padding(), (Vector2i{2, 5})); +} + +void AbstractGlyphCacheTest::constructDeprecatedNoPadding() { + /* Testing just the minimal set of getters as the deprecated constructor + should delegate */ + + CORRADE_IGNORE_DEPRECATED_PUSH + /* Not using the DummyGlyphCache as it'd warn about the deprecated + constructor even with the IGNORE macros */ + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + void doSetImage(const Vector2i&, const ImageView2D&) override {} + } cache{{1024, 512}}; + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(cache.format(), PixelFormat::R8Unorm); + CORRADE_COMPARE(cache.size(), (Vector3i{1024, 512, 1})); + CORRADE_IGNORE_DEPRECATED_PUSH + CORRADE_COMPARE(cache.textureSize(), (Vector2i{1024, 512})); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(cache.padding(), Vector2i{}); + CORRADE_COMPARE(cache.fontCount(), 0); + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.atlas().size(), (Vector3i{1024, 512, 1})); + CORRADE_COMPARE(cache.atlas().filledSize(), (Vector3i{1024, 0, 1})); + CORRADE_COMPARE(cache.atlas().flags(), TextureTools::AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(cache.atlas().padding(), Vector2i{}); +} +#endif + +void AbstractGlyphCacheTest::constructImageRowPadding() { + /* This shouldn't assert due to the data for the image being too small */ + + DummyGlyphCache cache{PixelFormat::RGB8Unorm, {2, 3, 5}}; + CORRADE_COMPARE(cache.size(), (Vector3i{2, 3, 5})); + CORRADE_COMPARE(cache.image().format(), PixelFormat::RGB8Unorm); + CORRADE_COMPARE(cache.image().size(), (Vector3i{2, 3, 5})); + CORRADE_COMPARE(cache.image().data().size(), 8*3*5); /* not 6*3*5 */ +} + +void AbstractGlyphCacheTest::constructZeroSize() { + CORRADE_SKIP_IF_NO_ASSERT(); + + std::ostringstream out; + Error redirectError{&out}; + DummyGlyphCache{PixelFormat::R8Unorm, {2, 0}}; + DummyGlyphCache{PixelFormat::R8Unorm, {0, 2}}; + CORRADE_COMPARE(out.str(), + "Text::AbstractGlyphCache: expected non-zero size, got {2, 0, 1}\n" + "Text::AbstractGlyphCache: expected non-zero size, got {0, 2, 1}\n"); +} + +void AbstractGlyphCacheTest::constructCopy() { + CORRADE_VERIFY(!std::is_copy_constructible{}); + CORRADE_VERIFY(!std::is_copy_assignable{}); +} + +void AbstractGlyphCacheTest::constructMove() { + DummyGlyphCache a{PixelFormat::R16F, {1024, 512, 3}, {2, 5}}; + + DummyGlyphCache b = Utility::move(a); + CORRADE_COMPARE(b.size(), (Vector3i{1024, 512, 3})); + + DummyGlyphCache c{PixelFormat::R8Unorm, {2, 3}}; + c = Utility::move(b); + CORRADE_COMPARE(c.size(), (Vector3i{1024, 512, 3})); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + +void AbstractGlyphCacheTest::features() { + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return GlyphCacheFeature::ImageProcessing; } + void doSetImage(const Vector2i&, const ImageView2D&) override {} + } cache{PixelFormat::R8Unorm, {2, 3}}; + + CORRADE_COMPARE(cache.features(), GlyphCacheFeature::ImageProcessing); +} + +#ifdef MAGNUM_BUILD_DEPRECATED +void AbstractGlyphCacheTest::textureSizeNot2D() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH + cache.textureSize(); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::textureSize(): can't be used on an array glyph cache\n"); +} +#endif + +void AbstractGlyphCacheTest::setInvalidGlyph() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}, {2, 3}}; + + cache.setInvalidGlyph({3, 5}, 2, {{15, 30}, {45, 35}}); + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.glyph(0), Containers::triple( + Vector2i{1, 2}, + 2, + Range2Di{{13, 27}, {47, 38}})); + + /* Invalid glyph spanning the whole area (with padding) shouldn't assert */ + cache.setInvalidGlyph({3, 5}, 2, {{2, 3}, {1022, 509}}); + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.glyph(0), Containers::triple( + Vector2i{1, 2}, + 2, + Range2Di{{}, {1024, 512}})); +} + +void AbstractGlyphCacheTest::setInvalidGlyph2D() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}, {2, 3}}; + + cache.setInvalidGlyph({3, 5}, {{15, 30}, {45, 35}}); + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.glyph(0), Containers::triple( + Vector2i{1, 2}, + 0, + Range2Di{{13, 27}, {47, 38}})); + + /* Invalid glyph spanning the whole area is tested above already */ +} + +void AbstractGlyphCacheTest::setInvalidGlyphOutOfRange() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + cache.setInvalidGlyph({}, -1, {{15, 30}, {45, 35}}); + cache.setInvalidGlyph({}, 3, {{15, 30}, {45, 35}}); + cache.setInvalidGlyph({}, 0, {{15, -1}, {45, 35}}); + cache.setInvalidGlyph({}, 0, {{-1, 30}, {45, 35}}); + cache.setInvalidGlyph({}, 0, {{15, 30}, {1025, 35}}); + cache.setInvalidGlyph({}, 0, {{15, 30}, {45, 513}}); + /* Negative rect size */ + cache.setInvalidGlyph({}, 0, {{45, 30}, {15, 35}}); + cache.setInvalidGlyph({}, 0, {{15, 35}, {45, 30}}); + CORRADE_COMPARE_AS(out.str(), + "Text::AbstractGlyphCache::setInvalidGlyph(): layer -1 and rectangle {{15, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 3 and rectangle {{15, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{15, -1}, {45, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{-1, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{15, 30}, {1025, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{15, 30}, {45, 513}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{45, 30}, {15, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{15, 35}, {45, 30}} out of range for size {1024, 512, 3} and padding {0, 0}\n", + TestSuite::Compare::String); +} + +void AbstractGlyphCacheTest::setInvalidGlyphOutOfRangePadded() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}, {2, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + /* Padding has no effect on layers */ + cache.setInvalidGlyph({}, -1, {{15, 30}, {45, 35}}); + cache.setInvalidGlyph({}, 3, {{15, 30}, {45, 35}}); + /* These four pass if padding is not included in the check */ + cache.setInvalidGlyph({}, 0, {{15, 1}, {45, 35}}); + cache.setInvalidGlyph({}, 0, {{1, 30}, {45, 35}}); + cache.setInvalidGlyph({}, 0, {{15, 30}, {1023, 35}}); + cache.setInvalidGlyph({}, 0, {{15, 30}, {45, 510}}); + /* Negative rect size. The second would pass if it was checked with + padding included. */ + cache.setInvalidGlyph({}, 0, {{45, 30}, {15, 35}}); + cache.setInvalidGlyph({}, 0, {{15, 35}, {45, 30}}); + CORRADE_COMPARE_AS(out.str(), + "Text::AbstractGlyphCache::setInvalidGlyph(): layer -1 and rectangle {{15, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 3 and rectangle {{15, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{15, 1}, {45, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{1, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{15, 30}, {1023, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{15, 30}, {45, 510}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{45, 30}, {15, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::setInvalidGlyph(): layer 0 and rectangle {{15, 35}, {45, 30}} out of range for size {1024, 512, 3} and padding {2, 3}\n", + TestSuite::Compare::String); +} + +void AbstractGlyphCacheTest::setInvalidGlyph2DNot2D() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + cache.setInvalidGlyph({}, {}); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::setInvalidGlyph(): use the layer overload for an array glyph cache\n"); +} + +void AbstractGlyphCacheTest::addFont() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}}; - /* Querying not available glyph falls back to "Not Found" */ - std::tie(position, rectangle) = cache[42]; - CORRADE_COMPARE(position, (Vector2i{1, 2})); - CORRADE_COMPARE(rectangle, (Range2Di{{8, 7}, {25, 48}})); + const AbstractFont* font = reinterpret_cast(0xdeadbeef); + CORRADE_COMPARE(cache.findFont(font), Containers::NullOpt); + + CORRADE_COMPARE(cache.addFont(35, nullptr), 0); + CORRADE_COMPARE(cache.fontCount(), 1); + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.fontGlyphCount(0), 35); + CORRADE_COMPARE(cache.fontPointer(0), nullptr); + CORRADE_COMPARE(cache.findFont(font), Containers::NullOpt); + + CORRADE_COMPARE(cache.addFont(12, font), 1); + CORRADE_COMPARE(cache.fontCount(), 2); + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.fontGlyphCount(1), 12); + CORRADE_COMPARE(cache.fontPointer(1), font); + CORRADE_COMPARE(cache.findFont(font), 1); +} + +void AbstractGlyphCacheTest::addFontDuplicatePointer() { + CORRADE_SKIP_IF_NO_ASSERT(); + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}}; + + cache.addFont(7, nullptr); + + const AbstractFont* font = reinterpret_cast(0xdeadbeef); + cache.addFont(35, font); + + std::ostringstream out; + Error redirectError{&out}; + cache.addFont(12, font); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::addFont(): pointer 0xdeadbeef already used for font 1\n"); +} + +void AbstractGlyphCacheTest::fontOutOfRange() { + CORRADE_SKIP_IF_NO_ASSERT(); + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}}; + + const AbstractFont* font = reinterpret_cast(0xdeadbeef); + cache.addFont(35, nullptr); + cache.addFont(12, font); + CORRADE_COMPARE(cache.fontCount(), 2); + + std::ostringstream out; + Error redirectError{&out}; + cache.fontGlyphCount(2); + cache.fontPointer(2); + CORRADE_COMPARE(out.str(), + "Text::AbstractGlyphCache::fontGlyphCount(): index 2 out of range for 2 fonts\n" + "Text::AbstractGlyphCache::fontPointer(): index 2 out of range for 2 fonts\n"); +} + +void AbstractGlyphCacheTest::findFontNullptr() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}}; + + std::ostringstream out; + Error redirectError{&out}; + cache.findFont(nullptr); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::findFont(): expected a non-null pointer\n"); } +#ifdef MAGNUM_BUILD_DEPRECATED void AbstractGlyphCacheTest::reserve() { - DummyGlyphCache cache{{24, 20}, {1, 2}}; + DummyGlyphCache cache{PixelFormat::R8Unorm, {24, 20}, {1, 2}}; /* Padding should get applied to all */ + CORRADE_IGNORE_DEPRECATED_PUSH std::vector out = cache.reserve({ {5, 3}, /* Landscape glyphs shouldn't get rotated */ @@ -130,6 +679,7 @@ void AbstractGlyphCacheTest::reserve() { {0, 1}, {3, 0}, }); + CORRADE_IGNORE_DEPRECATED_POP CORRADE_COMPARE_AS(Containers::arrayView(out), Containers::arrayView({ Range2Di::fromSize({6, 12}, {5, 3}), Range2Di::fromSize({1, 2}, {12, 6}), @@ -142,112 +692,1008 @@ void AbstractGlyphCacheTest::reserve() { void AbstractGlyphCacheTest::reserveIncremental() { CORRADE_SKIP_IF_NO_ASSERT(); - DummyGlyphCache cache{{24, 20}, {1, 2}}; + DummyGlyphCache cache{PixelFormat::R8Unorm, {24, 20}, {1, 2}}; /* insert() is what triggers the assert, not reserve() alone */ + CORRADE_IGNORE_DEPRECATED_PUSH cache.insert(34, {3, 5}, {{10, 10}, {23, 10}}); + CORRADE_IGNORE_DEPRECATED_POP std::ostringstream out; Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH cache.reserve({{12, 6}}); + CORRADE_IGNORE_DEPRECATED_POP CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::reserve(): reserving space in non-empty cache is not yet implemented\n"); } void AbstractGlyphCacheTest::reserveTooSmall() { - DummyGlyphCache cache{{24, 18}, {1, 2}}; + DummyGlyphCache cache{PixelFormat::R8Unorm, {24, 18}, {1, 2}}; std::ostringstream out; Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH CORRADE_VERIFY(cache.reserve({{5, 3}, {12, 6}, {10, 5}}).empty()); + CORRADE_IGNORE_DEPRECATED_POP CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::reserve(): requested atlas size Vector(24, 18) is too small to fit 3 textures. Generated atlas will be empty.\n"); } +void AbstractGlyphCacheTest::reserveNot2D() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH + cache.reserve({}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::reserve(): can't be used on an array glyph cache\n"); +} +#endif + +void AbstractGlyphCacheTest::addGlyph() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}, {2, 3}}; + + UnsignedInt font9 = cache.addFont(9); + UnsignedInt font3 = cache.addFont(3); + + /* The queried values are with padding applied */ + UnsignedInt font9Glyph6 = cache.addGlyph(font9, 6, {3, 4}, 2, {{15, 30}, {45, 35}}); + CORRADE_COMPARE(font9Glyph6, 1); + CORRADE_COMPARE(cache.glyph(font9Glyph6), Containers::triple( + Vector2i{1, 1}, + 2, + Range2Di{{13, 27}, {47, 38}})); + + /* Glyph in another font */ + UnsignedInt font3Glyph1 = cache.addGlyph(font3, 1, {5, 6}, 1, {{10, 15}, {25, 30}}); + CORRADE_COMPARE(font3Glyph1, 2); + CORRADE_COMPARE(cache.glyph(font3Glyph1), Containers::triple( + Vector2i{3, 3}, + 1, + Range2Di{{8, 12}, {27, 33}})); + + /* Glyph adding order shouldn't matter; glyph spanning the whole area (with + padding) shouldn't assert */ + UnsignedInt font3Glyph0 = cache.addGlyph(font3, 0, {3, 5}, 2, {{2, 3}, {1022, 509}}); + CORRADE_COMPARE(font3Glyph0, 3); + CORRADE_COMPARE(cache.glyph(font3Glyph0), Containers::triple( + Vector2i{1, 2}, + 2, + Range2Di{{}, {1024, 512}})); + + /* Another glyph in an earlier font */ + UnsignedInt font9Glyph3 = cache.addGlyph(font9, 3, {5, 7}, 0, {{5, 10}, {15, 30}}); + CORRADE_COMPARE(font9Glyph3, 4); + CORRADE_COMPARE(cache.glyph(font9Glyph3), Containers::triple( + Vector2i{3, 4}, + 0, + Range2Di{{3, 7}, {17, 33}})); +} + +void AbstractGlyphCacheTest::addGlyph2D() { + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}, {2, 3}}; + + cache.addFont(9); + UnsignedInt fontId = cache.addFont(3); + CORRADE_COMPARE(cache.addGlyph(fontId, 2, {3, 5}, {{15, 30}, {45, 35}}), 1); + CORRADE_COMPARE(cache.glyphCount(), 2); + CORRADE_COMPARE(cache.glyph(1), Containers::triple( + Vector2i{1, 2}, + 0, + Range2Di{{13, 27}, {47, 38}})); +} + +void AbstractGlyphCacheTest::addGlyphIndexOutOfRange() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + cache.addFont(9); + UnsignedInt fontId = cache.addFont(3); + + std::ostringstream out; + Error redirectError{&out}; + cache.addGlyph(cache.fontCount(), 0, {}, 2, {}); + cache.addGlyph(fontId, cache.fontGlyphCount(fontId), {}, 2, {}); + CORRADE_COMPARE(out.str(), + "Text::AbstractGlyphCache::addGlyph(): index 2 out of range for 2 fonts\n" + "Text::AbstractGlyphCache::addGlyph(): index 3 out of range for 3 glyphs in font 1\n"); +} + +void AbstractGlyphCacheTest::addGlyphAlreadyAdded() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + cache.addFont(9); + UnsignedInt fontId = cache.addFont(3); + cache.addGlyph(fontId, 0, {}, 2, {}); + cache.addGlyph(fontId, 1, {}, 2, {}); + cache.addGlyph(fontId, 2, {}, 2, {}); + + std::ostringstream out; + Error redirectError{&out}; + cache.addGlyph(fontId, 2, {}, 2, {}); + CORRADE_COMPARE(out.str(), + "Text::AbstractGlyphCache::addGlyph(): glyph 2 in font 1 already added at index 3\n"); +} + +void AbstractGlyphCacheTest::addGlyphOutOfRange() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + UnsignedInt fontId = cache.addFont(9); + + std::ostringstream out; + Error redirectError{&out}; + cache.addGlyph(fontId, 1, {}, -1, {{15, 30}, {45, 35}}); + cache.addGlyph(fontId, 2, {}, 3, {{15, 30}, {45, 35}}); + cache.addGlyph(fontId, 3, {}, 0, {{15, -1}, {45, 35}}); + cache.addGlyph(fontId, 4, {}, 0, {{-1, 30}, {45, 35}}); + cache.addGlyph(fontId, 5, {}, 0, {{15, 30}, {1025, 35}}); + cache.addGlyph(fontId, 6, {}, 0, {{15, 30}, {45, 513}}); + /* Negative rect size */ + cache.addGlyph(fontId, 8, {}, 0, {{45, 30}, {15, 35}}); + cache.addGlyph(fontId, 7, {}, 0, {{15, 35}, {45, 30}}); + CORRADE_COMPARE_AS(out.str(), + "Text::AbstractGlyphCache::addGlyph(): layer -1 and rectangle {{15, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 3 and rectangle {{15, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{15, -1}, {45, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{-1, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{15, 30}, {1025, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{15, 30}, {45, 513}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{45, 30}, {15, 35}} out of range for size {1024, 512, 3} and padding {0, 0}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{15, 35}, {45, 30}} out of range for size {1024, 512, 3} and padding {0, 0}\n", + TestSuite::Compare::String); +} + +void AbstractGlyphCacheTest::addGlyphOutOfRangePadded() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}, {2, 3}}; + + UnsignedInt fontId = cache.addFont(9); + + std::ostringstream out; + Error redirectError{&out}; + /* Padding has no effect on layers */ + cache.addGlyph(fontId, 1, {}, -1, {{15, 30}, {45, 35}}); + cache.addGlyph(fontId, 2, {}, 3, {{15, 30}, {45, 35}}); + /* These four pass if padding is not included in the check */ + cache.addGlyph(fontId, 3, {}, 0, {{15, 1}, {45, 35}}); + cache.addGlyph(fontId, 4, {}, 0, {{1, 30}, {45, 35}}); + cache.addGlyph(fontId, 5, {}, 0, {{15, 30}, {1023, 35}}); + cache.addGlyph(fontId, 6, {}, 0, {{15, 30}, {45, 510}}); + /* Negative rect size. The second would pass if it was checked with + padding included. */ + cache.addGlyph(fontId, 8, {}, 0, {{45, 30}, {15, 35}}); + cache.addGlyph(fontId, 7, {}, 0, {{15, 35}, {45, 30}}); + CORRADE_COMPARE_AS(out.str(), + "Text::AbstractGlyphCache::addGlyph(): layer -1 and rectangle {{15, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 3 and rectangle {{15, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{15, 1}, {45, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{1, 30}, {45, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{15, 30}, {1023, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{15, 30}, {45, 510}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{45, 30}, {15, 35}} out of range for size {1024, 512, 3} and padding {2, 3}\n" + "Text::AbstractGlyphCache::addGlyph(): layer 0 and rectangle {{15, 35}, {45, 30}} out of range for size {1024, 512, 3} and padding {2, 3}\n", + TestSuite::Compare::String); +} + +void AbstractGlyphCacheTest::addGlyphTooMany() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}}; + + /* Adding a font with over 65k potential glyphs is okay */ + UnsignedInt fontId = cache.addFont(100000); + + for(UnsignedInt i = 0; i != 65535; ++i) + cache.addGlyph(fontId, i, {}, {}); + + CORRADE_COMPARE(cache.glyphCount(), 65536); + + /* But adding 65k actual glyphs isn't */ + std::ostringstream out; + Error redirectError{&out}; + cache.addGlyph(fontId, 65536, {}, {}); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::addGlyph(): only at most 65536 glyphs can be added\n"); +} + +void AbstractGlyphCacheTest::addGlyph2DNot2D() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + cache.addGlyph(0, 0, {}, {}); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::addGlyph(): use the layer overload for an array glyph cache\n"); +} + +#ifdef MAGNUM_BUILD_DEPRECATED +void AbstractGlyphCacheTest::insert() { + DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 200}, {2, 3}}; + + /* Overwriting the "Not Found" glyph. Shouldn't result in any font or glyph + being added. */ + CORRADE_IGNORE_DEPRECATED_PUSH + cache.insert(0, {3, 5}, {{10, 10}, {23, 45}}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(cache.glyphCount(), 1); + CORRADE_COMPARE(cache.fontCount(), 0); + CORRADE_COMPARE(cache.glyph(0), Containers::triple( + Vector2i{1, 2}, + 0, + Range2Di{{8, 7}, {25, 48}})); + + /* Adding a new glyph adds the first font if not there yet, setting its + glyph count to fit the glyph ID */ + CORRADE_IGNORE_DEPRECATED_PUSH + cache.insert(25, {3, 4}, {{15, 30}, {45, 35}}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(cache.glyphCount(), 2); + CORRADE_COMPARE(cache.fontCount(), 1); + CORRADE_COMPARE(cache.fontGlyphCount(0), 26); + CORRADE_COMPARE(cache.glyph(0, 25), Containers::triple( + Vector2i{1, 1}, + 0, + Range2Di{{13, 27}, {47, 38}})); + + /* Adding another glyph with a lower ID doesn't change the font in any + way */ + CORRADE_IGNORE_DEPRECATED_PUSH + cache.insert(5, {5, 6}, {{10, 15}, {25, 30}}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(cache.glyphCount(), 3); + CORRADE_COMPARE(cache.fontCount(), 1); + CORRADE_COMPARE(cache.fontGlyphCount(0), 26); + CORRADE_COMPARE(cache.glyph(0, 5), Containers::triple( + Vector2i{3, 3}, + 0, + Range2Di{{8, 12}, {27, 33}})); + + /* Adding a glyph with greater ID expands the font glyph count again */ + CORRADE_IGNORE_DEPRECATED_PUSH + cache.insert(35, {5, 7}, {{5, 10}, {15, 30}}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(cache.glyphCount(), 4); + CORRADE_COMPARE(cache.fontCount(), 1); + CORRADE_COMPARE(cache.fontGlyphCount(0), 36); + CORRADE_COMPARE(cache.glyph(0, 35), Containers::triple( + Vector2i{3, 4}, + 0, + Range2Di{{3, 7}, {17, 33}})); +} + +void AbstractGlyphCacheTest::insertNot2D() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH + cache.insert(0, {}, {}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::insert(): can't be used on an array glyph cache\n"); +} + +void AbstractGlyphCacheTest::insertMultiFont() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}}; + + cache.addFont(15); + cache.addFont(35); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH + cache.insert(0, {}, {}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::insert(): can't be used on a multi-font glyph cache\n"); +} +#endif + +void AbstractGlyphCacheTest::flushImage() { + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + void doSetImage(const Vector3i& offset, const ImageView3D& image) override { + called = true; + + char pixels0[]{ + 'a', 'b', 'c', 0, + 'd', 'e', 'f', 0, + }; + char pixels1[]{ + '0', '1', '2', 0, + '3', '4', '5', 0, + }; + CORRADE_COMPARE(offset, (Vector3i{15, 30, 3})); + CORRADE_COMPARE(image.size(), (Vector3i{3, 2, 2})); + CORRADE_COMPARE_AS(image.pixels()[0], + (ImageView2D{PixelFormat::R8Snorm, {3, 2}, pixels0}), + DebugTools::CompareImage); + CORRADE_COMPARE_AS(image.pixels()[1], + (ImageView2D{PixelFormat::R8Snorm, {3, 2}, pixels1}), + DebugTools::CompareImage); + } + + bool called = false; + /* Padding should have no effect on any of this */ + } cache{PixelFormat::R8Snorm, {45, 35, 5}, {2, 3}}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + /* Copy two slices of the image */ + char pixels[]{ + 'a', 'b', 'c', + 'd', 'e', 'f', + '0', '1', '2', + '3', '4', '5', + }; + Utility::copy( + Containers::StridedArrayView3D{pixels, {2, 2, 3}}, + cache.image().pixels().sliceSize({3, 30, 15}, {2, 2, 3})); + + cache.flushImage(Range3Di::fromSize({15, 30, 3}, {3, 2, 2})); + CORRADE_VERIFY(cache.called); +} + +void AbstractGlyphCacheTest::flushImageWholeArea() { + /* Like above, but calling flushImage() with the whole size to test bounds + checking */ + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + void doSetImage(const Vector3i& offset, const ImageView3D& image) override { + called = true; + + char pixels0[]{ + 'a', 'b', 'c', 0, + 'd', 'e', 'f', 0, + }; + char pixels1[]{ + '0', '1', '2', 0, + '3', '4', '5', 0, + }; + CORRADE_COMPARE(offset, Vector3i{}); + CORRADE_COMPARE(image.size(), (Vector3i{3, 2, 2})); + CORRADE_COMPARE_AS(image.pixels()[0], + (ImageView2D{PixelFormat::R8Snorm, {3, 2}, pixels0}), + DebugTools::CompareImage); + CORRADE_COMPARE_AS(image.pixels()[1], + (ImageView2D{PixelFormat::R8Snorm, {3, 2}, pixels1}), + DebugTools::CompareImage); + } + + bool called = false; + /* Padding should have no effect on any of this */ + } cache{PixelFormat::R8Snorm, {3, 2, 2}, {2, 3}}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + /* Copy two slices of the image */ + char pixels[]{ + 'a', 'b', 'c', + 'd', 'e', 'f', + '0', '1', '2', + '3', '4', '5', + }; + Utility::copy( + Containers::StridedArrayView3D{pixels, {2, 2, 3}}, + cache.image().pixels()); + + cache.flushImage({{}, {3, 2, 2}}); + CORRADE_VERIFY(cache.called); +} + +void AbstractGlyphCacheTest::flushImageLayer() { + /* Single slice subset of flushImage() */ + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + void doSetImage(const Vector3i& offset, const ImageView3D& image) override { + called = true; + + char pixels[]{ + 'a', 'b', 'c', 0, + 'd', 'e', 'f', 0, + }; + CORRADE_COMPARE(offset, (Vector3i{15, 30, 3})); + CORRADE_COMPARE(image.size(), (Vector3i{3, 2, 1})); + CORRADE_COMPARE_AS(image.pixels()[0], + (ImageView2D{PixelFormat::R8Snorm, {3, 2}, pixels}), + DebugTools::CompareImage); + } + + bool called = false; + /* Padding should have no effect on any of this */ + } cache{PixelFormat::R8Snorm, {45, 35, 5}, {2, 3}}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + char pixels[]{ + 'a', 'b', 'c', + 'd', 'e', 'f', + }; + Utility::copy( + Containers::StridedArrayView3D{pixels, {1, 2, 3}}, + cache.image().pixels().sliceSize({3, 30, 15}, {1, 2, 3})); + + cache.flushImage(3, Range2Di::fromSize({15, 30}, {3, 2})); + CORRADE_VERIFY(cache.called); +} + +void AbstractGlyphCacheTest::flushImage2D() { + /* Like flushImageLayer() but reduced to two dimensions */ + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + void doSetImage(const Vector3i& offset, const ImageView3D& image) override { + called = true; + + char pixels[]{ + 'a', 'b', 'c', 0, + 'd', 'e', 'f', 0, + }; + CORRADE_COMPARE(offset, (Vector3i{15, 30, 0})); + CORRADE_COMPARE(image.size(), (Vector3i{3, 2, 1})); + CORRADE_COMPARE_AS(image.pixels()[0], + (ImageView2D{PixelFormat::R8Snorm, {3, 2}, pixels}), + DebugTools::CompareImage); + } + + bool called = false; + /* Padding should have no effect on any of this */ + } cache{PixelFormat::R8Snorm, {45, 35}, {2, 3}}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + char pixels[]{ + 'a', 'b', 'c', + 'd', 'e', 'f', + }; + Utility::copy( + Containers::StridedArrayView2D{pixels, {2, 3}}, + cache.image().pixels()[0].sliceSize({30, 15}, {2, 3})); + + cache.flushImage(Range2Di::fromSize({15, 30}, {3, 2})); + CORRADE_VERIFY(cache.called); +} + +void AbstractGlyphCacheTest::flushImage2DPassthrough2D() { + /* Like flushImage2D() but with 2D doSetImage() */ + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + void doSetImage(const Vector2i& offset, const ImageView2D& image) override { + called = true; + + char pixels[]{ + 'a', 'b', 'c', 0, + 'd', 'e', 'f', 0, + }; + CORRADE_COMPARE(offset, (Vector2i{15, 30})); + CORRADE_COMPARE(image.size(), (Vector2i{3, 2})); + CORRADE_COMPARE_AS(image.pixels(), + (ImageView2D{PixelFormat::R8Snorm, {3, 2}, pixels}), + DebugTools::CompareImage); + } + + bool called = false; + /* Padding should have no effect on any of this */ + } cache{PixelFormat::R8Snorm, {45, 35}, {2, 3}}; + + /* Capture correct function name */ + CORRADE_VERIFY(true); + + char pixels[]{ + 'a', 'b', 'c', + 'd', 'e', 'f', + }; + Utility::copy( + Containers::StridedArrayView2D{pixels, {2, 3}}, + cache.image().pixels()[0].sliceSize({30, 15}, {2, 3})); + + cache.flushImage(Range2Di::fromSize({15, 30}, {3, 2})); + CORRADE_VERIFY(cache.called); +} + +void AbstractGlyphCacheTest::flushImageNotImplemented() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + + /* The 2D variant shouldn't be called on an array cache */ + void doSetImage(const Vector2i&, const ImageView2D&) override { + CORRADE_FAIL("This should not be called"); + } + } cache{PixelFormat::R32F, {1024, 512, 8}}; + + std::ostringstream out; + Error redirectError{&out}; + cache.flushImage(0, {}); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::image(): not implemented by derived class\n"); +} + +void AbstractGlyphCacheTest::flushImagePassthrough2DNotImplemented() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct: AbstractGlyphCache { + using AbstractGlyphCache::AbstractGlyphCache; + + GlyphCacheFeatures doFeatures() const override { return {}; } + + /* Should try the 3D variant, and from that one call into the 2D where + it'd assert */ + } cache{PixelFormat::R32F, {1024, 512}}; + + std::ostringstream out; + Error redirectError{&out}; + cache.flushImage(0, {}); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::image(): not implemented by derived class\n"); +} + +void AbstractGlyphCacheTest::flushImageOutOfRange() { + CORRADE_SKIP_IF_NO_ASSERT(); + + /* The padding should have no effect on this */ + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 8}, {2, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + /* Negative min X, Y, layer */ + cache.flushImage({{-1, 30, 4}, {45, 35, 6}}); + cache.flushImage({{15, -1, 4}, {45, 35, 6}}); + cache.flushImage({{15, 30, -1}, {45, 35, 6}}); + cache.flushImage(-1, {{15, 30}, {45, 35}}); + /* Too large max X, Y, layer */ + cache.flushImage({{15, 30, 4}, {1025, 35, 6}}); + cache.flushImage({{15, 30, 4}, {45, 513, 6}}); + cache.flushImage({{15, 30, 4}, {45, 35, 9}}); + cache.flushImage(8, {{15, 30}, {45, 35}}); + /* Negative range size on X, Y, layer */ + cache.flushImage({{45, 30, 4}, {15, 35, 6}}); + cache.flushImage({{15, 35, 4}, {45, 30, 6}}); + cache.flushImage({{15, 30, 6}, {45, 35, 4}}); + CORRADE_COMPARE_AS(out.str(), + "Text::AbstractGlyphCache::flushImage(): {{-1, 30, 4}, {45, 35, 6}} out of range for size {1024, 512, 8}\n" + "Text::AbstractGlyphCache::flushImage(): {{15, -1, 4}, {45, 35, 6}} out of range for size {1024, 512, 8}\n" + "Text::AbstractGlyphCache::flushImage(): {{15, 30, -1}, {45, 35, 6}} out of range for size {1024, 512, 8}\n" + "Text::AbstractGlyphCache::flushImage(): {{15, 30, -1}, {45, 35, 0}} out of range for size {1024, 512, 8}\n" + + "Text::AbstractGlyphCache::flushImage(): {{15, 30, 4}, {1025, 35, 6}} out of range for size {1024, 512, 8}\n" + "Text::AbstractGlyphCache::flushImage(): {{15, 30, 4}, {45, 513, 6}} out of range for size {1024, 512, 8}\n" + "Text::AbstractGlyphCache::flushImage(): {{15, 30, 4}, {45, 35, 9}} out of range for size {1024, 512, 8}\n" + "Text::AbstractGlyphCache::flushImage(): {{15, 30, 8}, {45, 35, 9}} out of range for size {1024, 512, 8}\n" + + "Text::AbstractGlyphCache::flushImage(): {{45, 30, 4}, {15, 35, 6}} out of range for size {1024, 512, 8}\n" + "Text::AbstractGlyphCache::flushImage(): {{15, 35, 4}, {45, 30, 6}} out of range for size {1024, 512, 8}\n" + "Text::AbstractGlyphCache::flushImage(): {{15, 30, 6}, {45, 35, 4}} out of range for size {1024, 512, 8}\n", + TestSuite::Compare::String); +} + +void AbstractGlyphCacheTest::flushImage2DNot2D() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + cache.flushImage(Range2Di{}); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::flushImage(): use the 3D or layer overload for an array glyph cache\n"); +} + +#ifdef MAGNUM_BUILD_DEPRECATED void AbstractGlyphCacheTest::setImage() { + CORRADE_IGNORE_DEPRECATED_PUSH struct: AbstractGlyphCache { using AbstractGlyphCache::AbstractGlyphCache; GlyphCacheFeatures doFeatures() const override { return {}; } void doSetImage(const Vector2i& offset, const ImageView2D& image) override { - imageOffset = offset; - imageSize = image.size(); + called = true; + + char pixels[]{ + 'a', 'b', 'c', 0, + 'd', 'e', 'f', 0, + }; + CORRADE_COMPARE(offset, (Vector2i{15, 30})); + CORRADE_COMPARE_AS(image, + (ImageView2D{PixelFormat::R8Unorm, {3, 2}, pixels}), + DebugTools::CompareImage); } - Vector2i imageOffset, imageSize; - } cache{{100, 200}}; + bool called = false; + /* Deliberately using the deprecated PixelFormat-less constructor to verify + that passing a R8Unorm image "just works" */ + } cache{{45, 35}}; + CORRADE_IGNORE_DEPRECATED_POP - cache.setImage({80, 175}, ImageView2D{PixelFormat::R8Unorm, {20, 25}}); + /* Capture correct function name */ + CORRADE_VERIFY(true); - CORRADE_COMPARE(cache.imageOffset, (Vector2i{80, 175})); - CORRADE_COMPARE(cache.imageSize, (Vector2i{20, 25})); + char pixels[]{ + 0, 0, 0, 0, 0, + 0, 'a', 'b', 'c', 0, + 0, 'd', 'e', 'f', 0, + 0, 0, 0, 0, 0 + }; + + /* Testing with a custom PixelStorage to verify the right area gets copied + to the internal image */ + CORRADE_IGNORE_DEPRECATED_PUSH + cache.setImage({15, 30}, ImageView2D{ + PixelStorage{} + .setAlignment(1) + .setRowLength(5) + .setSkip({1, 1, 0}), + PixelFormat::R8Unorm, + {3, 2}, + pixels}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_VERIFY(cache.called); } void AbstractGlyphCacheTest::setImageOutOfRange() { CORRADE_SKIP_IF_NO_ASSERT(); - DummyGlyphCache cache{{100, 200}}; + DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 200}}; /* This is fine */ + CORRADE_IGNORE_DEPRECATED_PUSH cache.setImage({80, 175}, ImageView2D{PixelFormat::R8Unorm, {20, 25}}); + CORRADE_IGNORE_DEPRECATED_POP std::ostringstream out; Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH cache.setImage({81, 175}, ImageView2D{PixelFormat::R8Unorm, {20, 25}}); cache.setImage({80, 176}, ImageView2D{PixelFormat::R8Unorm, {20, 25}}); cache.setImage({-1, 175}, ImageView2D{PixelFormat::R8Unorm, {20, 25}}); cache.setImage({80, -1}, ImageView2D{PixelFormat::R8Unorm, {20, 25}}); + CORRADE_IGNORE_DEPRECATED_POP CORRADE_COMPARE_AS(out.str(), - "Text::AbstractGlyphCache::setImage(): Range({81, 175}, {101, 200}) out of range for texture size Vector(100, 200)\n" - "Text::AbstractGlyphCache::setImage(): Range({80, 176}, {100, 201}) out of range for texture size Vector(100, 200)\n" - "Text::AbstractGlyphCache::setImage(): Range({-1, 175}, {19, 200}) out of range for texture size Vector(100, 200)\n" - "Text::AbstractGlyphCache::setImage(): Range({80, -1}, {100, 24}) out of range for texture size Vector(100, 200)\n", + "Text::AbstractGlyphCache::setImage(): Range({81, 175}, {101, 200}) out of range for glyph cache of size Vector(100, 200)\n" + "Text::AbstractGlyphCache::setImage(): Range({80, 176}, {100, 201}) out of range for glyph cache of size Vector(100, 200)\n" + "Text::AbstractGlyphCache::setImage(): Range({-1, 175}, {19, 200}) out of range for glyph cache of size Vector(100, 200)\n" + "Text::AbstractGlyphCache::setImage(): Range({80, -1}, {100, 24}) out of range for glyph cache of size Vector(100, 200)\n", TestSuite::Compare::String); } -void AbstractGlyphCacheTest::image() { +void AbstractGlyphCacheTest::setImageInvalidFormat() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512}}; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH + cache.setImage({15, 30}, ImageView2D{PixelFormat::R8Unorm, {45, 35}}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::setImage(): expected PixelFormat::R32F but got PixelFormat::R8Unorm\n"); +} + +void AbstractGlyphCacheTest::setImageNot2D() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_IGNORE_DEPRECATED_PUSH + cache.setImage({}, ImageView2D{PixelFormat::R32F, {}, nullptr}); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::setImage(): can't be used on an array glyph cache\n"); +} +#endif + +void AbstractGlyphCacheTest::processedImage() { struct: AbstractGlyphCache { using AbstractGlyphCache::AbstractGlyphCache; - GlyphCacheFeatures doFeatures() const override { return GlyphCacheFeature::ImageDownload; } + GlyphCacheFeatures doFeatures() const override { + return GlyphCacheFeature::ProcessedImageDownload; + } void doSetImage(const Vector2i&, const ImageView2D&) override {} - Image2D doImage() override { return Image2D{PixelFormat::RG8Unorm}; } - } cache{{200, 300}}; + Image3D doProcessedImage() override { return Image3D{PixelFormat::RG8Unorm, {2, 3, 1}, Containers::Array{NoInit, 6*2}}; } + /* Using a different format or size for the source image shouldn't cause + any problem */ + } cache{PixelFormat::RG8Srgb, {200, 300}}; - Image2D image = cache.image(); + Image3D image = cache.processedImage(); CORRADE_COMPARE(image.format(), PixelFormat::RG8Unorm); + CORRADE_COMPARE(image.size(), (Vector3i{2, 3, 1})); } -void AbstractGlyphCacheTest::imageNotSupported() { +void AbstractGlyphCacheTest::processedImageNotSupported() { + auto&& data = ProcessedImageNotSupportedData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + CORRADE_SKIP_IF_NO_ASSERT(); - struct: AbstractGlyphCache { - using AbstractGlyphCache::AbstractGlyphCache; + struct Cache: AbstractGlyphCache { + explicit Cache(const Vector2i& size, GlyphCacheFeatures features): AbstractGlyphCache{PixelFormat::R8Unorm, size}, _features{features} {} - GlyphCacheFeatures doFeatures() const override { return {}; } + GlyphCacheFeatures doFeatures() const override { return _features; } void doSetImage(const Vector2i&, const ImageView2D&) override {} - } cache{{200, 300}}; + + GlyphCacheFeatures _features; + } cache{{200, 300}, data.features}; std::ostringstream out; Error redirectError{&out}; - cache.image(); - CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::image(): feature not supported\n"); + cache.processedImage(); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::processedImage(): feature not supported\n"); } -void AbstractGlyphCacheTest::imageNotImplemented() { +void AbstractGlyphCacheTest::processedImageNotImplemented() { CORRADE_SKIP_IF_NO_ASSERT(); struct: AbstractGlyphCache { using AbstractGlyphCache::AbstractGlyphCache; - GlyphCacheFeatures doFeatures() const override { return GlyphCacheFeature::ImageDownload; } + GlyphCacheFeatures doFeatures() const override { + return GlyphCacheFeature::ProcessedImageDownload; + } void doSetImage(const Vector2i&, const ImageView2D&) override {} - } cache{{200, 300}}; + } cache{PixelFormat::R8Unorm, {200, 300}}; + + std::ostringstream out; + Error redirectError{&out}; + cache.processedImage(); + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::processedImage(): feature advertised but not implemented\n"); +} + +void AbstractGlyphCacheTest::access() { + /* Padding tested well enough in addGlyph(), not using it here */ + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + cache.setInvalidGlyph({5, 7}, 2, {{5, 10}, {15, 30}}); + + UnsignedInt font9 = cache.addFont(9); + UnsignedInt font3 = cache.addFont(3); + UnsignedInt font9Glyph6 = cache.addGlyph(font9, 6, {3, 4}, 0, {{15, 30}, {45, 35}}); + UnsignedInt font3Glyph1 = cache.addGlyph(font3, 1, {5, 6}, 1, {{10, 15}, {25, 30}}); + UnsignedInt font9Glyph3 = cache.addGlyph(font9, 3, {6, 9}, 2, {{10, 5}, {25, 10}}); + CORRADE_COMPARE(font9Glyph6, 1); + CORRADE_COMPARE(font3Glyph1, 2); + CORRADE_COMPARE(font9Glyph3, 3); + + /* Mapping to the global glyph ID */ + CORRADE_COMPARE(cache.glyphId(font9, 6), font9Glyph6); + CORRADE_COMPARE(cache.glyphId(font3, 1), font3Glyph1); + CORRADE_COMPARE(cache.glyphId(font9, 3), font9Glyph3); + + /* Both overloads should return the same */ + CORRADE_COMPARE(cache.glyph(font9, 6), Containers::triple( + Vector2i{3, 4}, + 0, + Range2Di{{15, 30}, {45, 35}})); + CORRADE_COMPARE(cache.glyph(font9Glyph6), Containers::triple( + Vector2i{3, 4}, + 0, + Range2Di{{15, 30}, {45, 35}})); + + CORRADE_COMPARE(cache.glyph(font3, 1), Containers::triple( + Vector2i{5, 6}, + 1, + Range2Di{{10, 15}, {25, 30}})); + CORRADE_COMPARE(cache.glyph(font3Glyph1), Containers::triple( + Vector2i{5, 6}, + 1, + Range2Di{{10, 15}, {25, 30}})); + + CORRADE_COMPARE(cache.glyph(font9, 3), Containers::triple( + Vector2i{6, 9}, + 2, + Range2Di{{10, 5}, {25, 10}})); + CORRADE_COMPARE(cache.glyph(font9Glyph3), Containers::triple( + Vector2i{6, 9}, + 2, + Range2Di{{10, 5}, {25, 10}})); + + /* Mapping to the invalid glyph ID if it hasn't been added yet */ + CORRADE_COMPARE(cache.glyphId(font9, 5), 0); + CORRADE_COMPARE(cache.glyphId(font3, 2), 0); + + /* Querying glyphs that haven't been added yet gives back the invalid + glyph properties */ + CORRADE_COMPARE(cache.glyph(font9, 5), Containers::triple( + Vector2i{5, 7}, + 2, + Range2Di{{5, 10}, {15, 30}})); + CORRADE_COMPARE(cache.glyph(font3, 2), Containers::triple( + Vector2i{5, 7}, + 2, + Range2Di{{5, 10}, {15, 30}})); + CORRADE_COMPARE(cache.glyph(0), Containers::triple( + Vector2i{5, 7}, + 2, + Range2Di{{5, 10}, {15, 30}})); +} + +void AbstractGlyphCacheTest::accessBatch() { + /* Padding tested well enough in addGlyph(), not using it here */ + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + cache.setInvalidGlyph({5, 7}, 2, {{5, 10}, {15, 30}}); + + UnsignedInt font9 = cache.addFont(9); + UnsignedInt font3 = cache.addFont(3); + UnsignedInt font9Glyph6 = cache.addGlyph(font9, 6, {3, 4}, 0, {{15, 30}, {45, 35}}); + UnsignedInt font3Glyph1 = cache.addGlyph(font3, 1, {5, 6}, 1, {{10, 15}, {25, 30}}); + UnsignedInt font9Glyph3 = cache.addGlyph(font9, 3, {6, 9}, 2, {{10, 5}, {25, 10}}); + CORRADE_COMPARE(font9Glyph6, 1); + CORRADE_COMPARE(font3Glyph1, 2); + CORRADE_COMPARE(font9Glyph3, 3); + + /* Direct data access */ + CORRADE_COMPARE_AS(cache.glyphOffsets(), Containers::arrayView({ + {5, 7}, + {3, 4}, + {5, 6}, + {6, 9}, + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(cache.glyphLayers(), Containers::arrayView({ + 2, + 0, + 1, + 2 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(cache.glyphRectangles(), Containers::arrayView({ + {{5, 10}, {15, 30}}, + {{15, 30}, {45, 35}}, + {{10, 15}, {25, 30}}, + {{10, 5}, {25, 10}} + }), TestSuite::Compare::Container); + + /* Querying glyph IDs in a batch way. Invalid IDs are set to 0. */ + UnsignedInt glyphIds9[5]; + cache.glyphIdsInto(font9, {5, 6, 3, 6, 1}, glyphIds9); + CORRADE_COMPARE_AS(Containers::arrayView(glyphIds9), Containers::arrayView({ + 0u, 1u, 3u, 1u, 0u + }), TestSuite::Compare::Container); + + UnsignedInt glyphIds3[3]; + cache.glyphIdsInto(font3, {2, 0, 1}, glyphIds3); + CORRADE_COMPARE_AS(Containers::arrayView(glyphIds3), Containers::arrayView({ + 0u, 0u, 2u + }), TestSuite::Compare::Container); +} + +void AbstractGlyphCacheTest::accessInvalid() { + CORRADE_SKIP_IF_NO_DEBUG_ASSERT(); + + /* Silly test name, but these all test debug asserts while + accessBatchInvalid() tests non-debug asserts */ + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + cache.addFont(9); + UnsignedInt fontId = cache.addFont(3); + cache.addGlyph(0, 1, {}, 2, {}); + cache.addGlyph(fontId, 1, {}, 2, {}); + cache.addGlyph(fontId, 2, {}, 2, {}); + + UnsignedInt fontGlyphIds[]{ + 0, 0, cache.fontGlyphCount(fontId), 0 + }; + UnsignedInt glyphIds[4]; + + std::ostringstream out; + Error redirectError{&out}; + cache.glyph(cache.fontCount(), 0); + cache.glyphId(cache.fontCount(), 0); + cache.glyph(fontId, cache.fontGlyphCount(fontId)); + cache.glyphId(fontId, cache.fontGlyphCount(fontId)); + cache.glyphIdsInto(fontId, fontGlyphIds, glyphIds); + cache.glyph(cache.glyphCount()); + CORRADE_COMPARE_AS(out.str(), + "Text::AbstractGlyphCache::glyph(): index 2 out of range for 2 fonts\n" + "Text::AbstractGlyphCache::glyphId(): index 2 out of range for 2 fonts\n" + "Text::AbstractGlyphCache::glyph(): index 3 out of range for 3 glyphs in font 1\n" + "Text::AbstractGlyphCache::glyphId(): index 3 out of range for 3 glyphs in font 1\n" + "Text::AbstractGlyphCache::glyphIdsInto(): glyph 2 index 3 out of range for 3 glyphs in font 1\n" + "Text::AbstractGlyphCache::glyph(): index 4 out of range for 4 glyphs\n", + TestSuite::Compare::String); +} + +void AbstractGlyphCacheTest::accessBatchInvalid() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; + + cache.addFont(9); + UnsignedInt fontId = cache.addFont(3); + cache.addGlyph(fontId, 1, {}, 2, {}); + cache.addGlyph(fontId, 2, {}, 2, {}); + + UnsignedInt fontGlyphIds[4]; + UnsignedInt glyphIds[4]; + UnsignedInt glyphIdsInvalid[3]; + + std::ostringstream out; + Error redirectError{&out}; + cache.glyphIdsInto(cache.fontCount(), fontGlyphIds, glyphIds); + cache.glyphIdsInto(fontId, fontGlyphIds, glyphIdsInvalid); + CORRADE_COMPARE(out.str(), + "Text::AbstractGlyphCache::glyphIdsInto(): index 2 out of range for 2 fonts\n" + "Text::AbstractGlyphCache::glyphIdsInto(): expected fontGlyphIds and glyphIds views to have the same size but got 4 and 3\n"); +} + +#ifdef MAGNUM_BUILD_DEPRECATED +void AbstractGlyphCacheTest::accessDeprecated() { + DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 200}, {2, 3}}; + + cache.setInvalidGlyph({3, 5}, {{10, 10}, {23, 45}}); + + UnsignedInt fontId = cache.addFont(25); + cache.addGlyph(fontId, 15, {3, 4}, {{15, 30}, {45, 35}}); + CORRADE_IGNORE_DEPRECATED_PUSH + CORRADE_COMPARE(cache[15], std::make_pair( + Vector2i{1, 1}, + Range2Di{{13, 27}, {47, 38}} + )); + CORRADE_IGNORE_DEPRECATED_POP + + /* ID 0 gets the invalid glyph */ + CORRADE_IGNORE_DEPRECATED_PUSH + CORRADE_COMPARE(cache[0], std::make_pair( + Vector2i{1, 2}, + Range2Di{{8, 7}, {25, 48}} + )); + CORRADE_IGNORE_DEPRECATED_POP + + /* Glyph IDs out of bounds get the invalid glyph too */ + CORRADE_IGNORE_DEPRECATED_PUSH + CORRADE_COMPARE(cache[45], std::make_pair( + Vector2i{1, 2}, + Range2Di{{8, 7}, {25, 48}} + )); + CORRADE_IGNORE_DEPRECATED_POP +} + +void AbstractGlyphCacheTest::accessDeprecatedNot2D() { + CORRADE_SKIP_IF_NO_ASSERT(); + + DummyGlyphCache cache{PixelFormat::R32F, {1024, 512, 3}}; std::ostringstream out; Error redirectError{&out}; - cache.image(); - CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::image(): feature advertised but not implemented\n"); + CORRADE_IGNORE_DEPRECATED_PUSH + cache[5]; + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_COMPARE(out.str(), "Text::AbstractGlyphCache::operator[](): can't be used on an array glyph cache\n"); } +#endif }}}} diff --git a/src/Magnum/Text/Test/CMakeLists.txt b/src/Magnum/Text/Test/CMakeLists.txt index 4b0babb54..ff99ccecd 100644 --- a/src/Magnum/Text/Test/CMakeLists.txt +++ b/src/Magnum/Text/Test/CMakeLists.txt @@ -62,7 +62,15 @@ corrade_add_test(TextAbstractFontConverterTest AbstractFontConverterTest.cpp LIBRARIES Magnum MagnumTextTestLib FILES data.bin) target_include_directories(TextAbstractFontConverterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) -corrade_add_test(TextAbstractGlyphCacheTest AbstractGlyphCacheTest.cpp LIBRARIES MagnumTextTestLib) +corrade_add_test(TextAbstractGlyphCacheTest AbstractGlyphCacheTest.cpp + LIBRARIES MagnumTextTestLib MagnumDebugTools) +if(CORRADE_TARGET_EMSCRIPTEN) + if(CMAKE_VERSION VERSION_LESS 3.13) + message(FATAL_ERROR "CMake 3.13+ is required in order to specify Emscripten linker options") + endif() + # Some of the glyph caches tested are rather large + target_link_options(TextAbstractGlyphCacheTest PRIVATE "SHELL:-s ALLOW_MEMORY_GROWTH=1") +endif() corrade_add_test(TextAbstractLayouterTest AbstractLayouterTest.cpp LIBRARIES Magnum MagnumTextTestLib) if(MAGNUM_TARGET_GL AND MAGNUM_BUILD_GL_TESTS) diff --git a/src/Magnum/Text/Test/DistanceFieldGlyphCacheGLTest.cpp b/src/Magnum/Text/Test/DistanceFieldGlyphCacheGLTest.cpp index 8d702e312..6f694ddd7 100644 --- a/src/Magnum/Text/Test/DistanceFieldGlyphCacheGLTest.cpp +++ b/src/Magnum/Text/Test/DistanceFieldGlyphCacheGLTest.cpp @@ -28,6 +28,7 @@ #include /**< @todo remove once Debug is stream-free */ #include #include +#include #include /**< @todo remove once Debug is stream-free */ #include @@ -35,6 +36,10 @@ #include "Magnum/ImageView.h" #include "Magnum/PixelFormat.h" #include "Magnum/DebugTools/CompareImage.h" +#ifdef MAGNUM_TARGET_GLES +#include "Magnum/DebugTools/TextureImage.h" +#endif +#include "Magnum/Math/Range.h" #include "Magnum/GL/OpenGLTester.h" #include "Magnum/GL/Extensions.h" #include "Magnum/GL/PixelFormat.h" @@ -118,27 +123,66 @@ void DistanceFieldGlyphCacheGLTest::setImage() { CORRADE_COMPARE(inputImage->size(), (Vector2i{256, 256})); DistanceFieldGlyphCache cache{data.sourceSize, data.size, 32}; - /* Test also uploading under an offset */ - cache.setImage(data.sourceOffset, *inputImage); + Containers::StridedArrayView3D src = inputImage->pixels(); + /* Test also uploading under an offset. The cache might be three-component + in some cases, slice the destination view to just the first component */ + /** @todo actually the input can be just luminance, only the destination + cannot -- fix by dropping the dependency on GlyphCache and creating the + texture directly */ + Utility::copy(src, cache.image().pixels()[0].sliceSize({ + std::size_t(data.sourceOffset.y()), + std::size_t(data.sourceOffset.x()), + 0}, src.size())); + cache.flushImage(Range2Di::fromSize(data.sourceOffset, inputImage->size())); MAGNUM_VERIFY_NO_GL_ERROR(); + /* On GLES processedImage() isn't implemented as it'd mean creating a + temporary framebuffer. Do it via DebugTools here instead, we cannot + really verify that the size matches, but at least something. */ #ifndef MAGNUM_TARGET_GLES - Image2D actual = cache.image(); + Image3D actual3 = cache.processedImage(); + /** @todo ugh have slicing on images directly already */ + MutableImageView2D actual{actual3.format(), actual3.size().xy(), actual3.data()}; + #else + /* Pick a format that matches the internal texture format. This is rather + shitty, TBH. */ + #if !(defined(MAGNUM_TARGET_GLES) && defined(MAGNUM_TARGET_GLES2)) + const GL::PixelFormat format = GL::PixelFormat::Red; + #else + GL::PixelFormat format; + #ifndef MAGNUM_TARGET_WEBGL + if(GL::Context::current().isExtensionSupported()) { + format = GL::PixelFormat::Red; + } else + #endif + { + format = GL::PixelFormat::RGB; + } + #endif + Image2D actual = DebugTools::textureSubImage(cache.texture(), 0, {{}, data.size}, {format, GL::PixelType::UnsignedByte}); + #endif 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."); + /* On GLES2 if EXT_unpack_subimage isn't supported or on WebGL 1, the whole + texture gets uploaded and processed every time. Which circumvents this + problem. */ + #if defined(MAGNUM_TARGET_GLES2) && !defined(MAGNUM_TARGET_WEBGL) + CORRADE_EXPECT_FAIL_IF(GL::Context::current().isExtensionSupported() && !data.sourceOffset.isZero(), + "The distance field tool is currently broken if a non-zero offset is used."); + #elif !defined(MAGNUM_TARGET_GLES2) CORRADE_EXPECT_FAIL_IF(!data.sourceOffset.isZero(), "The distance field tool is currently broken if a non-zero offset is used."); - CORRADE_COMPARE_WITH(actual.pixels().exceptPrefix(data.offset), + #endif + /* The format may be three-component, consider just the first channel */ + Containers::StridedArrayView3D pixels = actual.pixels(); + CORRADE_COMPARE_WITH((Containers::arrayCast<2, const UnsignedByte>(pixels.prefix({pixels.size()[0], pixels.size()[1], 1})).exceptPrefix(data.offset)), Utility::Path::join(TEXTURETOOLS_DISTANCEFIELDGLTEST_DIR, "output.tga"), /* Same threshold as in TextureTools DistanceFieldGLTest */ (DebugTools::CompareImageToFile{_manager, 1.0f, 0.178f})); - #else - CORRADE_SKIP("Skipping image download test as it's not available on GLES."); - #endif } void DistanceFieldGlyphCacheGLTest::setDistanceFieldImage() { @@ -175,8 +219,17 @@ void DistanceFieldGlyphCacheGLTest::setDistanceFieldImage() { cache.setDistanceFieldImage({8, 4}, ImageView2D{format, GL::PixelType::UnsignedByte, {8, 4}, data}); MAGNUM_VERIFY_NO_GL_ERROR(); + /* On GLES processedImage() isn't implemented as it'd mean creating a + temporary framebuffer. Do it via DebugTools here instead, we cannot + really verify that the size matches, but at least something. */ #ifndef MAGNUM_TARGET_GLES - Image2D actual = cache.image(); + Image3D actual3 = cache.processedImage(); + /** @todo ugh have slicing on images directly already */ + MutableImageView2D actual{actual3.format(), actual3.size().xy(), actual3.data()}; + #else + Image2D actualGL = DebugTools::textureSubImage(cache.texture(), 0, {{}, {16, 8}}, {format, GL::PixelType::UnsignedByte}); + ImageView2D actual{*GL::genericPixelFormat(format, GL::PixelType::UnsignedByte), actualGL.size(), actualGL.data()}; + #endif MAGNUM_VERIFY_NO_GL_ERROR(); UnsignedByte expected[]{ @@ -192,9 +245,6 @@ void DistanceFieldGlyphCacheGLTest::setDistanceFieldImage() { CORRADE_COMPARE_AS(actual, (ImageView2D{PixelFormat::R8Unorm, {16, 8}, expected}), DebugTools::CompareImage); - #else - CORRADE_SKIP("Skipping image download test as it's not available on GLES."); - #endif } void DistanceFieldGlyphCacheGLTest::setDistanceFieldImageOutOfRange() { diff --git a/src/Magnum/Text/Test/GlyphCacheGLTest.cpp b/src/Magnum/Text/Test/GlyphCacheGLTest.cpp index 5f23b4133..75b7797e7 100644 --- a/src/Magnum/Text/Test/GlyphCacheGLTest.cpp +++ b/src/Magnum/Text/Test/GlyphCacheGLTest.cpp @@ -29,8 +29,12 @@ #include "Magnum/ImageView.h" #include "Magnum/PixelFormat.h" #include "Magnum/DebugTools/CompareImage.h" +#ifdef MAGNUM_TARGET_GLES +#include "Magnum/DebugTools/TextureImage.h" +#endif #include "Magnum/GL/OpenGLTester.h" #include "Magnum/GL/TextureFormat.h" +#include "Magnum/Math/Range.h" #include "Magnum/Text/GlyphCache.h" namespace Magnum { namespace Text { namespace Test { namespace { @@ -39,16 +43,18 @@ struct GlyphCacheGLTest: GL::OpenGLTester { explicit GlyphCacheGLTest(); void initialize(); - void initializeExplicitFormat(); + void initializeCustomFormat(); void setImage(); + void setImageCustomFormat(); }; GlyphCacheGLTest::GlyphCacheGLTest() { addTests({&GlyphCacheGLTest::initialize, - &GlyphCacheGLTest::initializeExplicitFormat, + &GlyphCacheGLTest::initializeCustomFormat, - &GlyphCacheGLTest::setImage}); + &GlyphCacheGLTest::setImage, + &GlyphCacheGLTest::setImageCustomFormat}); } void GlyphCacheGLTest::initialize() { @@ -61,7 +67,7 @@ void GlyphCacheGLTest::initialize() { #endif } -void GlyphCacheGLTest::initializeExplicitFormat() { +void GlyphCacheGLTest::initializeCustomFormat() { GlyphCache cache{ #ifndef MAGNUM_TARGET_GLES2 GL::TextureFormat::RGBA8, @@ -77,43 +83,95 @@ void GlyphCacheGLTest::initializeExplicitFormat() { #endif } +const UnsignedByte InputData[]{ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, +}; + +const UnsignedByte ExpectedData[]{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0, 0, 0, 0, 0, 0, 0, 0, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0, 0, 0, 0, 0, 0, 0, 0, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, +}; + void GlyphCacheGLTest::setImage() { GlyphCache cache{{16, 8}}; - /* Clear the texture first, as it'd have random garbage otherwise */ - UnsignedByte zeros[16*8]{}; - cache.setImage({}, ImageView2D{PixelFormat::R8Unorm, {16, 8}, zeros}); + cache.setImage({8, 4}, ImageView2D{PixelFormat::R8Unorm, {8, 4}, InputData}); MAGNUM_VERIFY_NO_GL_ERROR(); - UnsignedByte data[]{ - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, - }; - cache.setImage({8, 4}, ImageView2D{PixelFormat::R8Unorm, {8, 4}, data}); + MutableImageView3D actual3 = cache.image(); + /** @todo ugh have slicing on images directly already */ + MutableImageView2D actual{actual3.format(), actual3.size().xy(), actual3.data()}; MAGNUM_VERIFY_NO_GL_ERROR(); + ImageView2D expected{PixelFormat::R8Unorm, {16, 8}, ExpectedData}; + CORRADE_COMPARE_AS(actual, + expected, + DebugTools::CompareImage); + + #ifdef MAGNUM_TARGET_GLES2 + CORRADE_SKIP("Luminance format used on GLES2 isn't usable for framebuffer reading, can't verify texture contents."); + #else + /* Verify the actual texture. It should be the same as above. On GLES we + cannot really verify that the size matches, but at least something. */ #ifndef MAGNUM_TARGET_GLES - Image2D actual = cache.image(); + Image2D image = cache.texture().image(0, {PixelFormat::R8Unorm}); + #else + Image2D image = DebugTools::textureSubImage(cache.texture(), 0, {{}, {16, 8}}, {PixelFormat::R8Unorm}); + #endif + MAGNUM_VERIFY_NO_GL_ERROR(); + CORRADE_COMPARE_AS(image, + expected, + DebugTools::CompareImage); + #endif +} + +void GlyphCacheGLTest::setImageCustomFormat() { + /* Same as setImage(), but with a four-channel format (so quarter of + width). Needed to be able to read the texture on ES2 to verify the + upload works, as there's a special case for when the EXT_unpack_subimage + extension isn't present. */ + + GlyphCache cache{ + #if !(defined(MAGNUM_TARGET_GLES2) && defined(MAGNUM_TARGET_WEBGL)) + GL::TextureFormat::RGBA8, + #else + GL::TextureFormat::RGBA, + #endif + {4, 8}}; + + cache.setImage({2, 4}, ImageView2D{PixelFormat::RGBA8Unorm, {2, 4}, InputData}); + MAGNUM_VERIFY_NO_GL_ERROR(); + + MutableImageView3D actual3 = cache.image(); + /** @todo ugh have slicing on images directly already */ + MutableImageView2D actual{actual3.format(), actual3.size().xy(), actual3.data()}; MAGNUM_VERIFY_NO_GL_ERROR(); - UnsignedByte expected[]{ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0, 0, 0, 0, 0, 0, 0, 0, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, - 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0, 0, 0, 0, 0, 0, 0, 0, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, - }; + ImageView2D expected{PixelFormat::RGBA8Unorm, {4, 8}, ExpectedData}; CORRADE_COMPARE_AS(actual, - (ImageView2D{PixelFormat::R8Unorm, {16, 8}, expected}), + expected, DebugTools::CompareImage); + + /* Verify the actual texture. It should be the same as above. On GLES we + cannot really verify that the size matches, but at least something. */ + #ifndef MAGNUM_TARGET_GLES + Image2D image = cache.texture().image(0, {PixelFormat::RGBA8Unorm}); #else - CORRADE_SKIP("Skipping image download test as it's not available on GLES."); + Image2D image = DebugTools::textureSubImage(cache.texture(), 0, {{}, {4, 8}}, {PixelFormat::RGBA8Unorm}); #endif + MAGNUM_VERIFY_NO_GL_ERROR(); + CORRADE_COMPARE_AS(image, + expected, + DebugTools::CompareImage); } }}}}