From 085ae0ee8d958634d721bffbbc7778d65f408ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 23 Apr 2023 20:44:18 +0200 Subject: [PATCH] SceneTools: add filterObjects(). --- src/Magnum/SceneTools/Filter.cpp | 130 +++++++++ src/Magnum/SceneTools/Filter.h | 23 ++ src/Magnum/SceneTools/Test/FilterTest.cpp | 321 +++++++++++++++++++++- 3 files changed, 473 insertions(+), 1 deletion(-) diff --git a/src/Magnum/SceneTools/Filter.cpp b/src/Magnum/SceneTools/Filter.cpp index fdda8e841..e52229c17 100644 --- a/src/Magnum/SceneTools/Filter.cpp +++ b/src/Magnum/SceneTools/Filter.cpp @@ -26,6 +26,7 @@ #include "Filter.h" #include +#include #include #include #include @@ -259,4 +260,133 @@ Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, const std::in return filterFieldEntries(scene, Containers::arrayView(entriesToKeep)); } +namespace { + +template std::size_t filterObjectsImplementation(const Trade::SceneData& scene, const Containers::ArrayView> fieldStorage, const Containers::MutableBitArrayView maskStorage, const Containers::BitArrayView objects, std::map, Containers::Optional>& uniqueMappings) { + std::size_t fieldOffset = 0; + std::size_t maskOffset = 0; + for(UnsignedInt fieldId = 0; fieldId != scene.fieldCount(); ++fieldId) { + /* Skip empty fields as there's nothing to do for them and they don't + even have an entry in the uniqueMappings map */ + if(!scene.fieldSize(fieldId)) + continue; + + const Containers::StridedArrayView1D mapping = scene.mapping(fieldId); + + /* Shared mappings need to stay shared, thus filterFieldEntries() needs + to get the exact same mask for such fields -- for implementation + simplicity not just the bit values but the actual view */ + Containers::Optional& sharedMapping = uniqueMappings.at(std::make_tuple(mapping.data(), mapping.size(), mapping.stride())); + + /* If a mask was already calculated for this mapping, reuse the view */ + if(sharedMapping) { + /* If the field wasn't filtered in any way, it wasn't added to the + list, which is indicated by ~UnsignedInt{}. Do nothing in that + case. */ + if(*sharedMapping != ~UnsignedInt{}) + fieldStorage[fieldOffset++] = {fieldId, fieldStorage[*sharedMapping].second()}; + + /* If not, calculate the mask and remember it for potential other + fields that share the same mapping view */ + } else { + const Containers::MutableBitArrayView mask = maskStorage.sliceSize(maskOffset, mapping.size()); + + bool anyFiltered = false; + for(std::size_t i = 0; i != mapping.size(); ++i) { + /** @todo ugh! mask.set(i, objects[mapping[i]]) and then .all() + once it's implemented (needs BMI variants similarly to + count()) */ + if(objects[mapping[i]]) + mask.set(i); + else { + anyFiltered = true; + mask.reset(i); + } + } + + /* Only add the field to the list if it's not all 1s */ + if(anyFiltered) { + sharedMapping = fieldOffset; + fieldStorage[fieldOffset++] = {fieldId, mask}; + /* Not bothering with rounding this to whole bytes as + Utility::copyMasked() has to special-case the begin/end + anyway */ + maskOffset += mask.size(); + } else { + sharedMapping = ~UnsignedInt{}; + } + } + } + + CORRADE_INTERNAL_ASSERT(fieldOffset <= fieldStorage.size()); + CORRADE_INTERNAL_ASSERT(maskOffset <= maskStorage.size()); + + return fieldOffset; +} + +} + +Trade::SceneData filterObjects(const Trade::SceneData& scene, const Containers::BitArrayView objects) { + CORRADE_ASSERT(objects.size() == scene.mappingBound(), + "SceneTools::filterObjects(): expected" << scene.mappingBound() << "bits but got" << objects.size(), (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + + /** @todo while a BitArrayView is certainly faster for lookup than an + unordered list of IDs, it might become rather problematic in cases + where the mapping bound is sparse and *really huge* (i.e., storing + pointers) -- then there either needs to be an overload that takes an + `ArrayView` and does some less ideal lookup, or a + `packObjects()` tool that makes the object numbering contiguous for + this API to be usable, storing also mapping back to the original ID in + the scene, and an `unpackObjects()` that restores the original IDs */ + + /* Count the total count of bits possibly needed */ + std::size_t bitCount = 0; + for(UnsignedInt i = 0; i != scene.fieldCount(); ++i) + bitCount += scene.fieldSize(i); + + /* Allocate scratch memory for all the bits and field references */ + Containers::ArrayView> fieldStorage; + Containers::MutableBitArrayView maskStorage; + Containers::ArrayTuple storage{ + {NoInit, scene.fieldCount(), fieldStorage}, + {NoInit, bitCount, maskStorage} + }; + + /* Collect a map of unique mappings. The value is a placeholder where + filterObjectsImplementation() will subsequently record a reference to a + BitArrayView that should be used for all fields. */ + std::map, Containers::Optional> uniqueMappings; + for(UnsignedInt i = 0; i != scene.fieldCount(); ++i) { + /* Skip empty fields as those make no sense to include for sharing */ + if(!scene.fieldSize(i)) + continue; + + const Containers::StridedArrayView2D mapping = scene.mapping(i); + uniqueMappings.emplace(std::make_tuple(mapping.data(), mapping.size()[0], mapping.stride()[0]), Containers::NullOpt); + } + + /* Delegate to a concrete filtering implementation based on used mapping + type. Returns the prefix of fieldStorage that got filled, with fields + that didn't need to be changed omitted. */ + std::size_t fieldCount = ~std::size_t{}; + switch(scene.mappingType()) { + case Trade::SceneMappingType::UnsignedByte: + fieldCount = filterObjectsImplementation(scene, fieldStorage, maskStorage, objects, uniqueMappings); + break; + case Trade::SceneMappingType::UnsignedShort: + fieldCount = filterObjectsImplementation(scene, fieldStorage, maskStorage, objects, uniqueMappings); + break; + case Trade::SceneMappingType::UnsignedInt: + fieldCount = filterObjectsImplementation(scene, fieldStorage, maskStorage, objects, uniqueMappings); + break; + case Trade::SceneMappingType::UnsignedLong: + fieldCount = filterObjectsImplementation(scene, fieldStorage, maskStorage, objects, uniqueMappings); + break; + } + CORRADE_INTERNAL_ASSERT(fieldCount != ~std::size_t{}); + + /* Delegate the rest to the low-level field entry filtering API */ + return filterFieldEntries(scene, fieldStorage.prefix(fieldCount)); +} + }} diff --git a/src/Magnum/SceneTools/Filter.h b/src/Magnum/SceneTools/Filter.h index 83afadd97..7d064e938 100644 --- a/src/Magnum/SceneTools/Filter.h +++ b/src/Magnum/SceneTools/Filter.h @@ -157,6 +157,29 @@ MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterFieldEntries(const Trade::SceneD */ MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, std::initializer_list> entriesToKeep); +/** +@brief Filter objects in a scene +@m_since_latest + +Returns a copy of @p scene containing the same fields but only with entries +mapped to objects for which the corresponding bit in @p objectsToKeep is set. +The size of @p objectsToKeep is expected to be equal to +@ref Trade::SceneData::mappingBound(). + +Fields that don't contain any entries mapped to filtered-out objects are passed +through unchanged. The data filtering is performed using +@ref filterFieldEntries() which then delegates to @ref combineFields() for +repacking the data, see their documentation for more information. + +Note that this function performs only filtering of the data, it doesn't change +the data in any other way. If there are references to the removed objects from +other fields such as @ref Trade::SceneField::Parent, it's the responsibility of +the caller to deal with them either before or after calling this API, otherwise +the returned data may end up being unusable. +@experimental +*/ +MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterObjects(const Trade::SceneData& scene, Containers::BitArrayView objectsToKeep); + }} #endif diff --git a/src/Magnum/SceneTools/Test/FilterTest.cpp b/src/Magnum/SceneTools/Test/FilterTest.cpp index b07cff326..a6a9b083b 100644 --- a/src/Magnum/SceneTools/Test/FilterTest.cpp +++ b/src/Magnum/SceneTools/Test/FilterTest.cpp @@ -59,6 +59,12 @@ struct FilterTest: TestSuite::Tester { void fieldEntriesSharedMapping(); void fieldEntriesSharedMappingInvalid(); + + template void objects(); + void objectsUnchangedFields(); + void objectsSharedMapping(); + void objectsSharedMappingAllRemoved(); + void objectsWrongBitCount(); }; using namespace Math::Literals; @@ -91,7 +97,16 @@ FilterTest::FilterTest() { &FilterTest::fieldEntriesBitField, &FilterTest::fieldEntriesSharedMapping, - &FilterTest::fieldEntriesSharedMappingInvalid}); + &FilterTest::fieldEntriesSharedMappingInvalid, + + &FilterTest::objects, + &FilterTest::objects, + &FilterTest::objects, + &FilterTest::objects, + &FilterTest::objectsUnchangedFields, + &FilterTest::objectsSharedMapping, + &FilterTest::objectsSharedMappingAllRemoved, + &FilterTest::objectsWrongBitCount}); } void FilterTest::fields() { @@ -760,6 +775,310 @@ void FilterTest::fieldEntriesSharedMappingInvalid() { "SceneTools::filterFieldEntries(): field Trade::SceneField::Custom(1) shares mapping with 3 fields but only 2 are filtered\n"); } +template void FilterTest::objects() { + setTestCaseTemplateName(Math::TypeTraits::name()); + + const struct { + T meshMapping[5]{7, 8, 15, 3, 2}; + UnsignedByte mesh[5]{2, 222, 3, 222, 222}; + T lightMapping[4]{2, 1, 3, 2}; + UnsignedInt light[4]{66666, 23, 66666, 66666}; + T parentMapping[3]{2, 3, 8}; + Short parents[3]{6666, 6666, 6666}; + } data[1]; + + Trade::SceneData scene{Trade::Implementation::sceneMappingTypeFor(), 76, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMapping), + Containers::arrayView(data->mesh)}, + /* This one has duplicate entries for an object, both will be removed */ + Trade::SceneFieldData{Trade::SceneField::Light, + Containers::arrayView(data->lightMapping), + Containers::arrayView(data->light)}, + /* This one gets all entries removed */ + Trade::SceneFieldData{Trade::SceneField::Parent, + Containers::arrayView(data->parentMapping), + Containers::arrayView(data->parents)}, + /* This one is already empty */ + Trade::SceneFieldData{Trade::SceneField::Camera, + Containers::ArrayView{}, + Containers::ArrayView{}}, + }}; + + Containers::BitArray objectsToKeep{DirectInit, std::size_t(scene.mappingBound()), true}; + objectsToKeep.reset(8); + objectsToKeep.reset(3); + objectsToKeep.reset(2); + + Trade::SceneData filtered = filterObjects(scene, objectsToKeep); + + CORRADE_COMPARE(filtered.fieldCount(), 4); + CORRADE_COMPARE(filtered.mappingType(), Trade::Implementation::sceneMappingTypeFor()); + CORRADE_COMPARE(filtered.mappingBound(), 76); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Mesh)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Mesh), + Containers::arrayView({7, 15}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Mesh), + Containers::arrayView({2, 3}), + TestSuite::Compare::Container); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Light)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Light), + Containers::arrayView({1}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Light), + Containers::arrayView({23}), + TestSuite::Compare::Container); + + /* Parents are all removed */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Parent)); + CORRADE_COMPARE(filtered.fieldSize(Trade::SceneField::Parent), 0); + + /* Cameras were empty before already */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Camera)); + CORRADE_COMPARE(filtered.fieldSize(Trade::SceneField::Camera), 0); + + /* The attribute data should not be a growable array to make this usable in + plugins */ + Containers::Array fieldData = filtered.releaseFieldData(); + CORRADE_VERIFY(!fieldData.deleter()); +} + +void FilterTest::objectsUnchangedFields() { + /* Compared to above, this contains fields that don't have any objects + that should be filtered out, which are thus passed through unchanged + (and thus can be even of type that is unuspported by + filterFieldEntries()) */ + + const struct { + UnsignedShort meshMapping[5]{7, 8, 15, 3, 2}; + UnsignedByte mesh[5]{2, 222, 3, 222, 222}; + UnsignedShort visibilityMapping[2]{22, 1}; + bool visible[2]{false, true}; + } data[1]; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 76, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::sceneFieldCustom(15), + Containers::arrayView(data->visibilityMapping), + Containers::stridedArrayView(data->visible).sliceBit(0)}, + }}; + + Containers::BitArray objectsToKeep{DirectInit, std::size_t(scene.mappingBound()), true}; + objectsToKeep.reset(8); + objectsToKeep.reset(3); + objectsToKeep.reset(2); + + Trade::SceneData filtered = filterObjects(scene, objectsToKeep); + CORRADE_COMPARE(filtered.fieldCount(), 2); + CORRADE_COMPARE(filtered.mappingType(), Trade::SceneMappingType::UnsignedShort); + CORRADE_COMPARE(filtered.mappingBound(), 76); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Mesh)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Mesh), + Containers::arrayView({7, 15}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Mesh), + Containers::arrayView({2, 3}), + TestSuite::Compare::Container); + + /* Bits weren't affected and thus were passed through unchanged */ + CORRADE_VERIFY(filtered.hasField(Trade::sceneFieldCustom(15))); + CORRADE_COMPARE_AS(filtered.mapping(Trade::sceneFieldCustom(15)), + Containers::arrayView(data->visibilityMapping), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.fieldBits(Trade::sceneFieldCustom(15)), + Containers::stridedArrayView(data->visible).sliceBit(0), + TestSuite::Compare::Container); +} + +void FilterTest::objectsSharedMapping() { + const struct { + UnsignedShort meshMaterialMapping[5]{7, 8, 15, 3, 2}; + UnsignedByte mesh[5]{2, 222, 3, 222, 222}; + Byte meshMaterial[5]{-1, 111, 7, 111, 111}; + UnsignedShort trsMapping[5]{1, 8, 7, 2, 15}; + Vector2 translation[5]{ + {1.0f, 2.0f}, + {}, + {3.0f, 4.0f}, + {}, + {5.0f, 6.0f} + }; + Complex rotation[5]{ + Complex::rotation(15.0_degf), + {}, + Complex::rotation(30.0_degf), + {}, + Complex::rotation(45.0_degf) + }; + Float uniformScale[5]{10.0f, 0.0f, -5.0f, 0.0f, 555.0f}; + UnsignedInt light[2]{34, 25}; + Int parent[3]{-1, 0, 3}; + } data[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 176, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::SceneField::MeshMaterial, + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->meshMaterial)}, + Trade::SceneFieldData{Trade::SceneField::Translation, + Containers::arrayView(data->trsMapping), + Containers::arrayView(data->translation)}, + Trade::SceneFieldData{Trade::SceneField::Rotation, + Containers::arrayView(data->trsMapping), + Containers::arrayView(data->rotation)}, + /* Shares trsMapping, sharing should be preserved even though not + enforced */ + Trade::SceneFieldData{Trade::sceneFieldCustom(15), + Containers::arrayView(data->trsMapping), + Containers::arrayView(data->uniformScale)}, + /* Shares a prefix of meshMaterialMapping, should not be preserved */ + Trade::SceneFieldData{Trade::SceneField::Light, + Containers::arrayView(data->meshMaterialMapping).prefix(2), + Containers::arrayView(data->light)}, + /* Shares every 2nd item of trsMapping, should not be preserved */ + Trade::SceneFieldData{Trade::SceneField::Parent, + Containers::stridedArrayView(data->trsMapping).every(2), + Containers::arrayView(data->parent)}, + }}; + + Containers::BitArray objectsToKeep{DirectInit, std::size_t(scene.mappingBound()), true}; + objectsToKeep.reset(8); + objectsToKeep.reset(3); + objectsToKeep.reset(2); + + Trade::SceneData filtered = filterObjects(scene, objectsToKeep); + CORRADE_COMPARE(filtered.fieldCount(), 7); + CORRADE_COMPARE(filtered.mappingType(), Trade::SceneMappingType::UnsignedShort); + CORRADE_COMPARE(filtered.mappingBound(), 176); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Mesh)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Mesh), + Containers::arrayView({7, 15}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Mesh), + Containers::arrayView({2, 3}), + TestSuite::Compare::Container); + + /* Mapping shared with Mesh */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::MeshMaterial)); + CORRADE_COMPARE(filtered.mapping(Trade::SceneField::MeshMaterial).data(), + filtered.mapping(Trade::SceneField::Mesh).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::MeshMaterial), + Containers::arrayView({-1, 7}), + TestSuite::Compare::Container); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Translation)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Translation), + Containers::arrayView({1, 7, 15}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Translation), + Containers::arrayView({{1.0f, 2.0f}, {3.0f, 4.0f}, {5.0f, 6.0f}}), + TestSuite::Compare::Container); + + /* Mapping shared with Translation */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Rotation)); + CORRADE_COMPARE(filtered.mapping(Trade::SceneField::Rotation).data(), + filtered.mapping(Trade::SceneField::Translation).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Rotation), + Containers::arrayView({Complex::rotation(15.0_degf), Complex::rotation(30.0_degf), Complex::rotation(45.0_degf)}), + TestSuite::Compare::Container); + + /* Mapping shared with Translation again */ + CORRADE_VERIFY(filtered.hasField(Trade::sceneFieldCustom(15))); + CORRADE_COMPARE(filtered.mapping(Trade::sceneFieldCustom(15)).data(), + filtered.mapping(Trade::SceneField::Translation).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::sceneFieldCustom(15)), + Containers::arrayView({10.0f, -5.0f, 555.0f}), + TestSuite::Compare::Container); + + /* These fields don't share any mapping even though they could */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Light)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Light), + Containers::arrayView({7}), + TestSuite::Compare::Container); + CORRADE_VERIFY(filtered.mapping(Trade::SceneField::Light).data() != filtered.mapping(Trade::SceneField::Mesh).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Light), + Containers::arrayView({34}), + TestSuite::Compare::Container); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Parent)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Parent), + Containers::arrayView({1, 7, 15}), + TestSuite::Compare::Container); + CORRADE_VERIFY(filtered.mapping(Trade::SceneField::Parent).data() != filtered.mapping(Trade::SceneField::Translation).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Parent), + Containers::arrayView(data->parent), + TestSuite::Compare::Container); +} + +void FilterTest::objectsSharedMappingAllRemoved() { + const struct { + UnsignedShort meshMaterialMapping[3]{8, 3, 2}; + UnsignedByte mesh[3]{}; + UnsignedShort lightMapping[3]{2, 1, 3}; + UnsignedInt light[3]{66666, 23, 66666}; + Byte meshMaterial[3]{}; + } data[1]; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 76, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::SceneField::Light, + Containers::arrayView(data->lightMapping), + Containers::arrayView(data->light)}, + Trade::SceneFieldData{Trade::SceneField::MeshMaterial, + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->meshMaterial)}, + }}; + + Containers::BitArray objectsToKeep{DirectInit, std::size_t(scene.mappingBound()), true}; + objectsToKeep.reset(8); + objectsToKeep.reset(3); + objectsToKeep.reset(2); + + Trade::SceneData filtered = filterObjects(scene, objectsToKeep); + CORRADE_COMPARE(filtered.fieldCount(), 3); + CORRADE_COMPARE(filtered.mappingType(), Trade::SceneMappingType::UnsignedShort); + CORRADE_COMPARE(filtered.mappingBound(), 76); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Mesh)); + CORRADE_COMPARE(filtered.fieldSize(Trade::SceneField::Mesh), 0); + + /* This one should reuse the (emptied) Mesh mapping instead of going + through everything again */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::MeshMaterial)); + CORRADE_COMPARE(filtered.fieldSize(Trade::SceneField::MeshMaterial), 0); + + /* Other fields get filtered as usual */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Light)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Light), + Containers::arrayView({1}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Light), + Containers::arrayView({23}), + TestSuite::Compare::Container); +} + +void FilterTest::objectsWrongBitCount() { + CORRADE_SKIP_IF_NO_ASSERT(); + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 176, nullptr, {}}; + + std::ostringstream out; + Error redirectError{&out}; + filterObjects(scene, Containers::BitArray{ValueInit, 177}); + CORRADE_COMPARE(out.str(), "SceneTools::filterObjects(): expected 176 bits but got 177\n"); +} + }}}} CORRADE_TEST_MAIN(Magnum::SceneTools::Test::FilterTest)