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); } }}}}