diff --git a/indra/llappearance/llavatarappearance.cpp b/indra/llappearance/llavatarappearance.cpp index 6e8c5685d0..5f8b6d258f 100644 --- a/indra/llappearance/llavatarappearance.cpp +++ b/indra/llappearance/llavatarappearance.cpp @@ -1128,12 +1128,16 @@ bool LLAvatarAppearance::loadMeshNodes() LLPolyMesh *poly_mesh = NULL; + // Hair, skirt and eyelashes are drawn blended, and keep the order + // their triangles were made in. + const bool blended = mesh_id == MESH_ID_HAIR || mesh_id == MESH_ID_SKIRT || mesh_id == MESH_ID_EYELASH; + if (!info->mReferenceMeshName.empty()) { polymesh_map_t::const_iterator polymesh_iter = mPolyMeshes.find(info->mReferenceMeshName); if (polymesh_iter != mPolyMeshes.end()) { - poly_mesh = LLPolyMesh::getMesh(info->mMeshFileName, polymesh_iter->second); + poly_mesh = LLPolyMesh::getMesh(info->mMeshFileName, polymesh_iter->second, blended); poly_mesh->setAvatar(this); } else @@ -1145,7 +1149,7 @@ bool LLAvatarAppearance::loadMeshNodes() } else { - poly_mesh = LLPolyMesh::getMesh(info->mMeshFileName); + poly_mesh = LLPolyMesh::getMesh(info->mMeshFileName, NULL, blended); poly_mesh->setAvatar(this); } diff --git a/indra/llappearance/llpolymesh.cpp b/indra/llappearance/llpolymesh.cpp index 0fe7648e7b..479e5a82d6 100644 --- a/indra/llappearance/llpolymesh.cpp +++ b/indra/llappearance/llpolymesh.cpp @@ -270,7 +270,7 @@ bool LLPolyMeshSharedData::allocateJointNames( U32 numJointNames ) //-------------------------------------------------------------------- // LLPolyMeshSharedData::loadMesh() //-------------------------------------------------------------------- -bool LLPolyMeshSharedData::loadMesh( const std::string& fileName ) +bool LLPolyMeshSharedData::loadMesh( const std::string& fileName, bool keep_order ) { //------------------------------------------------------------------------- // Open the file @@ -565,7 +565,7 @@ bool LLPolyMeshSharedData::loadMesh( const std::string& fileName ) // weights address vertices, which keep their order, and an // LOD's faces are its own. const U32 vertex_count = (U32)mNumVertices; - bool in_range = numFaces > 0; + bool in_range = numFaces > 0 && !keep_order; for (i = 0; i < (U32)numFaces * 3 && in_range; i++) { in_range = (U32)mFaces[i / 3][i % 3] < vertex_count; @@ -835,7 +835,7 @@ LLPolyMesh::~LLPolyMesh() //----------------------------------------------------------------------------- // LLPolyMesh::getMesh() //----------------------------------------------------------------------------- -LLPolyMesh *LLPolyMesh::getMesh(const std::string &name, LLPolyMesh* reference_mesh) +LLPolyMesh *LLPolyMesh::getMesh(const std::string &name, LLPolyMesh* reference_mesh, bool keep_order) { //------------------------------------------------------------------------- // search for an existing mesh by this name @@ -859,7 +859,7 @@ LLPolyMesh *LLPolyMesh::getMesh(const std::string &name, LLPolyMesh* reference_m { mesh_data->setupLOD(reference_mesh->getSharedData()); } - if ( ! mesh_data->loadMesh( full_path ) ) + if ( ! mesh_data->loadMesh( full_path, keep_order ) ) { delete mesh_data; return NULL; diff --git a/indra/llappearance/llpolymesh.h b/indra/llappearance/llpolymesh.h index 6de68e9b37..c55a58bd44 100644 --- a/indra/llappearance/llpolymesh.h +++ b/indra/llappearance/llpolymesh.h @@ -128,8 +128,10 @@ class LLPolyMeshSharedData // Retrieve the number of KB of memory used by this instance U32 getNumKB(); - // Load mesh data from file - bool loadMesh( const std::string& fileName ); + // Load mesh data from file. Its triangles are ordered for the vertex + // cache unless keep_order is set, for a mesh drawn blended, whose + // triangles blend over one another in the order they were made in. + bool loadMesh( const std::string& fileName, bool keep_order = false ); public: void genIndices(S32 offset); @@ -170,7 +172,8 @@ class LLPolyMesh // Requests a mesh by name. // If the mesh already exists in the global mesh table, it is returned, // otherwise it is loaded from file, added to the table, and returned. - static LLPolyMesh *getMesh( const std::string &name, LLPolyMesh* reference_mesh = NULL); + // keep_order is loadMesh's. + static LLPolyMesh *getMesh( const std::string &name, LLPolyMesh* reference_mesh = NULL, bool keep_order = false); // Frees all loaded meshes. // This should only be called once you know there are no outstanding diff --git a/indra/llmath/CMakeLists.txt b/indra/llmath/CMakeLists.txt index e0787a35ed..7c1a792c57 100644 --- a/indra/llmath/CMakeLists.txt +++ b/indra/llmath/CMakeLists.txt @@ -109,13 +109,16 @@ target_precompile_headers(llmath REUSE_FROM llprecompiled) # compiler may round one sum differently at each place it inlines it, and a # touch would then land a unit in the last place from the coordinate its # vertex is drawn with. MSVC is held to the same by the float_control pragma -# around the kernels in alsimdkernels.inl. -if(NOT COMPILER_IS_MSVC AND NOT COMPILER_IS_CLANG_CL) +# around the kernels in alsimdkernels.inl, which clang-cl does not take, and +# clang-cl by the same options as Clang. +set(AL_STRICT_FP_OPTIONS "-ffp-contract=off;-fno-associative-math;-fno-reciprocal-math") +if(COMPILER_IS_CLANG_CL) + list(TRANSFORM AL_STRICT_FP_OPTIONS PREPEND "/clang:") +endif() +if(NOT COMPILER_IS_MSVC) set_source_files_properties( alsimdplanar.cpp - PROPERTIES - COMPILE_OPTIONS "-ffp-contract=off;-fno-associative-math;-fno-reciprocal-math" - SKIP_PRECOMPILE_HEADERS TRUE + PROPERTIES COMPILE_OPTIONS "${AL_STRICT_FP_OPTIONS}" SKIP_PRECOMPILE_HEADERS TRUE ) endif() @@ -172,10 +175,10 @@ if(AL_BUILD_TESTS) # intrinsics, and the file passes there as it is. Accuracy against a double # reference, and the library's own entry built as the viewer has it, are # checked to a tolerance. - if(NOT COMPILER_IS_MSVC AND NOT COMPILER_IS_CLANG_CL) + if(NOT COMPILER_IS_MSVC) set_source_files_properties( tests/alsimdkernels_test.cpp - PROPERTIES COMPILE_OPTIONS "-ffp-contract=off;-fno-associative-math;-fno-reciprocal-math" + PROPERTIES COMPILE_OPTIONS "${AL_STRICT_FP_OPTIONS}" ) endif() diff --git a/indra/llmath/almeshsimplifier.cpp b/indra/llmath/almeshsimplifier.cpp index a540fcd03d..8a6b5451a3 100644 --- a/indra/llmath/almeshsimplifier.cpp +++ b/indra/llmath/almeshsimplifier.cpp @@ -37,8 +37,33 @@ #include #include +// base's faces as meshoptimizer takes them, their vertices and indices +// numbered face after face. +struct ALMeshSimplifier::Mesh +{ + std::vector first; // each face's first vertex, then the vertex count + std::vector first_index; // each face's first index, then the index count + std::vector indices; // each face's triangles, those of its indices that name its vertices + std::vector positions; // in the authored shape + std::vector attributes; // ATTRIBUTE_COUNT a vertex + std::vector shared; // at a position another face has a vertex at too + std::vector held; // shared, and at a position on the open border of a face + std::vector uv_seam; // at a position its face has a vertex with another texture coordinate at + F32 extent = 0.f; // the largest extent of the positions + + size_t vertexCount() const { return first.back(); } + size_t indexCount() const { return first_index.back(); } + + S32 faceOf(U32 vertex) const + { + return (S32)(std::upper_bound(first.begin(), first.end(), vertex) - first.begin()) - 1; + } +}; + namespace { + using Mesh = ALMeshSimplifier::Mesh; + // What the simplifier weighs at a vertex besides its position: its // normal, as a direction in the authored shape, and its texture // coordinate over its face's density. @@ -58,28 +83,6 @@ namespace constexpr F32 ATTRIBUTE_WEIGHTS[ATTRIBUTE_COUNT] = { NORMAL_WEIGHT, NORMAL_WEIGHT, NORMAL_WEIGHT, UV_WEIGHT, UV_WEIGHT }; - // base's faces as meshoptimizer takes them, their vertices and indices - // numbered face after face. - struct Mesh - { - std::vector first; // each face's first vertex, then the vertex count - std::vector first_index; // each face's first index, then the index count - std::vector indices; // each face's triangles, those of its indices that name its vertices - std::vector positions; // in the authored shape - std::vector attributes; // ATTRIBUTE_COUNT a vertex - std::vector shared; // at a position another face has a vertex at too - std::vector uv_seam; // at a position its face has a vertex with another texture coordinate at - F32 extent = 0.f; // the largest extent of the positions - - size_t vertexCount() const { return first.back(); } - size_t indexCount() const { return first_index.back(); } - - S32 faceOf(U32 vertex) const - { - return (S32)(std::upper_bound(first.begin(), first.end(), vertex) - first.begin()) - 1; - } - }; - LLVector4a unscale(const LLVector3& normalized_scale) { // A loader keeps a size of nothing as 1, and so does this. @@ -156,6 +159,11 @@ namespace if (face.mTexCoords && vertices) { + // A coordinate that is not finite counts for nothing, and is + // weighed as none: in the simplifier's error it would make + // every collapse near it free. + auto finite = [](const LLVector2& uv) { return std::isfinite(uv.mV[0]) && std::isfinite(uv.mV[1]); }; + F64 area = 0.0; size_t triangles = 0; for (U32 i = mesh.first_index[f]; i < mesh.first_index[f + 1]; i += 3) @@ -163,25 +171,35 @@ namespace const LLVector2& a = face.mTexCoords[mesh.indices[i] - first]; const LLVector2& b = face.mTexCoords[mesh.indices[i + 1] - first]; const LLVector2& c = face.mTexCoords[mesh.indices[i + 2] - first]; - area += fabs(0.5 * ((F64)(b.mV[0] - a.mV[0]) * (c.mV[1] - a.mV[1]) - (F64)(c.mV[0] - a.mV[0]) * (b.mV[1] - a.mV[1]))); - ++triangles; + if (finite(a) && finite(b) && finite(c)) + { + area += fabs(0.5 * ((F64)(b.mV[0] - a.mV[0]) * (c.mV[1] - a.mV[1]) - + (F64)(c.mV[0] - a.mV[0]) * (b.mV[1] - a.mV[1]))); + ++triangles; + } } const F64 mean = triangles ? area / (F64)triangles : 0.0; const F32 density = mean > 0.0 && std::isfinite(mean) ? (F32)(1.0 / sqrt(mean)) : 0.f; // Measured from the face's least coordinates, so that their // size stays near the density's whatever the texture's offset. - LLVector2 least = face.mTexCoords[0]; - for (U32 v = 1; v < vertices; ++v) + LLVector2 least(F32_MAX, F32_MAX); + for (U32 v = 0; v < vertices; ++v) { - least.mV[0] = llmin(least.mV[0], face.mTexCoords[v].mV[0]); - least.mV[1] = llmin(least.mV[1], face.mTexCoords[v].mV[1]); + if (finite(face.mTexCoords[v])) + { + least.mV[0] = llmin(least.mV[0], face.mTexCoords[v].mV[0]); + least.mV[1] = llmin(least.mV[1], face.mTexCoords[v].mV[1]); + } } for (U32 v = 0; v < vertices; ++v) { - F32* attribute = &mesh.attributes[(first + v) * ATTRIBUTE_COUNT]; - attribute[3] = (face.mTexCoords[v].mV[0] - least.mV[0]) * density; - attribute[4] = (face.mTexCoords[v].mV[1] - least.mV[1]) * density; + if (density > 0.f && finite(face.mTexCoords[v])) + { + F32* attribute = &mesh.attributes[(first + v) * ATTRIBUTE_COUNT]; + attribute[3] = (face.mTexCoords[v].mV[0] - least.mV[0]) * density; + attribute[4] = (face.mTexCoords[v].mV[1] - least.mV[1]) * density; + } } } } @@ -218,11 +236,52 @@ namespace } } } + // Each position on the open border of a face, where an edge of its + // runs that no other of its triangles has, its vertices taken at + // their positions. Where faces meet, one's border lies along the + // other; faces that overlap, as a garment's inside is a copy of its + // outside, share positions across their insides too. + std::vector border(vertex_count, 0); + std::vector edges; + for (S32 f = 0; f < faces; ++f) + { + edges.clear(); + for (U32 i = mesh.first_index[f]; i < mesh.first_index[f + 1]; i += 3) + { + for (U32 k = 0; k < 3; ++k) + { + const U32 a = remap[mesh.indices[i + k]]; + const U32 b = remap[mesh.indices[i + (k + 1) % 3]]; + if (a != b) + { + edges.push_back(((U64)llmin(a, b) << 32) | llmax(a, b)); + } + } + } + std::sort(edges.begin(), edges.end()); + for (size_t e = 0; e < edges.size();) + { + size_t run = e + 1; + while (run < edges.size() && edges[run] == edges[e]) + { + ++run; + } + if (run - e == 1) + { + border[(U32)(edges[e] >> 32)] = 1; + border[(U32)edges[e]] = 1; + } + e = run; + } + } + mesh.shared.resize(vertex_count); + mesh.held.resize(vertex_count); mesh.uv_seam.resize(vertex_count); for (U32 v = 0; v < vertex_count; ++v) { mesh.shared[v] = spans[remap[v]]; + mesh.held[v] = spans[remap[v]] && border[remap[v]]; mesh.uv_seam[v] = seam[remap[v]]; } @@ -279,13 +338,16 @@ namespace }; for (size_t i = 0; i < index_count; i += 3) { - const U32 a = root(remap[indices[i]]); + // a stays the root of the corners joined so far. + U32 a = root(remap[indices[i]]); for (S32 k = 1; k < 3; ++k) { const U32 b = root(remap[indices[i + k]]); if (a != b) { - parent[llmax(a, b)] = llmin(a, b); + const U32 joined = llmin(a, b); + parent[llmax(a, b)] = joined; + a = joined; } } } @@ -337,8 +399,9 @@ namespace // indices numbered from first. Where prune is set, it may drop detached // parts as its error grows past their size, none larger than // PRUNE_LIMIT; where it would have dropped a larger one to reach its - // target, it drops each part within PRUNE_LIMIT first and simplifies - // what is left without dropping any more. + // target, or every part, it drops each part within PRUNE_LIMIT first + // and simplifies what is left without dropping any more. An error + // within PRUNE_LIMIT drops no part larger than it. size_t simplifyMesh(const Mesh& mesh, U32 first, U32 count, const U32* indices, size_t index_count, size_t target_count, F32 error, U32 options, const std::vector& locks, bool prune, U32* destination, F32& result_error) { @@ -359,8 +422,10 @@ namespace } const size_t simplified = simplify(indices, index_count, options | meshopt_SimplifyPrune); - const F32 extent = meshopt_simplifyScale(positions, count, sizeof(LLVector4a)); - if (largestDropped(vertices, count, indices, index_count, destination, simplified, extent) <= PRUNE_LIMIT) + if (simplified >= 3 && + (error <= PRUNE_LIMIT || + largestDropped(vertices, count, indices, index_count, destination, simplified, + meshopt_simplifyScale(positions, count, sizeof(LLVector4a))) <= PRUNE_LIMIT)) { return simplified; } @@ -521,11 +586,20 @@ bool ALMeshSimplifier::compact(const LLVolume& base, S32 face, const U32* indice return true; } -// static -F32 ALMeshSimplifier::simplifyModel(const LLVolume& base, LLVolume& target, const Target& goal, Seams seams) +ALMeshSimplifier::ALMeshSimplifier(const LLVolume& base) + : mBase(base) { - Mesh mesh; - prepare(base, mesh); + auto mesh = std::make_unique(); + prepare(base, *mesh); + mMesh = std::move(mesh); +} + +ALMeshSimplifier::~ALMeshSimplifier() = default; + +F32 ALMeshSimplifier::simplifyModel(LLVolume& target, const Target& goal, Seams seams) const +{ + const LLVolume& base = mBase; + const Mesh& mesh = *mMesh; const size_t index_count = mesh.indices.size(); if (index_count < 3) { @@ -581,7 +655,7 @@ F32 ALMeshSimplifier::simplifyModel(const LLVolume& base, LLVolume& target, cons S32 kept = 0; for (S32 g = 0; g < faces; ++g) { - if (simplifyFace(base, g, target.getVolumeFace(g), goal, seams) > 0.f) + if (simplifyFace(g, target.getVolumeFace(g), goal, seams) > 0.f) { face_count += target.getVolumeFace(g).mNumIndices; ++kept; @@ -600,11 +674,10 @@ F32 ALMeshSimplifier::simplifyModel(const LLVolume& base, LLVolume& target, cons return (F32)index_count / (F32)count; } -// static -F32 ALMeshSimplifier::simplifyFace(const LLVolume& base, S32 face, LLVolumeFace& target, const Target& goal, Seams seams) +F32 ALMeshSimplifier::simplifyFace(S32 face, LLVolumeFace& target, const Target& goal, Seams seams) const { - Mesh mesh; - prepare(base, mesh); + const LLVolume& base = mBase; + const Mesh& mesh = *mMesh; const U32 first = mesh.first[face]; const U32 vertex_count = mesh.first[face + 1] - first; const size_t index_count = mesh.first_index[face + 1] - mesh.first_index[face]; @@ -638,22 +711,31 @@ F32 ALMeshSimplifier::simplifyFace(const LLVolume& base, S32 face, LLVolumeFace& } else { - // A face that meets another keeps its border, which the other's - // simplification keeps as well, and all of its parts, as a part of - // one face may be joined to another face's. + // A face that meets another keeps its vertices where it meets it, + // which the other's simplification keeps as well, and all of its + // parts, as a part of one face may be joined to another face's. A + // face meets another along a border, one's or the other's; its + // borders that meet no face are free, and so is its inside where + // another face lies over it. const bool meets = std::any_of(mesh.shared.begin() + first, mesh.shared.begin() + first + vertex_count, [](U8 shared) { return shared != 0; }); - U32 options = seamOptions(seams, goal); - if (meets) - { - options |= meshopt_SimplifyLockBorder; - } std::vector locks; if (seams != Seams::ALL) { protect(mesh, seams, first, vertex_count, locks); } - count = simplifyMesh(mesh, first, vertex_count, indices.data(), index_count, target_count, error, options, locks, + if (meets) + { + locks.resize(vertex_count, 0); + for (U32 v = 0; v < vertex_count; ++v) + { + if (mesh.held[first + v]) + { + locks[v] |= (U8)meshopt_SimplifyVertex_Lock; + } + } + } + count = simplifyMesh(mesh, first, vertex_count, indices.data(), index_count, target_count, error, seamOptions(seams, goal), locks, goal.distant && !meets, simplified.data(), result_error); } if (count < 3) diff --git a/indra/llmath/almeshsimplifier.h b/indra/llmath/almeshsimplifier.h index 7928473f57..59fe36181b 100644 --- a/indra/llmath/almeshsimplifier.h +++ b/indra/llmath/almeshsimplifier.h @@ -28,6 +28,8 @@ #include "stdtypes.h" +#include + class LLVolume; class LLVolumeFace; @@ -39,7 +41,8 @@ class LLVolumeFace; // coordinates drift as well as the positions. A simplified face takes its // vertices from the base face unchanged, each attribute as it was: its // position too, so whatever is keyed by position, as a model's skin weights -// are, still finds it. +// are, still finds it. A simplifier takes its model's faces as it weighs +// them once, for every simplification it makes of the model. class ALMeshSimplifier { public: @@ -73,6 +76,13 @@ class ALMeshSimplifier bool distant = false; }; + // base's faces as the simplifier takes them. + struct Mesh; + + // A simplifier of base, which must outlive it unchanged. + explicit ALMeshSimplifier(const LLVolume& base); + ~ALMeshSimplifier(); + // Simplifies base's faces together, as one mesh, into target's, which // has as many faces. A triangle belongs to the face its first vertex // came from. A part detached from the rest is dropped only with all of @@ -81,14 +91,17 @@ class ALMeshSimplifier // they were. A face left with no triangle is one of three indices to a // vertex at the origin. Where a face would take more vertices than 16-bit // indices reach, each face is simplified on its own instead. - static F32 simplifyModel(const LLVolume& base, LLVolume& target, const Target& goal, Seams seams); + F32 simplifyModel(LLVolume& target, const Target& goal, Seams seams) const; // Simplifies base's face on its own into target. A face that shares a - // position with any other face of base keeps its border where it is, so - // that no gap opens between the two, and keeps all of its parts; only - // one that shares none may lose its small detached parts. Returns as simplifyModel does; where no triangle - // is left, target is the face of three indices to a vertex at the origin. - static F32 simplifyFace(const LLVolume& base, S32 face, LLVolumeFace& target, const Target& goal, Seams seams); + // position with any other face of base keeps its vertices where they + // are at those of the positions that lie on either face's open border, + // so that no gap opens between the two, and keeps all of its parts; + // only one that shares none may lose its small detached parts. Where two + // faces overlap, each is free inside its borders. Returns as + // simplifyModel does; where no triangle is left, target is the face of + // three indices to a vertex at the origin. + F32 simplifyFace(S32 face, LLVolumeFace& target, const Target& goal, Seams seams) const; // The face of base's face's layout that the given triangles make: the // vertices they name and no others, each with every attribute it has in @@ -97,6 +110,10 @@ class ALMeshSimplifier // from any face. Returns false, leaving target as it was, where the // triangles name more vertices than 16-bit indices reach. static bool compact(const LLVolume& base, S32 face, const U32* indices, size_t index_count, LLVolumeFace& target); + +private: + const LLVolume& mBase; + std::unique_ptr mMesh; }; #endif // AL_MESHSIMPLIFIER_H diff --git a/indra/llmath/alsimdkernels.h b/indra/llmath/alsimdkernels.h index fd60b5ddda..2a80e53436 100644 --- a/indra/llmath/alsimdkernels.h +++ b/indra/llmath/alsimdkernels.h @@ -226,9 +226,9 @@ void indexed_tangents(const LLVector4a* positions, const LLVector4a* normals, co bool grid_tangents_central(const LLVector4a* positions, const LLVector4a* normals, size_t columns, size_t rows, bool joins, F32 same, F32 sign, F32 w, LLVector4a* tangents); -// Every vector at unit length in x, y and z, w scaled with them, where its -// squared length in x, y and z is above FLT_MIN; elsewhere (0, 0, 0, 1). -// Returns whether any was set so. +// Every vector at unit length in x, y and z, with a w of 0, where its +// squared length in x, y and z is above FLT_MIN; elsewhere (0, 0, 0, 1), +// whatever w it had. Returns whether any was set so. bool normalize3_or_mark(LLVector4a* v, size_t n); } // namespace alsimd diff --git a/indra/llmath/alsimdkernels.inl b/indra/llmath/alsimdkernels.inl index c483644941..dd54d2d380 100644 --- a/indra/llmath/alsimdkernels.inl +++ b/indra/llmath/alsimdkernels.inl @@ -392,6 +392,13 @@ using ALSimdLanes = ALSimd8; using ALSimdLanes = ALSimd4; #endif +// Each file that includes this one makes the kernels it uses for itself, +// under its own floating-point options and instruction set: with external +// linkage the linker would keep one copy of each, and the strict kernel +// test's copies would run in place of the library's, or one tier's in a +// binary built for another. +namespace +{ namespace kernels { @@ -1295,8 +1302,11 @@ struct TangentLanes // left, top right, top left, and its corners' shares go to its four // vertices' sums. A lane holds a quad where it is before the last line // and not at the end of its own: where its place, its lane less whole -// lines, is short of the last. winding is -1 where the grid is walked -// down its columns. +// lines, is short of the last. The whole lines are counted from the +// middle of the lane, half a lane clear of a line's ends, so that a +// division the compiler makes a multiply by a rounded reciprocal still +// counts a line's first lane in its own line. winding is -1 where the +// grid is walked down its columns. template AL_SIMD_INLINE void grid_quads(const TangentLanes& lanes, size_t q, size_t across, size_t quads, F32 winding, typename K::reg& total) { @@ -1307,7 +1317,8 @@ AL_SIMD_INLINE void grid_quads(const TangentLanes& lanes, size_t q, size_t acros const size_t tl_at = q + across; const size_t tr_at = q + across + 1; const reg lane = K::add(K::ramp(), K::set1(F32(q))); - const reg lane_place = K::fnmadd(K::floor(K::div(lane, across_f)), across_f, lane); + const reg lines = K::floor(K::div(K::add(lane, K::set1(0.5f)), across_f)); + const reg lane_place = K::fnmadd(lines, across_f, lane); const reg real = K::select(K::and_(K::cmplt(lane_place, K::set1(F32(across - 1))), K::cmplt(lane, K::set1(F32(quads)))), K::set1(1.f), K::zero()); const Lanes3 bl = load_lanes3(lanes.position, bl_at); @@ -1641,7 +1652,7 @@ bool normalize3_or_mark_impl(LLVector4a* v, size_t n) const reg scale = W::rsqrt(W::max(length2, W::set1(FLT_MIN))); a = { W::select(none, W::zero(), W::mul(a.x, scale)), W::select(none, W::zero(), W::mul(a.y, scale)), W::select(none, W::zero(), W::mul(a.z, scale)) }; - w = W::select(none, W::set1(1.f), W::mul(w, scale)); + w = W::select(none, W::set1(1.f), W::zero()); const mask real = W::cmplt(W::add(W::ramp(), W::set1(F32(first))), W::set1(F32(n))); marked = W::or_(marked, W::and_(real, none)); W::store_columns(a.x, a.y, a.z, w, [&](int i) { return first + i < n ? v[first + i].getF32ptr() : spill; }); @@ -1943,6 +1954,7 @@ void planar_tangents_impl(const LLVector4a* normals, size_t n, const PlanarArgs& #endif } // namespace kernels +} // namespace } // namespace alsimd #endif // AL_SIMDKERNELS_INL diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index e2db948e76..cee1206c8c 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -1293,7 +1293,7 @@ S32 LLPath::getNumNGonPoints(const LLPathParams& params, S32 sides, F32 startOff return ret; } -void LLPath::genNGon(const LLPathParams& params, S32 sides, F32 startOff, F32 end_scale, F32 twist_scale) +void LLPath::genNGon(const LLPathParams& params, S32 sides, F32 startOff, F32 end_scale, F32 twist_scale, bool is_sculpted) { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; @@ -1398,11 +1398,24 @@ void LLPath::genNGon(const LLPathParams& params, S32 sides, F32 startOff, F32 en // affect most sample points. t = ((S32)(t * sides)) / (F32)sides; + // A step the snap brings within a hundredth of a step of the begin cut, + // as it does a cut just short of a step, or the first step where the + // rounding of the step's size takes it back to the cut, is the begin's + // own: it would leave a row with no height after the begin's. A sculpt + // keeps it, as it keeps the end's, and so does a circle2 path, a spiral + // whose stations alternate sides: leaving one out would turn every + // station after it the other way. + if (!is_sculpted && (params.getCurveType() & 0xf0) == LL_PCODE_PATH_CIRCLE && t - params.getBegin() < step * 0.01f) + { + t += step; + } + // Run through the non-cut dependent points. A point within a hundredth // of a step of the end cut is the end's own, short of it only by the // rounding the steps add up to: it would leave a row with no height - // before the end's. - const F32 end_before = params.getEnd() - step * 0.01f; + // before the end's. A sculpt keeps it: its rows are the rows of its map + // it samples, one a station, as every viewer samples them. + const F32 end_before = is_sculpted ? params.getEnd() : params.getEnd() - step * 0.01f; while (t < end_before) { pt = mPath.append(1); @@ -1604,7 +1617,7 @@ bool LLPath::generate(const LLPathParams& params, F32 detail, S32 split, sides = llmax(sculpt_size, 1); if (0 < sides) - genNGon(params, sides); + genNGon(params, sides, 0.f, 1.f, 1.f, is_sculpted); } break; @@ -2739,7 +2752,8 @@ bool LLVolume::decodeVolumeFaces(const LLSD& mdl) { LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("unpackVolumeFaces - normals"); - if (!norm.empty()) + // An array short of the positions' count is taken as none. + if (norm.size() >= (size_t)num_verts * 3 * sizeof(U16)) { // from sixteen bits to -1..1 alsimd::dequantize_u16x3(norm.data(), num_verts, LLVector4a(2.f / 65535.f), LLVector4a(-1.f), face.mNormals); @@ -2786,7 +2800,7 @@ bool LLVolume::decodeVolumeFaces(const LLSD& mdl) { LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("unpackVolumeFaces - texcoords"); - if (!tc.empty()) + if (tc.size() >= (size_t)num_verts * 2 * sizeof(U16)) { LLVector4a tc_scale; tc_scale.setMul(tc_range, u16_step); @@ -2873,6 +2887,14 @@ bool LLVolume::decodeVolumeFaces(const LLSD& mdl) LL_WARNS() << "Vertex weight count does not match vertex count!" << LL_ENDL; } + // A vertex the weights run out before takes the weight of a + // vertex with no influence, rather than what the allocation + // held. + for (; cur_vertex < num_verts; ++cur_vertex) + { + face.mWeights[cur_vertex].set(0.999f, 0.f, 0.f, 0.f); + } + } // modifier flags? @@ -3264,6 +3286,7 @@ void LLVolume::sculptGenerateEmptyPlaceholder() p[0] = 0; p[1] = 0; p[2] = 0; + p[3] = 0; llassert(pt.isFinite3()); } @@ -3299,6 +3322,8 @@ void LLVolume::sculptGenerateSpherePlaceholder() p[0] = (F32)(sin(F_PI * v) * cos(2.0 * F_PI * u) * RADIUS); p[1] = (F32)(-sin(F_PI * v) * sin(2.0 * F_PI * u) * RADIUS); p[2] = (F32)(cos(F_PI * v) * RADIUS); + // mMesh's resize leaves what it adds as it found it. + p[3] = 0; llassert(pt.isFinite3()); } @@ -3534,6 +3559,7 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, } mSculptLevel = sculpt_level; + mSculptSpherePlaceholder = data_is_empty && visible_placeholder; // Delete any existing faces so that they get regenerated mVolumeFaces.clear(); @@ -5694,8 +5720,13 @@ bool LLVolumeFace::cacheOptimize(bool gen_tangents) } catch (std::bad_alloc&) { - LLError::LLUserWarningMsg::showOutOfMemory(); - LL_ERRS("LLVolume") << "Bad memory allocation in cacheOptimize, " << vertex_count << " vertices, " << index_count << " indices" << LL_ENDL; + // Only the scratch and meshoptimizer's allocations throw, all made + // before the face's buffers are replaced, so the face keeps its + // buffers. This runs on the mesh threads, where a fatal error may + // not be raised. + LL_WARNS("LLVolume") << "Bad memory allocation in cacheOptimize, " << vertex_count << " vertices, " << index_count << " indices" + << LL_ENDL; + return false; } return true; @@ -5975,8 +6006,40 @@ const LLVolumeOctree* LLVolumeFace::getOctree() const LLVector4a LLVolumeFace::makePlanarNormal() const { LLVector4a planar(0.f, 0.f, 0.f, -1.f); + bool keep = true; if (mPositions && mNormals && mNumVertices > 0) { + // A triangle two of whose corners are one place has no area, which + // a fused multiply-add would leave as the rounding of its cross + // product. + auto same_place = [](const LLVector4a& p, const LLVector4a& q) + { + return std::memcmp(p.getF32ptr(), q.getF32ptr(), 3 * sizeof(F32)) == 0; + }; + LLVector4a sum; + sum.clear(); + for (S32 i = 0; i + 2 < mNumIndices; i += 3) + { + const LLVector4a& a = mPositions[mIndices[i]]; + const LLVector4a& b = mPositions[mIndices[i + 1]]; + const LLVector4a& c = mPositions[mIndices[i + 2]]; + if (same_place(a, b) || same_place(b, c) || same_place(a, c)) + { + continue; + } + LLVector4a ab, ac, area; + ab.setSub(b, a); + ac.setSub(c, a); + area.setCross3(ab, ac); + sum.add(area); + } + const F32 length2 = sum.dot3(sum).getF32(); + + // A face with no area keeps nothing, and is made again at the next + // call: a flexible prim's sides have none before its first pose, and + // the one normal they have then is no direction of theirs. + keep = length2 > 0.f && std::isfinite(length2); + auto same_normal = [&](S32 v) { return std::memcmp(mNormals[v].getF32ptr(), mNormals[0].getF32ptr(), 3 * sizeof(F32)) == 0; }; bool one_normal = true; for (S32 v = 1; v < mNumVertices && one_normal; ++v) @@ -5989,38 +6052,25 @@ LLVector4a LLVolumeFace::makePlanarNormal() const planar = mNormals[0]; planar.getF32ptr()[3] = 1.f; } - else + else if (keep) { - LLVector4a sum; - sum.clear(); - for (S32 i = 0; i + 2 < mNumIndices; i += 3) + sum.mul(1.f / sqrtf(length2)); + constexpr F32 cone = PLANAR_CONE_COS + PLANAR_CONE_TIE; + bool within = true; + for (S32 v = 0; v < mNumVertices && within; ++v) { - const LLVector4a& a = mPositions[mIndices[i]]; - LLVector4a ab, ac, area; - ab.setSub(mPositions[mIndices[i + 1]], a); - ac.setSub(mPositions[mIndices[i + 2]], a); - area.setCross3(ab, ac); - sum.add(area); - } - - const F32 length2 = sum.dot3(sum).getF32(); - if (length2 > 0.f && std::isfinite(length2)) - { - sum.mul(1.f / sqrtf(length2)); - constexpr F32 cone = PLANAR_CONE_COS + PLANAR_CONE_TIE; - bool within = true; - for (S32 v = 0; v < mNumVertices && within; ++v) - { - const F32 along = mNormals[v].dot3(sum).getF32(); - within = along > 0.f && along * along >= cone * cone * mNormals[v].dot3(mNormals[v]).getF32(); - } - planar = sum; - planar.getF32ptr()[3] = within ? 1.f : -1.f; + const F32 along = mNormals[v].dot3(sum).getF32(); + within = along > 0.f && along * along >= cone * cone * mNormals[v].dot3(mNormals[v]).getF32(); } + planar = sum; + planar.getF32ptr()[3] = within ? 1.f : -1.f; } } - std::memcpy(mPlanarNormal, planar.getF32ptr(), sizeof(mPlanarNormal)); + if (keep) + { + std::memcpy(mPlanarNormal, planar.getF32ptr(), sizeof(mPlanarNormal)); + } return planar; } @@ -6214,10 +6264,19 @@ namespace // two of a triangle's corners in one place, an edge there has no length // and the product is exactly zero, fused multiply-add or not. A quad // between a copied row or column adds nothing, so the edge there stays - // hard. + // hard. A quad whose diagonal has no length, as a sculpt can fold one, + // has no area on either side of it and adds nothing either: the edges + // at its corners off the diagonal are each other's reverse, whose + // product a fused multiply-add leaves as the rounding of one of its + // terms. inline void add_quad_to_normals(LLVector4a* norm, S32 bl, S32 br, S32 tl, S32 tr, const LLVector4a& bottom, const LLVector4a& right, - const LLVector4a& top, const LLVector4a& left) + const LLVector4a& top, const LLVector4a& left, const LLVector4a& diagonal) { + if (diagonal.equals3(LLVector4a::getZero(), SAME_PLACE)) + { + return; + } + LLVector4a lower, upper; lower.setCross3(bottom, right); upper.setCross3(top, left); @@ -6237,10 +6296,11 @@ namespace // the middle of a sculpt's pinch, has no normal of its own. It takes the // nearest one up or down its column; where its whole column has none, // the nearest along its row; where its row has none either, as where a - // copied column crosses a copied row, the nearest around it. It takes - // only a normal a vertex has of its own, so that none is passed on twice. - // A grid with no normal at all faces +Z. Such a vertex is marked with a - // w of 1 until it has one; a normal of its own has a w of 0. + // copied column crosses a copied row, one of the nearest around it, in + // rings of the grid. It takes only a normal a vertex has of its own, so + // that none is passed on twice. A grid with no normal at all faces +Z. + // Such a vertex is marked with a w of 1 until it has one, and of 2 while + // it waits for the search around; a normal of its own has a w of 0. void normalize_grid_normals(LLVector4a* norm, S32 num_s, S32 num_t) { const S32 count = num_s * num_t; @@ -6259,6 +6319,7 @@ namespace return; } + bool around = false; for (S32 t = 0; t < num_t; ++t) { for (S32 s = 0; s < num_s; ++s) @@ -6291,38 +6352,64 @@ namespace } } } - // Each square looks only at its ring, since every point inside - // it was looked at by the square before. - for (S32 d = 1; d < llmax(num_s, num_t) && !donor; ++d) + if (donor) { - for (S32 row = llmax(t - d, 0); row <= llmin(t + d, num_t - 1) && !donor; ++row) + n = *donor; + n.getF32ptr()[3] = 1.f; + } + else + { + n.getF32ptr()[3] = 2.f; + around = true; + } + } + } + + if (around) + { + // Every vertex's nearest normal of its own in rings, found + // outward from all of them at once a ring at a time: a step to + // any of the eight vertices around one is one ring further. + ALScratchArena::Scope scratch; + U32* source = scratch.allocate(count); + U32* queue = scratch.allocate(count); + size_t head = 0; + size_t tail = 0; + for (S32 v = 0; v < count; ++v) + { + source[v] = U32_MAX; + if (own(norm[v])) + { + source[v] = (U32)v; + queue[tail++] = (U32)v; + } + } + while (head < tail) + { + const U32 v = queue[head++]; + const S32 s = (S32)v % num_s; + const S32 t = (S32)v / num_s; + for (S32 row = llmax(t - 1, 0); row <= llmin(t + 1, num_t - 1); ++row) + { + for (S32 column = llmax(s - 1, 0); column <= llmin(s + 1, num_s - 1); ++column) { - if (row == t - d || row == t + d) + const S32 u = column + num_s * row; + if (source[u] == U32_MAX) { - for (S32 column = llmax(s - d, 0); column <= llmin(s + d, num_s - 1); ++column) - { - if (own(norm[column + num_s * row])) - { - donor = &norm[column + num_s * row]; - break; - } - } - } - else - { - for (S32 column : { s - d, s + d }) - { - if (column >= 0 && column < num_s && own(norm[column + num_s * row])) - { - donor = &norm[column + num_s * row]; - break; - } - } + source[u] = source[v]; + queue[tail++] = (U32)u; } } } - n = donor ? *donor : LLVector4a(0.f, 0.f, 1.f); - n.getF32ptr()[3] = 1.f; + } + + for (S32 v = 0; v < count; ++v) + { + if (norm[v][3] == 2.f) + { + norm[v] = norm[source[v]]; + norm[v].getF32ptr()[3] = 1.f; + } } } @@ -7828,6 +7915,10 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) bool sculpt_invert = sculpt_type & LL_SCULPT_FLAG_INVERT; bool sculpt_mirror = sculpt_type & LL_SCULPT_FLAG_MIRROR; bool sculpt_reverse_horizontal = (sculpt_invert ? !sculpt_mirror : sculpt_mirror); // XOR + if (volume->isSculptSpherePlaceholder()) + { + sculpt_stitching = LL_SCULPT_TYPE_SPHERE; + } S32 num_vertices, num_indices; @@ -8051,13 +8142,15 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) end_index = doubled ? grid_indices(mIndices, limit, mNumS, mNumT, lefts, quads, kept) : grid_indices(mIndices, limit, mNumS, mNumT, lefts, quads, kept); } - else if (sculpt_stitching != LL_SCULPT_TYPE_NONE && !volume->isUnique() && quads > 0 && rows > 0) + else if (sculpt_stitching != LL_SCULPT_TYPE_NONE && !volume->isUnique() && !doubled && quads > 0 && rows > 0) { // A sculpt's triangles two of whose corners are one point have // no area, and are left out: the points' codes find them and the // points confirm them. A flexible sculpt's points move after its // full build, and it keeps them. A face keeps at least one - // triangle. + // triangle. The flat inner side of a sculpt whose parameters + // carry a square or triangle hole has its corners doubled, and + // keeps all of its quads. ALScratchArena::Scope scratch; U32* codes = scratch.allocate(num_vertices + 8); alsimd::unit_cube_codes(pos, num_vertices, codes); @@ -8109,14 +8202,15 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) { const S32 bl = row + s; const S32 tl = bl + mNumS; - LLVector4a bottom, right, top; + LLVector4a bottom, right, top, diagonal; bottom.setSub(pos[bl + 1], pos[bl]); right.setSub(pos[tl + 1], pos[bl + 1]); top.setSub(pos[tl + 1], pos[tl]); + diagonal.setSub(pos[tl + 1], pos[bl]); drop_rounding(bottom); drop_rounding(right); drop_rounding(top); - add_quad_to_normals(norm, bl, bl + 1, tl, tl + 1, bottom, right, top, left); + add_quad_to_normals(norm, bl, bl + 1, tl, tl + 1, bottom, right, top, left, diagonal); left = right; } } @@ -8125,13 +8219,15 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) // one seam, and across its first and last rows: a prim's where they are // one place in every row or column, as its own positions say; a // sculpt's where its stitching samples them from one column or row of - // the map. A flat inner side's seam is one of its corners. + // the map. A flat inner side's seam is one of its corners. A flexible + // prim's path is open, and its first build, which its partial builds + // keep the joins of, lays every row at one place before it has a pose. if (!partial_build) { if (sculpt_stitching == LL_SCULPT_TYPE_NONE) { mJoinsS = !doubled && columns_meet(pos, mNumS, mNumT, 0, mNumS - 1); - mJoinsT = rows_meet(pos, mNumS, 0, mNumT - 1); + mJoinsT = !mFlexible && rows_meet(pos, mNumS, 0, mNumT - 1); } else { @@ -8198,26 +8294,28 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) } // On a sphere's side, a column that is one place in every row is a pole - // on the axis the half circle sweeps about, and takes the axis. A cut - // end of a hollow sphere lies on the axis, and has no area. A flat inner - // side is a cone at a pole, and its ends keep the normals of the strips - // they end. + // on the axis the half circle sweeps about, and takes the axis: away + // from the sphere's centre on an outer side, toward it on an inner one. + // Which end of the axis a column's pole is at follows the twist, which + // turns the half circle about its centre. A cut end of a hollow sphere + // lies on the axis, and has no area. A flat inner side is a cone at a + // pole, and its ends keep the normals of the strips they end. if (sculpt_stitching == LL_SCULPT_TYPE_NONE && !(mTypeMask & END_MASK) && !doubled && volume->getPathType() == LL_PCODE_PATH_CIRCLE && ((volume->getProfileType() & LL_PCODE_PROFILE_MASK) == LL_PCODE_PROFILE_CIRCLE_HALF)) { - if (column_is_point(pos, mNumS, mNumT, 0)) - { //all lower S have same normal - for (S32 i = 0; i < mNumT; i++) - { - norm[mNumS*i].set(1,0,0); - } - } - - if (column_is_point(pos, mNumS, mNumT, mNumS - 1)) - { //all upper S have same normal - for (S32 i = 0; i < mNumT; i++) + const bool outer = !(mTypeMask & INNER_MASK); + for (S32 column : { 0, mNumS - 1 }) + { + if (column_is_point(pos, mNumS, mNumT, column)) { - norm[mNumS*i+mNumS-1].set(-1,0,0); + // A pole at the centre, of a sphere with no size, takes the + // side it has untwisted. + const F32 at = pos[column][0]; + const F32 x = at == 0.f ? (column == 0 ? 1.f : -1.f) : ((at > 0.f) == outer ? 1.f : -1.f); + for (S32 i = 0; i < mNumT; i++) + { + norm[mNumS * i + column].set(x, 0.f, 0.f); + } } } } diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 0454a539e0..87aa12de9b 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -821,7 +821,8 @@ class LLPath static S32 getNumPoints(const LLPathParams& params, F32 detail); static S32 getNumNGonPoints(const LLPathParams& params, S32 sides, F32 offset=0.0f, F32 end_scale = 1.f, F32 twist_scale = 1.f); - void genNGon(const LLPathParams& params, S32 sides, F32 offset=0.0f, F32 end_scale = 1.f, F32 twist_scale = 1.f); + void genNGon(const LLPathParams& params, S32 sides, F32 offset=0.0f, F32 end_scale = 1.f, F32 twist_scale = 1.f, + bool is_sculpted = false); virtual bool generate(const LLPathParams& params, F32 detail=1.0f, S32 split = 0, bool is_sculpted = false, S32 sculpt_size = 0); @@ -974,8 +975,10 @@ class LLVolumeFace // weighted by their areas, normalized; in w 1 where the face is // projected as one plane and -1 where it is not. Made on the first call // and kept until a build other than a flexible prim's partial one: its - // frames keep their first build's planar texture coordinates. Main - // thread only. + // frames keep their first build's planar texture coordinates. A face + // with no area keeps nothing, and its planar normal is made again at + // each call: a flexible prim's sides have none before its first pose. + // Main thread only. LLVector4a getPlanarNormal() const { LLVector4a planar; @@ -1166,6 +1169,11 @@ class LLVolume : public LLRefCount S32 getSculptLevel() const { return mSculptLevel; } void setSculptLevel(S32 level) { mSculptLevel = level; } + // Whether the sculpt shows the sphere it shows while its map is missing + // or has no area, which is laid out and stitched as a sphere sculpt is, + // whatever its own stitching. + bool isSculptSpherePlaceholder() const { return mSculptSpherePlaceholder; } + static void getLoDTriangleCounts(const LLVolumeParams& params, S32* counts); @@ -1236,6 +1244,7 @@ class LLVolume : public LLRefCount bool mLODInvariant; F32 mDetail; S32 mSculptLevel; + bool mSculptSpherePlaceholder = false; F32 mSurfaceArea; //unscaled surface area bool mIsMeshAssetLoaded; bool mIsMeshAssetUnavaliable; diff --git a/indra/llmath/tests/almeshsimplifier_test.cpp b/indra/llmath/tests/almeshsimplifier_test.cpp index 25185acde7..1594fed00e 100644 --- a/indra/llmath/tests/almeshsimplifier_test.cpp +++ b/indra/llmath/tests/almeshsimplifier_test.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -521,7 +522,7 @@ namespace { INFO(fmt::format("{}, a {}th of {} triangles, {}", name, decimator, base_triangles, nameOf(seams))); LLPointer target = targetFor(*base); - const F32 ratio = ALMeshSimplifier::simplifyModel(*base, *target, toCount(decimator), seams); + const F32 ratio = ALMeshSimplifier(*base).simplifyModel(*target, toCount(decimator), seams); REQUIRE(ratio > 0.f); REQUIRE(wellFormed(target->getVolumeFace(0))); CHECK(fromBase(target->getVolumeFace(0), *base)); @@ -551,14 +552,14 @@ namespace const F32 decimator = 9.f; LLPointer held = targetFor(*base); - const F32 held_ratio = ALMeshSimplifier::simplifyModel(*base, *held, toCount(decimator), Seams::ALL); + const F32 held_ratio = ALMeshSimplifier(*base).simplifyModel(*held, toCount(decimator), Seams::ALL); INFO(fmt::format("every seam held: a ratio of {}", held_ratio)); CHECK(held_ratio * ALLOWED_DRIFT < decimator); for (Seams seams : { Seams::UV, Seams::NONE }) { LLPointer free = targetFor(*base); - const F32 ratio = ALMeshSimplifier::simplifyModel(*base, *free, toCount(decimator), seams); + const F32 ratio = ALMeshSimplifier(*base).simplifyModel(*free, toCount(decimator), seams); INFO(fmt::format("{}: a ratio of {}", nameOf(seams), ratio)); REQUIRE(ratio > 0.f); REQUIRE(wellFormed(free->getVolumeFace(0))); @@ -582,7 +583,7 @@ namespace ALMeshSimplifier::Target goal; goal.error = error; LLPointer target = targetFor(*base); - const F32 ratio = ALMeshSimplifier::simplifyModel(*base, *target, goal, Seams::ALL); + const F32 ratio = ALMeshSimplifier(*base).simplifyModel(*target, goal, Seams::ALL); const F32 measured = deviation(*base, *target); const S32 triangles = triangleCount(*target); INFO(fmt::format("an error of {}: {} of {} triangles, a ratio of {}, {} measured", error, triangles, triangleCount(*base), @@ -594,6 +595,36 @@ namespace } } + // A texture coordinate that is not finite, as a loader may pass on, + // counts for nothing, and the rest of its face's coordinates weigh as + // they did: one would otherwise make the face's texture density not a + // number, and the face's coordinates weigh nothing at all. A flat plane + // whose coordinates bend across it is held up by them alone. + TEST_CASE("almeshsimplifier: a texture coordinate that is not finite leaves its face's others their weight", "[almeshsimplifier]") + { + ArenaInstalled installed; + Geometry model = plane(16); + for (LLVector2& uv : model.uvs) + { + uv.mV[0] += 0.2f * sinf(F_TWO_PI * 2.f * uv.mV[1]); + } + ALMeshSimplifier::Target goal; + goal.error = 0.005f; + auto simplified = [&](const Geometry& geometry) + { + LLPointer base = volumeOf({ geometry }); + LLPointer target = targetFor(*base); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*target, goal, Seams::ALL) > 0.f); + return triangleCount(*target); + }; + const S32 whole = simplified(model); + model.uvs[0] = LLVector2(std::numeric_limits::quiet_NaN(), 0.f); + const S32 with_nan = simplified(model); + INFO(fmt::format("{} triangles from finite coordinates, {} with one not", whole, with_nan)); + REQUIRE(whole > 8); + CHECK(with_nan * 2 >= whole); + } + // ------------------------------------------------------------------ // Borders between faces @@ -686,7 +717,7 @@ namespace LLPointer target = targetFor(*base); for (S32 f = 0; f < 2; ++f) { - CHECK(ALMeshSimplifier::simplifyFace(*base, f, target->getVolumeFace(f), toCount(decimator), seams) > 0.f); + CHECK(ALMeshSimplifier(*base).simplifyFace(f, target->getVolumeFace(f), toCount(decimator), seams) > 0.f); REQUIRE(wellFormed(target->getVolumeFace(f))); CHECK(fromBase(target->getVolumeFace(f), *base)); } @@ -710,7 +741,7 @@ namespace INFO(fmt::format("{} halves, a {}th, {}", flat_unmapped ? "flat shaded, unmapped" : "smooth", decimator, nameOf(seams))); LLPointer target = targetFor(*base); - REQUIRE(ALMeshSimplifier::simplifyModel(*base, *target, toCount(decimator), seams) > 0.f); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*target, toCount(decimator), seams) > 0.f); for (S32 f = 0; f < 2; ++f) { const LLVolumeFace& face = target->getVolumeFace(f); @@ -726,8 +757,9 @@ namespace } // A face that meets no other is free at its border, where one that - // meets another is held: the held square keeps the 128 edges of its - // border, more than a hundredth of its triangles can make. + // meets another is held where it meets it: the held square keeps the + // 32 edges along the side it shares, and is free along its other + // three, which it simplifies as the square on its own does. TEST_CASE("almeshsimplifier: a face is held at its border only where it meets another", "[almeshsimplifier]") { ArenaInstalled installed; @@ -735,17 +767,58 @@ namespace LLPointer alone = volumeOf({ plane(32) }); LLPointer alone_target = targetFor(*alone); - const F32 alone_ratio = ALMeshSimplifier::simplifyFace(*alone, 0, alone_target->getVolumeFace(0), toCount(decimator), Seams::ALL); + const F32 alone_ratio = ALMeshSimplifier(*alone).simplifyFace(0, alone_target->getVolumeFace(0), toCount(decimator), Seams::ALL); INFO(fmt::format("on its own: a ratio of {}", alone_ratio)); CHECK(alone_ratio * ALLOWED_DRIFT >= decimator); // The same square beside another, which it meets along x = 0.5. LLPointer pair = volumeOf({ plane(32), plane(32, 0.5f) }); LLPointer pair_target = targetFor(*pair); - const F32 pair_ratio = ALMeshSimplifier::simplifyFace(*pair, 0, pair_target->getVolumeFace(0), toCount(decimator), Seams::ALL); + const F32 pair_ratio = ALMeshSimplifier(*pair).simplifyFace(0, pair_target->getVolumeFace(0), toCount(decimator), Seams::ALL); INFO(fmt::format("beside another: a ratio of {}", pair_ratio)); - CHECK(pair_ratio * ALLOWED_DRIFT < decimator); - CHECK(borderOf(pair_target->getVolumeFace(0)).size() == 4 * 32); + CHECK(pair_ratio > 0.f); + const std::set shared = sharedPositions(*pair); + REQUIRE(shared.size() == 33); + const std::set border = borderOf(pair_target->getVolumeFace(0)); + S32 held = 0; + for (const EdgeKey& edge : border) + { + held += shared.count(edge.first) && shared.count(edge.second); + } + CHECK(held == 32); + // Fewer triangles than a border of 128 edges, all held, would need. + CHECK(pair_target->getVolumeFace(0).mNumIndices / 3 < 4 * 32); + } + + // Two faces at the same positions, as a garment's inside is a copy of + // its outside turned over, with a material of its own: each is held + // where its open border runs, which the other's is too, and simplifies + // inside it as a face on its own does. + TEST_CASE("almeshsimplifier: faces that lie over one another simplify face by face", "[almeshsimplifier]") + { + ArenaInstalled installed; + const F32 decimator = 9.f; + const Geometry front = plane(32); + Geometry back = front; + for (size_t i = 0; i < back.indices.size(); i += 3) + { + std::swap(back.indices[i + 1], back.indices[i + 2]); + } + for (LLVector3& normal : back.normals) + { + normal = -normal; + } + LLPointer base = volumeOf({ front, back }); + LLPointer target = targetFor(*base); + for (S32 f = 0; f < 2; ++f) + { + INFO(fmt::format("face {}", f)); + const F32 ratio = ALMeshSimplifier(*base).simplifyFace(f, target->getVolumeFace(f), toCount(decimator), Seams::ALL); + INFO(fmt::format("a ratio of {}", ratio)); + CHECK(ratio * ALLOWED_DRIFT >= decimator); + REQUIRE(wellFormed(target->getVolumeFace(f))); + CHECK(borderOf(target->getVolumeFace(f)).size() == 4 * 32); + } } // ------------------------------------------------------------------ @@ -763,7 +836,7 @@ namespace { INFO(fmt::format("a {}th, {}", decimator, nameOf(seams))); LLPointer target = targetFor(*base); - REQUIRE(ALMeshSimplifier::simplifyModel(*base, *target, toCount(decimator), seams) > 0.f); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*target, toCount(decimator), seams) > 0.f); const LLVolumeFace& face = target->getVolumeFace(0); REQUIRE(wellFormed(face)); size_t across = 0; @@ -827,11 +900,11 @@ namespace for (Seams seams : { Seams::ALL, Seams::UV }) { LLPointer held = targetFor(*base); - REQUIRE(ALMeshSimplifier::simplifyModel(*base, *held, toCount(decimator), seams) > 0.f); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*held, toCount(decimator), seams) > 0.f); CHECK(spanning(held->getVolumeFace(0)) == 0); } LLPointer free = targetFor(*base); - REQUIRE(ALMeshSimplifier::simplifyModel(*base, *free, toCount(decimator), Seams::NONE) > 0.f); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*free, toCount(decimator), Seams::NONE) > 0.f); CHECK(spanning(free->getVolumeFace(0)) > 0); } } @@ -857,14 +930,14 @@ namespace { INFO(fmt::format("a {}th", decimator)); LLPointer held = targetFor(*base); - REQUIRE(ALMeshSimplifier::simplifyModel(*base, *held, toCount(decimator), Seams::ALL) > 0.f); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*held, toCount(decimator), Seams::ALL) > 0.f); CHECK(mixed(held->getVolumeFace(0)) == 0); } // Free to cross them, at a level of detail the held one cannot // reach, it does. LLPointer free = targetFor(*base); - REQUIRE(ALMeshSimplifier::simplifyModel(*base, *free, toCount(100.f), Seams::UV) > 0.f); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*free, toCount(100.f), Seams::UV) > 0.f); CHECK(mixed(free->getVolumeFace(0)) > 0); } @@ -900,7 +973,7 @@ namespace LLPointer target = targetFor(*base); ALMeshSimplifier::Target goal = toCount(9.f); goal.distant = true; - REQUIRE(ALMeshSimplifier::simplifyModel(*base, *target, goal, Seams::ALL) > 0.f); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*target, goal, Seams::ALL) > 0.f); const LLVolumeFace& face = target->getVolumeFace(0); REQUIRE(wellFormed(face)); S32 bolt_vertices = 0; @@ -915,10 +988,52 @@ namespace // A level of detail seen from near keeps them, short of its target. LLPointer near_target = targetFor(*base); - REQUIRE(ALMeshSimplifier::simplifyModel(*base, *near_target, toCount(9.f), Seams::ALL) > 0.f); + REQUIRE(ALMeshSimplifier(*base).simplifyModel(*near_target, toCount(9.f), Seams::ALL) > 0.f); CHECK(triangleCount(*near_target) > target_triangles); } + // A distant level of detail made to an error, with no triangle target, + // of a model that is nothing but detached parts within that error: the + // parts are all it has, and it keeps them rather than drop every one, + // as a simplification to nothing would leave the uploader to take the + // whole model for it. The modes tried hold the cubes' seams, so only + // dropping a part could take one away; a mode free to cross them may + // collapse a cube smaller than the error to nothing. + TEST_CASE("almeshsimplifier: a distant level of detail to an error keeps a model of small parts", "[almeshsimplifier]") + { + ArenaInstalled installed; + Geometry model; + for (S32 bead = 0; bead < 16; ++bead) + { + const Geometry part = cube(1); + const F32 phi = F_TWO_PI * bead / 16; + const LLVector3 at(0.5f * cosf(phi), 0.5f * sinf(phi), 0.f); + const U16 first = (U16)model.positions.size(); + for (size_t v = 0; v < part.positions.size(); ++v) + { + model.add(at + part.positions[v] * 0.02f, part.normals[v], part.uvs[v]); + } + for (U16 index : part.indices) + { + model.indices.push_back((U16)(first + index)); + } + } + LLPointer base = volumeOf({ model }); + + ALMeshSimplifier::Target goal; + goal.decimator = 0.f; + goal.error = 0.03f; + goal.distant = true; + for (Seams seams : { Seams::ALL, Seams::UV }) + { + INFO(nameOf(seams)); + LLPointer target = targetFor(*base); + CHECK(ALMeshSimplifier(*base).simplifyModel(*target, goal, seams) > 0.f); + REQUIRE(wellFormed(target->getVolumeFace(0))); + CHECK(triangleCount(*target) >= 16); + } + } + // ------------------------------------------------------------------ // Compaction @@ -1197,7 +1312,7 @@ namespace { Tally& tally = tallies[{ (S32)seams, decimator }]; LLPointer target = targetFor(base); - const F32 ratio = ALMeshSimplifier::simplifyModel(base, *target, toCount(decimator), seams); + const F32 ratio = ALMeshSimplifier(base).simplifyModel(*target, toCount(decimator), seams); if (ratio < 0.f) { ++tally.failed; diff --git a/indra/llmath/tests/alsimdkernels_test.cpp b/indra/llmath/tests/alsimdkernels_test.cpp index 9d2ab8b9ba..fea2a0c95e 100644 --- a/indra/llmath/tests/alsimdkernels_test.cpp +++ b/indra/llmath/tests/alsimdkernels_test.cpp @@ -1408,8 +1408,10 @@ namespace // ------------------------------------------------------------------ // Normalizing - // Vectors at unit length, or marked (0, 0, 0, 1) where they have none, - // against normalize3 to within its rounding at every width and count. + // Vectors at unit length with a w of 0, or marked (0, 0, 0, 1) where + // they have none, against normalize3 to within its rounding at every + // width and count, whatever w they came with: sums of cross products + // carry the rounding of their edges' w. TEST_CASE("alsimdkernels: normalize3_or_mark against normalize3 at every width", "[alsimdkernels]") { for (size_t n : COUNTS) @@ -1425,7 +1427,7 @@ namespace } for (size_t i = 0; i < n; ++i) { - src[i].getF32ptr()[3] = 0.f; + src[i].getF32ptr()[3] = i % 3 == 0 ? 0.f : 0.25f * (F32)(i % 5 + 1); } bool any_expected = false; std::vector expected(src.begin(), src.begin() + n); @@ -1434,6 +1436,7 @@ namespace if (e.dot3(e).getF32() > FLT_MIN) { e.normalize3(); + e.getF32ptr()[3] = 0.f; } else { diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 938f7ca218..63ecaba9f2 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -2977,6 +2977,65 @@ namespace } } + // A plane whose mesh vertex (10, 10) has a quad of no area at each of + // its corners, each folded so that its diagonal's ends are one place: + // vertices (9, 9), (10, 10) and (11, 11) are one point, and so are + // (9, 10) and (10, 11), and (10, 9) and (11, 10). Vertex (c, r) reads + // pixel (2c, 2r). + SculptMap pinwheelMap() + { + auto surface = [](S32 x, S32 y) + { + const F32 u = (F32)x / (PINCH_MAP - 1); + const F32 v = (F32)y / (PINCH_MAP - 1); + return LLVector3(u - 0.5f, v - 0.5f, 0.2f * sinf(F_TWO_PI * u) * cosf(F_PI * v)); + }; + return sculptMap(PINCH_MAP, PINCH_MAP, [&](U16 x, U16 y) + { + if (x % 2 == 0 && y % 2 == 0) + { + const S32 dc = x / 2 - 10; + const S32 dr = y / 2 - 10; + if (dc == dr && dc >= -1 && dc <= 1) + { + return surface(20, 20); + } + if ((dc == -1 && dr == 0) || (dc == 0 && dr == 1)) + { + return surface(18, 20); + } + if ((dc == 0 && dr == -1) || (dc == 1 && dr == 0)) + { + return surface(20, 18); + } + } + return surface(x, y); + }); + } + + // A vertex whose quads all have no area has no normal of its own, and + // takes the nearest one down or up its column. Under fused multiply-add + // the corners off a folded quad's diagonal would take the rounding of + // an edge crossed with its own reverse for one. + TEST_CASE("llvolume: a vertex among quads folded along their diagonals takes a neighbour's normal", "[llvolume]") + { + LLPointer volume = buildSculpt(LL_SCULPT_TYPE_PLANE, pinwheelMap(), 3); + REQUIRE(volume->getNumVolumeFaces() == 1); + const LLVolumeFace& face = volume->getVolumeFace(0); + REQUIRE(isGrid(face)); + auto at = [&](S32 c, S32 r) { return gridIndex(face, c, r); }; + REQUIRE(coincide(face.mPositions[at(9, 9)], face.mPositions[at(10, 10)])); + REQUIRE(coincide(face.mPositions[at(11, 11)], face.mPositions[at(10, 10)])); + REQUIRE(coincide(face.mPositions[at(9, 10)], face.mPositions[at(10, 11)])); + REQUIRE(coincide(face.mPositions[at(10, 9)], face.mPositions[at(11, 10)])); + REQUIRE(!coincide(face.mPositions[at(10, 10)], face.mPositions[at(10, 9)])); + + const LLVector4a& normal = face.mNormals[at(10, 10)]; + const LLVector4a& below = face.mNormals[at(10, 9)]; + INFO(fmt::format("({}, {}, {}) against ({}, {}, {}) below it", normal[0], normal[1], normal[2], below[0], below[1], below[2])); + CHECK(std::memcmp(normal.getF32ptr(), below.getF32ptr(), 3 * sizeof(F32)) == 0); + } + // A map whose every pixel is one colour has no area, and makes the // sphere above LOD 0, where the area is tested. SculptMap oneColourMap() @@ -2985,29 +3044,34 @@ namespace } // The sphere a sculpt shows while its map is missing or has no area, - // and the empty one it shows otherwise. + // whatever its stitching, and the empty one it shows otherwise. void inspectPlaceholders(const Inspect& how, bool spheres_only, Report& report) { for (S32 lod = 0; lod < LOD_COUNT; ++lod) { - for (bool visible : { true, false }) + for (U8 stitching : STITCHINGS) { - if (spheres_only && !visible) + for (bool visible : { true, false }) { - continue; + if (spheres_only && !visible) + { + continue; + } + LLPointer volume = buildSculpt(stitching, SculptMap(), lod, visible); + const std::string name = + fmt::format("{} placeholder of a {} sculpt at LOD {}", visible ? "sphere" : "empty", stitchingName(stitching), lod); + if (volume->getSculptLevel() != -1 || volume->getNumVolumeFaces() != 1) + { + report.fail( + fmt::format("{}: sculpt level {} and {} faces", name, volume->getSculptLevel(), volume->getNumVolumeFaces())); + continue; + } + inspect(*volume, name, how, report); } - LLPointer volume = buildSculpt(LL_SCULPT_TYPE_SPHERE, SculptMap(), lod, visible); - const std::string name = fmt::format("{} placeholder at LOD {}", visible ? "sphere" : "empty", lod); - if (volume->getSculptLevel() != -1 || volume->getNumVolumeFaces() != 1) + if (!spheres_only || lod > 0) { - report.fail(fmt::format("{}: sculpt level {} and {} faces", name, volume->getSculptLevel(), volume->getNumVolumeFaces())); - continue; + inspectSculpt(stitching, oneColourMap(), lod, "one colour", how, report); } - inspect(*volume, name, how, report); - } - if (!spheres_only || lod > 0) - { - inspectSculpt(LL_SCULPT_TYPE_SPHERE, oneColourMap(), lod, "one colour", how, report); } } } @@ -3997,6 +4061,69 @@ namespace CHECK(report.failures == 0); } + // The grid form takes every quad the index buffer would, at widths + // whose lines a division by a rounded reciprocal of the width would + // count a lane short at their first quad: a rippled grid, its UVs + // rippled too, so that each quad's tangent differs from its + // neighbours' and a quad left out moves its corners' sums. The two + // add the same shares in different orders, and agree to within that. + TEST_CASE("llvolume: a grid's tangents take every quad at widths a reciprocal rounds", "[llvolume]") + { + Report report; + for (size_t columns : { 41, 47, 55, 61, 97, 107 }) + { + const size_t rows = 6; + const size_t count = columns * rows; + std::vector positions(count), normals(count); + std::vector uvs(count); + for (size_t t = 0; t < rows; ++t) + { + for (size_t s = 0; s < columns; ++s) + { + const F32 x = 0.1f * (F32)s; + const F32 y = 0.1f * (F32)t; + const F32 phase = 0.37f * (F32)s + 0.21f * (F32)t; + const size_t v = s + columns * t; + positions[v].set(x, y, 0.05f * sinf(phase), 0.f); + // The surface's normal: (-dz/dx, -dz/dy, 1), normalized. + normals[v].set(-0.05f * 3.7f * cosf(phase), -0.05f * 2.1f * cosf(phase), 1.f, 0.f); + normals[v].normalize3fast(); + uvs[v].set((F32)s / (F32)(columns - 1) + 0.03f * sinf(0.5f * (F32)t + (F32)s), + (F32)t / (F32)(rows - 1) + 0.02f * cosf(0.7f * (F32)s)); + } + } + std::vector indices; + for (size_t t = 0; t + 1 < rows; ++t) + { + for (size_t s = 0; s + 1 < columns; ++s) + { + const U16 bl = (U16)(s + columns * t); + const U16 br = (U16)(bl + 1); + const U16 tl = (U16)(bl + columns); + const U16 tr = (U16)(tl + 1); + indices.insert(indices.end(), { bl, tr, tl, bl, br, tr }); + } + } + + std::vector grid(count), indexed(count); + alsimd::grid_tangents(positions.data(), normals.data(), uvs.data(), columns, rows, grid.data()); + alsimd::indexed_tangents(positions.data(), normals.data(), uvs.data(), count, indices.data(), indices.size(), indexed.data()); + for (size_t v = 0; v < count; ++v) + { + const F64 apart = degreesApart(v3(grid[v]), v3(indexed[v])); + if (!(apart <= 1e-3) || grid[v][3] != indexed[v][3]) + { + report.fail(fmt::format("{} columns: vertex {} (column {}, row {}) is {} degrees from the indexed tangent, " + "w {} against {}", + columns, v, v % columns, v / columns, apart, grid[v][3], indexed[v][3])); + break; + } + } + } + INFO(report.text()); + CHECK(report.failures == 0); + } + // A card drawn both ways, the one triangle twice sharing two vertices, // its third corner mirrored in U a hair off true, and turned off the // axes: at the shared vertices the two triangles' tangents all but @@ -4055,6 +4182,98 @@ namespace } } + // A mesh face's vertex count is its positions'. A normal or texture + // coordinate array that falls short of it is taken as none, its zeroes + // in their place, rather than read past its end. + TEST_CASE("llvolume: a mesh face whose normals or texture coordinates fall short of its positions decodes without them", "[llvolume]") + { + auto binary = [](std::initializer_list values) + { + LLSD::Binary bytes; + for (U16 value : values) + { + bytes.push_back((U8)(value & 0xff)); + bytes.push_back((U8)(value >> 8)); + } + return LLSD(bytes); + }; + LLSD face; + face["Position"] = binary({ 0, 0, 0, 65535, 0, 0, 0, 65535, 0 }); + face["Normal"] = binary({ 32768, 32768, 65535 }); + face["TexCoord0"] = binary({ 65535, 65535 }); + face["TriangleList"] = binary({ 0, 1, 2 }); + face["PositionDomain"]["Min"] = LLVector3(-0.5f, -0.5f, -0.5f).getValue(); + face["PositionDomain"]["Max"] = LLVector3(0.5f, 0.5f, 0.5f).getValue(); + face["TexCoord0Domain"]["Min"] = LLVector2(0.f, 0.f).getValue(); + face["TexCoord0Domain"]["Max"] = LLVector2(1.f, 1.f).getValue(); + LLSD mdl = LLSD::emptyArray(); + mdl.append(face); + + LLVolumeParams params; + params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE); + params.setSculptID(LLUUID("00000000-0000-0000-0000-00000000000a"), LL_SCULPT_TYPE_MESH); + LLPointer volume = new LLVolume(params, LLVolumeLODGroup::getVolumeScaleFromDetail(3)); + REQUIRE(ll_test::VolumeProbe::decode(*volume, mdl)); + REQUIRE(volume->getNumVolumeFaces() == 1); + const LLVolumeFace& decoded = volume->getVolumeFace(0); + REQUIRE(decoded.mNumVertices == 3); + for (S32 v = 0; v < decoded.mNumVertices; ++v) + { + CAPTURE(v); + CHECK(decoded.mNormals[v].equals3(LLVector4a::getZero())); + CHECK(decoded.mTexCoords[v] == LLVector2(0.f, 0.f)); + } + } + + // A rigged face's weights are read vertex by vertex until they run out; + // a vertex they run out before takes the weight of one with no + // influence, all of it on the first joint, rather than whatever its + // allocation held. + TEST_CASE("llvolume: a mesh face whose weights run out before its vertices weighs the rest by no influence", "[llvolume]") + { + auto binary = [](std::initializer_list values) + { + LLSD::Binary bytes; + for (U16 value : values) + { + bytes.push_back((U8)(value & 0xff)); + bytes.push_back((U8)(value >> 8)); + } + return LLSD(bytes); + }; + LLSD face; + face["Position"] = binary({ 0, 0, 0, 65535, 0, 0, 0, 65535, 0 }); + face["TriangleList"] = binary({ 0, 1, 2 }); + face["PositionDomain"]["Min"] = LLVector3(-0.5f, -0.5f, -0.5f).getValue(); + face["PositionDomain"]["Max"] = LLVector3(0.5f, 0.5f, 0.5f).getValue(); + face["TexCoord0Domain"]["Min"] = LLVector2(0.f, 0.f).getValue(); + face["TexCoord0Domain"]["Max"] = LLVector2(1.f, 1.f).getValue(); + // The first vertex's influences, joint 3 at half, and no more. + face["Weights"] = LLSD(LLSD::Binary{ 3, 0xff, 0x7f, 0xff }); + LLSD mdl = LLSD::emptyArray(); + mdl.append(face); + + LLVolumeParams params; + params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE); + params.setSculptID(LLUUID("00000000-0000-0000-0000-00000000000b"), LL_SCULPT_TYPE_MESH); + LLPointer volume = new LLVolume(params, LLVolumeLODGroup::getVolumeScaleFromDetail(3)); + REQUIRE(ll_test::VolumeProbe::decode(*volume, mdl)); + REQUIRE(volume->getNumVolumeFaces() == 1); + const LLVolumeFace& decoded = volume->getVolumeFace(0); + REQUIRE(decoded.mNumVertices == 3); + REQUIRE(decoded.mWeights); + CHECK((S32)decoded.mWeights[0][0] == 3); + for (S32 v = 1; v < decoded.mNumVertices; ++v) + { + CAPTURE(v); + const LLVector4a& weight = decoded.mWeights[v]; + CHECK(weight[0] == 0.999f); + CHECK(weight[1] == 0.f); + CHECK(weight[2] == 0.f); + CHECK(weight[3] == 0.f); + } + } + // A circle path's stations step around it and end at its end cut. The // steps add up with rounding, and a station they bring a rounding short // of the end would leave a row with no height before the end's: no two @@ -4087,6 +4306,58 @@ namespace CHECK(report.failures == 0); } + // A begin cut just short of a step, as the cuts the wire carries can + // be, snaps the path's first step to within a hundredth of a step of + // the cut, and so can the rounding of the step's size where the cut is + // on a step: a twist of 1.4 turns at LOD 3 gives a torus 41 sides, + // whose first step rounds back to a cut at 0. That step is the cut's + // own, as the end's last step is, and the first row has height. + TEST_CASE("llvolume: a circle path's first station after its begin cut is apart from it", "[llvolume]") + { + Report report; + S32 cuts = 0; + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + const F32 detail = LOD_DETAIL[lod]; + for (F32 twist : { 0.f, 0.7f }) + { + const S32 sides = llfloor(MIN_DETAIL_FACES * detail + 2.f * twist * 3.5f * (detail - 0.5f)); + const F32 step = 1.f / sides; + std::vector begins = { 0.f }; + for (S32 k = 1; k < sides; ++k) + { + const S32 quanta = (S32)((F32)k / (F32)sides / CUT_QUANTA); + for (S32 j = 0; j < 3; ++j) + { + begins.push_back((F32)(quanta - j) * CUT_QUANTA); + } + } + for (F32 begin : begins) + { + Shape s = torus(); + s.twist_begin = -twist; + s.twist_end = twist; + s.path_begin = begin; + LLPath path; + path.generate(s.params().getPathParams(), detail); + ++cuts; + for (S32 i = 0; i + 1 < (S32)path.mPath.size(); ++i) + { + if (!(path.mPath[i + 1].mTexT - path.mPath[i].mTexT >= step * 0.01f)) + { + report.fail(fmt::format("{} sides at LOD {}, cut at {}: stations {} and {} are at t {} and {}", sides, lod, + begin, i, i + 1, path.mPath[i].mTexT, path.mPath[i + 1].mTexT)); + break; + } + } + } + } + } + INFO(fmt::format("{} cuts", cuts)); + INFO(report.text()); + CHECK(report.failures == 0); + } + // The streaming cost counts a circle2 path's points with // getNumNGonPoints, which walks the stations as genNGon places them. An // end cut on a step, which the summed steps reach a rounding short of, @@ -4118,6 +4389,38 @@ namespace CHECK(report.failures == 0); } + // A sculpt samples a row of its map at each station of its path, as + // every viewer samples it, so its path keeps the station the summed + // steps bring a rounding short of the end, which a prim's leaves out: + // its last station before the end is the last step the steps take + // short of the end, and a step on from it is not short of the end. + TEST_CASE("llvolume: a sculpt's path keeps a station a rounding short of its end", "[llvolume]") + { + Report report; + S32 kept = 0; + const LLPathParams params = sphere().params().getPathParams(); + for (S32 sides = 2; sides <= 300; ++sides) + { + LLPath path; + path.generate(params, 1.f, 0, true, sides); + const S32 count = (S32)path.mPath.size(); + const F32 step = 1.0f / sides; + const F32 last = path.mPath[count - 2].mTexT; + if (!(last < params.getEnd()) || last + step < params.getEnd()) + { + report.fail( + fmt::format("{} sides: the last station before the end is at t {}, a step of {} short of the end", sides, last, step)); + } + if (last > params.getEnd() - step * 0.01f) + { + ++kept; + } + } + INFO(report.text()); + CHECK(report.failures == 0); + CHECK(kept > 0); + } + // A triangle whose UVs have no area has no dP/du, and one whose // positions have no area has no surface to give one: neither adds to // its corners' tangents, so a real neighbour's tangent stands alone. @@ -4285,6 +4588,85 @@ namespace CHECK(report.failures == 0); } + // A flexible prim of one section, as a distant one is drawn, keeps the + // two stations its volume is first built with, both at the origin + // before the rebuild gives them a pose, so every frame is a partial + // build of that first one. Its first and last rows were one place + // there, and are not joined for it: its normals are those a full build + // at its pose makes. + TEST_CASE("llvolume: a flexible prim of one section keeps its first and last rows apart", "[llvolume]") + { + Report report; + for (U8 profile : { LL_PCODE_PROFILE_SQUARE, LL_PCODE_PROFILE_CIRCLE }) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + const std::string name = + fmt::format("flexible {}, one section, LOD {}", profile == LL_PCODE_PROFILE_SQUARE ? "box" : "cylinder", lod); + LLPointer volume = flexiVolume(profile, LL_PCODE_HOLE_SAME, 0.f, lod); + REQUIRE(volume->getPath().getPathLength() == 2); + bendPath(*volume, 1, 1.1f); + volume->regen(); + genAllTangents(*volume); + + LLPointer full = flexiVolume(profile, LL_PCODE_HOLE_SAME, 0.f, lod); + full->resizePath(2); + bendPath(*full, 1, 1.1f); + full->regen(); + genAllTangents(*full); + compareBuilds(*volume, *full, report, name); + } + } + INFO(report.text()); + CHECK(report.failures == 0); + } + + // A flexible prim's sides have no area before its first pose, and keep + // no planar normal from it: a one-section flexible prim's frames, every + // one a partial build of that first one, would otherwise project every + // side along z. Once posed, each side's planar normal is the one a full + // build at the pose makes. Its caps have area from the first, and keep + // the planar normal they first have, as every face of a flexible prim + // does. + TEST_CASE("llvolume: a flexible prim keeps no planar normal from before its first pose", "[llvolume]") + { + Report report; + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLPointer volume = flexiVolume(LL_PCODE_PROFILE_SQUARE, LL_PCODE_HOLE_SAME, 0.f, lod); + for (S32 f = 0; f < volume->getNumVolumeFaces(); ++f) + { + volume->getVolumeFace(f).getPlanarNormal(); + } + bendPath(*volume, 1, 1.1f); + volume->regen(); + + LLPointer full = flexiVolume(LL_PCODE_PROFILE_SQUARE, LL_PCODE_HOLE_SAME, 0.f, lod); + full->resizePath(2); + bendPath(*full, 1, 1.1f); + full->regen(); + REQUIRE(volume->getNumVolumeFaces() == full->getNumVolumeFaces()); + for (S32 f = 0; f < volume->getNumVolumeFaces(); ++f) + { + if (!(volume->getVolumeFace(f).mTypeMask & LLVolumeFace::SIDE_MASK)) + { + continue; + } + const LLVector4a got = volume->getVolumeFace(f).getPlanarNormal(); + const LLVector4a want = full->getVolumeFace(f).getPlanarNormal(); + if (std::memcmp(got.getF32ptr(), want.getF32ptr(), 4 * sizeof(F32)) != 0) + { + report.fail(fmt::format("flexible box, one section, LOD {}: {} has planar normal ({}, {}, {}, {}), " + "a full build ({}, {}, {}, {})", + lod, faceName(volume->getVolumeFace(f)), got[0], got[1], got[2], got[3], want[0], want[1], + want[2], want[3])); + } + } + } + INFO(report.text()); + CHECK(report.failures == 0); + } + // The uncut cube cap is built only on a line path, which flexi never // has; its partial build is the one a regen of an unmoved box makes. TEST_CASE("llvolume: an uncut cube cap's partial build keeps its buffers", "[llvolume]") @@ -4360,11 +4742,74 @@ namespace CHECK(report.failures == 0); } + // A sphere's poles are where its half circle touches the axis it sweeps + // about, and their normals lie along that axis: out of the sphere on its + // outer side, into it on a hollow sphere's inner side. A twist of a half + // turn at both ends puts each pole at the other end of the axis. + TEST_CASE("llvolume: a sphere's pole normals face out of it at any half turn of twist", "[llvolume]") + { + Report report; + S32 poles = 0; + for (F32 twist : { 0.f, 0.5f, -0.5f, 1.f }) + { + for (F32 hollow : { 0.f, 0.5f }) + { + Shape s = sphere(); + s.twist_begin = s.twist_end = twist; + s.hollow = hollow; + s.name = fmt::format("sphere twisted {} hollow {}", twist, hollow); + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLPointer volume = build(s, lod); + for (S32 f = 0; f < volume->getNumVolumeFaces(); ++f) + { + const LLVolumeFace& face = volume->getVolumeFace(f); + if (!(face.mTypeMask & LLVolumeFace::SIDE_MASK) || face.mNumS * face.mNumT != face.mNumVertices) + { + continue; + } + const F32 inward = (face.mTypeMask & LLVolumeFace::INNER_MASK) ? -1.f : 1.f; + for (S32 column : { 0, face.mNumS - 1 }) + { + bool pole = true; + for (S32 t = 1; t < face.mNumT && pole; ++t) + { + pole = face.mPositions[column + face.mNumS * t].equals3(face.mPositions[column], 1e-6f); + } + if (!pole) + { + continue; + } + ++poles; + for (S32 t = 0; t < face.mNumT; ++t) + { + const S32 v = column + face.mNumS * t; + const F32 out = inward * face.mNormals[v].dot3(face.mPositions[v]).getF32(); + if (!(out > 0.f)) + { + report.fail(fmt::format("{}: {}'s pole at column {} has normal ({}, {}, {}) at ({}, {}, {})", + lodName(s, lod), faceName(face), column, face.mNormals[v][0], + face.mNormals[v][1], face.mNormals[v][2], face.mPositions[v][0], + face.mPositions[v][1], face.mPositions[v][2])); + break; + } + } + } + } + } + } + } + INFO(report.text()); + CHECK(poles > 0); + CHECK(report.failures == 0); + } + // The sphere placeholder gets a sphere sculpt's normal stitching, so it // must run as a sphere sculpt's map does, around along the profile and // pole to pole along the path. The other way round, the poles' average // would land on one meridian, and the S wrap would sum the north pole - // with the south. + // with the south. It gets the sphere's stitching whatever its own: a + // torus's would sum its two poles, and a plane's leave its seam open. TEST_CASE("llvolume: the sphere placeholder's normals follow its surface", "[llvolume]") { Inspect how; diff --git a/indra/llprimitive/tests/llmodel_test.cpp b/indra/llprimitive/tests/llmodel_test.cpp index 0a112d9d99..f7522f405d 100644 --- a/indra/llprimitive/tests/llmodel_test.cpp +++ b/indra/llprimitive/tests/llmodel_test.cpp @@ -125,7 +125,7 @@ namespace lod->setNumVolumeFaces(1); ALMeshSimplifier::Target goal; goal.decimator = 9.f; - REQUIRE(ALMeshSimplifier::simplifyModel(*model, *lod, goal, seams) > 0.f); + REQUIRE(ALMeshSimplifier(*model).simplifyModel(*lod, goal, seams) > 0.f); lod->mSkinWeights = model->mSkinWeights; CHECK(weightsFound(*lod)); } diff --git a/indra/newview/gltf/primitive.cpp b/indra/newview/gltf/primitive.cpp index a30942efed..d0e8c8810f 100644 --- a/indra/newview/gltf/primitive.cpp +++ b/indra/newview/gltf/primitive.cpp @@ -29,7 +29,6 @@ #include "asset.h" #include "buffer_util.h" #include "../llviewershadermgr.h" -#include "alscratcharena.h" #include @@ -284,59 +283,6 @@ struct MikktMesh }; -// An attribute's values in the order remap gives them, and only those it -// keeps. -template -static void remap_attribute(std::vector& attribute, const U32* remap, size_t kept) -{ - if (!attribute.empty()) - { - meshopt_remapVertexBuffer(attribute.data(), attribute.data(), attribute.size(), sizeof(T), remap); - attribute.resize(kept); - } -} - -// A triangle list's triangles in an order a vertex cache reuses, then its -// vertices in the order those triangles first take them, every attribute -// moved alike; a vertex no triangle takes is dropped. The remap and -// meshoptimizer's scratch come from the thread's arena. A primitive whose -// attributes are not one value a vertex, or whose indices are not whole -// triangles, is left as it is. -static void order_for_vertex_cache(Primitive& prim) -{ - const size_t index_count = prim.mIndexArray.size(); - const size_t vertex_count = prim.mPositions.size(); - if (prim.mGLMode != LLRender::TRIANGLES || index_count < 3 || index_count % 3 != 0 || vertex_count == 0) - { - return; - } - for (size_t size : { prim.mNormals.size(), prim.mTangents.size(), prim.mTexCoords0.size(), prim.mTexCoords1.size(), prim.mColors.size(), - prim.mWeights.size(), prim.mJoints.size() }) - { - if (size != 0 && size != vertex_count) - { - return; - } - } - - U32* indices = prim.mIndexArray.data(); - meshopt_optimizeVertexCache(indices, indices, index_count, vertex_count); - - ALScratchArena::Scope scratch; - U32* remap = scratch.allocate(vertex_count); - const size_t kept = meshopt_optimizeVertexFetchRemap(remap, indices, index_count, vertex_count); - meshopt_remapIndexBuffer(indices, indices, index_count, remap); - - remap_attribute(prim.mPositions, remap, kept); - remap_attribute(prim.mNormals, remap, kept); - remap_attribute(prim.mTangents, remap, kept); - remap_attribute(prim.mTexCoords0, remap, kept); - remap_attribute(prim.mTexCoords1, remap, kept); - remap_attribute(prim.mColors, remap, kept); - remap_attribute(prim.mWeights, remap, kept); - remap_attribute(prim.mJoints, remap, kept); -} - static void vertical_flip(std::vector& texcoords) { for (auto& tc : texcoords) @@ -522,8 +468,6 @@ bool Primitive::prep(Asset& asset) } } - order_for_vertex_cache(*this); - if (!mNormals.empty()) { mask |= LLVertexBuffer::MAP_NORMAL; diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index bd67480d1b..4155835374 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -1539,15 +1539,16 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d goal.decimator = indices_decimator; goal.error = lod_error_threshold; goal.distant = lod <= LLModel::LOD_LOW; + const ALMeshSimplifier simplifier(*base); auto simplify_model = [&](F32 decimator, ALMeshSimplifier::Seams seams) { ALMeshSimplifier::Target at = goal; at.decimator = decimator; - return ALMeshSimplifier::simplifyModel(*base, *target_model, at, seams); + return simplifier.simplifyModel(*target_model, at, seams); }; auto simplify_face = [&](S32 face_idx, ALMeshSimplifier::Seams seams) { - return ALMeshSimplifier::simplifyFace(*base, face_idx, target_model->getVolumeFace(face_idx), goal, seams); + return simplifier.simplifyFace(face_idx, target_model->getVolumeFace(face_idx), goal, seams); }; // Ideally this should run not per model, @@ -1599,18 +1600,25 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d if (model_meshopt_mode == MESH_OPTIMIZER_AUTO) { - // Remove progressively more data if we can't reach the target. + // Remove progressively more data if we can't reach the target, + // keeping the closest a mode came: target_model holds the + // last mode's. F32 allowed_ratio_drift = 1.8f; - F32 precise_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::ALL); - - if (precise_ratio < 0 || (precise_ratio * allowed_ratio_drift < indices_decimator)) - { - precise_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::UV); - } - - if (precise_ratio < 0 || (precise_ratio * allowed_ratio_drift < indices_decimator)) + using Seams = ALMeshSimplifier::Seams; + F32 precise_ratio = -1.f; + Seams precise_seams = Seams::ALL; + for (Seams seams : { Seams::ALL, Seams::UV, Seams::NONE }) { - precise_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::NONE); + const F32 ratio = simplify_model(indices_decimator, seams); + if (ratio > precise_ratio) + { + precise_ratio = ratio; + precise_seams = seams; + } + if (ratio >= 0 && ratio * allowed_ratio_drift >= indices_decimator) + { + break; + } } if (precise_ratio < 0 || (precise_ratio * allowed_ratio_drift < indices_decimator)) @@ -1635,10 +1643,10 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d } // Sloppy has a tendecy to error into lower side, so a request for 100 - // triangles turns into ~70, so check for significant difference from target decimation + // triangles turns into ~70, so check for significant difference from target decimation. + // A sloppy pass that failed has no ratio to correct by; smaller decimators are tried below. F32 sloppy_ratio_drift = 1.4f; - if (lod_mode == LIMIT_TRIANGLES - && (sloppy_ratio > indices_decimator * sloppy_ratio_drift || sloppy_ratio < 0)) + if (lod_mode == LIMIT_TRIANGLES && sloppy_ratio > indices_decimator * sloppy_ratio_drift) { // Apply a correction to compensate. @@ -1649,7 +1657,10 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d sloppy_ratio = simplify_model(sloppy_decimator, ALMeshSimplifier::Seams::SLOPPY); } - if (last_working_decimator > 0 && sloppy_ratio < last_working_ratio) + // A correction that failed, or came out further from the + // target than the first pass, goes back to the first. + if (last_working_decimator > 0 && + (sloppy_ratio < 0 || fabs(sloppy_ratio - indices_decimator) > fabs(last_working_ratio - indices_decimator))) { // Compensation didn't work, return back to previous decimator sloppy_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::SLOPPY); @@ -1688,8 +1699,8 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d } else { - // Fallback to normal method - precise_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::ALL); + // Fallback to normal method, in the mode that came closest + precise_ratio = simplify_model(indices_decimator, precise_seams); } LL_DEBUGS("Upload") << "Model " << target_model->getName() diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 3c0232f2bc..28121b4a70 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -2643,22 +2643,20 @@ bool LLVOVolume::hasMedia() const LLVector3 LLVOVolume::getApproximateFaceNormal(U8 face_id) { LLVolume* volume = getVolume(); - LLVector4a result; - result.clear(); - LLVector3 ret; + // A face that faces one way, as planar texgen takes it, faces along its + // planar normal. One that wraps around faces no one way, and the zero + // returned leaves the caller to look along the camera. if (volume && face_id < volume->getNumVolumeFaces()) { - const LLVolumeFace& face = volume->getVolumeFace(face_id); - for (S32 i = 0; i < (S32)face.mNumVertices; ++i) + const LLVector4a planar = volume->getVolumeFace(face_id).getPlanarNormal(); + if (planar[3] > 0.f) { - result.add(face.mNormals[i]); + ret.set(planar.getF32ptr()); + ret = volumeDirectionToAgent(ret); + ret.normVec(); } - - ret.set(result.getF32ptr()); - ret = volumeDirectionToAgent(ret); - ret.normVec(); } return ret; diff --git a/indra/vcpkg-configuration.json b/indra/vcpkg-configuration.json index eff7739f30..741a2ec9ea 100644 --- a/indra/vcpkg-configuration.json +++ b/indra/vcpkg-configuration.json @@ -2,7 +2,7 @@ "registries": [ { "kind": "git", - "baseline": "a6132ca341959dc989c88cd2aac992399a6d12bb", + "baseline": "18989321bd0cb4b6163827833989621fce1e9209", "reference": "main", "repository": "https://github.com/AlchemyViewer/alchemy-registry", "packages": [