From f13d52e949657420d3df4357197bb4502513a7c4 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:52:05 -0400 Subject: [PATCH 01/16] A round prim's side normals point straight out of its surface createSide summed each triangle's normal into its corners and then, to "even out quad contributions", added it a second time to the corner both triangles of a quad share. That corner and the one opposite it already had both triangles, so the extra weight made every vertex normal lean around the axis, one way on a face's first row and the other on its last: on a six-sided cylinder by +10.9 and -19.1 degrees, still +2.5 and -4.5 at LOD 3. Under PBR the highlight slanted across every facet. Each grid quad now gives the two corners off its diagonal their own triangle's cross product and the two on it half of each, so a parallelogram weights its four corners alike and no corner takes a normal from a triangle it is not in. The products are taken at the corners off the diagonal, so a quad between copied rows or columns adds exactly nothing and a sculpt's hard edge stays hard. The loop runs over the grid, not the index buffer, so the result does not depend on triangle order. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 133 ++++++++++----------------- indra/llmath/tests/llvolume_test.cpp | 65 +++++++------ 2 files changed, 85 insertions(+), 113 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 3b401b378a..c73f236232 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -6371,6 +6371,42 @@ size_t LLVolumeFace::weldVertices(VertexData* destination_vertices, U32* destina return unique; } +namespace +{ + // Adds a grid quad to its corners' normals. Its two triangles, (bl, br, + // tr) and (bl, tr, tl), share the diagonal from bl to tr: each gives its + // corner off the diagonal its cross product, twice its area along its + // normal, and each corner on the diagonal half of it. A parallelogram + // then gives its four corners one weight, so a vertex has as much from + // either side of it in every row, and no corner takes a normal from a + // triangle it is not in. Each cross product is taken at the corner off + // the diagonal: where a copied or collapsed row or column puts 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. + void add_quad_to_normals(LLVector4a* norm, const LLVector4a* pos, S32 bl, S32 br, S32 tl, S32 tr) + { + LLVector4a bottom, right, top, left; + bottom.setSub(pos[br], pos[bl]); + right.setSub(pos[tr], pos[br]); + top.setSub(pos[tr], pos[tl]); + left.setSub(pos[tl], pos[bl]); + + LLVector4a lower, upper; + lower.setCross3(bottom, right); + upper.setCross3(top, left); + + LLVector4a shared; + shared.setAdd(lower, upper); + shared.mul(0.5f); + + norm[bl].add(shared); + norm[br].add(lower); + norm[tr].add(shared); + norm[tl].add(upper); + } +} + bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; @@ -6581,98 +6617,25 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) } } - //clear normals - F32* dst = (F32*) mNormals; - F32* end = (F32*) (mNormals+mNumVertices); - LLVector4a zero = LLVector4a::getZero(); - - while (dst < end) - { - zero.store4a(dst); - dst += 4; - } - - //generate normals - U32 count = mNumIndices/3; - + // The normals come from the grid's quads, not from the triangles the + // indices make of them, so they do not depend on the diagonals or on the + // order of the triangles. LLVector4a* norm = mNormals; - static thread_local LLAlignedArray triangle_normals; - try + for (S32 v = 0; v < mNumVertices; ++v) { - triangle_normals.resize(count); + norm[v].clear(); } - catch (std::bad_alloc&) - { - LL_WARNS("LLVOLUME") << "Resize of triangle_normals to " << count << " failed" << LL_ENDL; - return false; - } - LLVector4a* output = triangle_normals.mArray; - LLVector4a* end_output = output+count; - - U16* idx = mIndices; - while (output < end_output) + for (t = 0; t < mNumT - 1; ++t) { - LLVector4a b,v1,v2; - b.load4a((F32*) (pos+idx[0])); - v1.load4a((F32*) (pos+idx[1])); - v2.load4a((F32*) (pos+idx[2])); - - //calculate triangle normal - LLVector4a a; - - a.setSub(b, v1); - b.sub(v2); - - v1.setCross3(a, b); - - llassert(v1.isFinite3()); - - v1.store4a((F32*) output); - - - output++; - idx += 3; - } - - idx = mIndices; - - LLVector4a* src = triangle_normals.mArray; - - for (U32 i = 0; i < count; i++) //for each triangle - { - LLVector4a c; - c.load4a((F32*) (src++)); - - LLVector4a* n0p = norm+idx[0]; - LLVector4a* n1p = norm+idx[1]; - LLVector4a* n2p = norm+idx[2]; - - idx += 3; - - LLVector4a n0,n1,n2; - n0.load4a((F32*) n0p); - n1.load4a((F32*) n1p); - n2.load4a((F32*) n2p); - - n0.add(c); - n1.add(c); - n2.add(c); - - llassert(c.isFinite3()); - - //even out quad contributions - switch (i%2+1) + const S32 row = mNumS * t; + for (s = 0; s < mNumS - 1; ++s) { - case 0: n0.add(c); break; - case 1: n1.add(c); break; - case 2: n2.add(c); break; - }; - - n0.store4a((F32*) n0p); - n1.store4a((F32*) n1p); - n2.store4a((F32*) n2p); + const S32 bl = row + s; + const S32 tl = bl + mNumS; + add_quad_to_normals(norm, pos, bl, bl + 1, tl, tl + 1); + } } // adjust normals based on wrapping and stitching diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 63467e255a..a25c04f25c 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -2549,10 +2549,12 @@ namespace // ------------------------------------------------------------------ // Today's defects - // The even-out switch adds each triangle's normal a second time to - // its top-right vertex, so a round prim's vertex normals lean around - // the axis, by an amount that depends on the row. - TEST_CASE("llvolume: a cylinder's side normals point straight out from its axis", "[llvolume][!shouldfail]") + // A cylinder side's quads are rectangles of one size, so a vertex has + // as much area on either side of it, in every row: a weight that went to + // some corners of a quad and not others, as the top right corner or the + // corners of one triangle, would lean the normals around the axis, by + // an amount that depends on the row. + TEST_CASE("llvolume: a cylinder's side normals point straight out from its axis", "[llvolume]") { for (S32 lod = 0; lod < LOD_COUNT; ++lod) { @@ -2581,37 +2583,44 @@ namespace } } - // The double count goes to whichever vertex is top right in - // the grid, so a mirrored sculpt, whose rows run the other way, does - // not get the mirror image of the plain one's normals. - TEST_CASE("llvolume: a mirrored sculpt's normals mirror the plain one's", "[llvolume][!shouldfail]") + // A mirrored sculpt's rows run the other way, so a weight that went to + // whichever corner is top right in the grid would not give it the mirror + // image of the plain one's normals. The map is a cylinder's, whose quads + // are flat even in eight bits: a mirrored sculpt's diagonals run the + // other way across its surface, and where a quad is warped, as eight bits + // warp a torus's, its two triangles are not the mirror image of the plain + // one's, and neither are normals that follow them. + TEST_CASE("llvolume: a mirrored sculpt's normals mirror the plain one's", "[llvolume]") { - const SculptMap map = torusMap(64, 64); - LLPointer plain = buildSculpt(LL_SCULPT_TYPE_TORUS, map, 3); - LLPointer mirrored = buildSculpt((U8)(LL_SCULPT_TYPE_TORUS | LL_SCULPT_FLAG_MIRROR), map, 3); - const LLVolumeFace& a = plain->getVolumeFace(0); - const LLVolumeFace& b = mirrored->getVolumeFace(0); - REQUIRE(a.mNumS == b.mNumS); - F32 worst = 0.f; - for (S32 t = 0; t < a.mNumT; ++t) + const SculptMap map = cylinderMap(64, 64); + for (S32 lod = 0; lod < LOD_COUNT; ++lod) { - for (S32 s = 0; s < a.mNumS; ++s) + LLPointer plain = buildSculpt(LL_SCULPT_TYPE_CYLINDER, map, lod); + LLPointer mirrored = buildSculpt((U8)(LL_SCULPT_TYPE_CYLINDER | LL_SCULPT_FLAG_MIRROR), map, lod); + const LLVolumeFace& a = plain->getVolumeFace(0); + const LLVolumeFace& b = mirrored->getVolumeFace(0); + REQUIRE(a.mNumS == b.mNumS); + REQUIRE(a.mNumT == b.mNumT); + F32 worst = 0.f; + for (S32 t = 0; t < a.mNumT; ++t) { - LLVector3 expected = v3(a.mNormals[gridIndex(a, a.mNumS - 1 - s, t)]); - expected.mV[0] = -expected.mV[0]; - worst = llmax(worst, degreesBetween(v3(b.mNormals[gridIndex(b, s, t)]), expected)); + for (S32 s = 0; s < a.mNumS; ++s) + { + LLVector3 expected = v3(a.mNormals[gridIndex(a, a.mNumS - 1 - s, t)]); + expected.mV[0] = -expected.mV[0]; + worst = llmax(worst, degreesBetween(v3(b.mNormals[gridIndex(b, s, t)]), expected)); + } } + INFO(fmt::format("LOD {}: the mirrored normals are up to {:.2f} degrees from the mirror image", lod, worst)); + CHECK(worst < 0.1f); } - INFO(fmt::format("the mirrored normals are up to {:.2f} degrees from the mirror image", worst)); - CHECK(worst < 0.1f); } - // Where a smooth side creases sharply, as a flattened torus - // tube does at LOD 0, the double count's weights turn a normal past - // the triangles on the crease's light side. Equal weights cannot: a - // crease turns by less than 180 degrees, and its normal by half that - // from each side. - TEST_CASE("llvolume: a torus's normals face every triangle at a sharp crease", "[llvolume][!shouldfail]") + // Where a smooth side creases sharply, as a flattened torus tube does at + // LOD 0, weights that favour one side of the crease turn a normal past + // the triangles on its light side. Equal weights cannot: a crease turns + // by less than 180 degrees, and its normal by half that from each side. + TEST_CASE("llvolume: a torus's normals face every triangle at a sharp crease", "[llvolume]") { Inspect how; how.checks = Checks::Inversion; From 229feace0ce28a626d8c3537291b90c3b8411930 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:52:20 -0400 Subject: [PATCH 02/16] A side joins its normals only across a seam its own positions close The S wrap summed a side's first and last columns whenever the profile counted as closed. On a box those columns are the two edges of one flat side, so a twisted box, tube or prism averaged each side's left and right edges together, and a half cylinder at LOD 2 and 3 added its two opposite rims and turned them over. The T wrap trusted the path's openness, which ignores revolutions and skew: a torus at one and a half turns summed rows half a turn apart, and a skewed circle2 path whose rows meet was never wrapped. A side of two rows was never wrapped at all, since the pole test compared row 0 with itself, leaving the seams of LOD 0 and 1 cylinders 40 to 60 degrees apart. Each face now decides on a full build, from its own positions, whether its first and last columns are one seam and whether its first and last rows are (mJoinsS, mJoinsT), and joins only those. Sculpts keep joining where their stitching samples one column or row of the map. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 228 +++++++++++++++------------ indra/llmath/llvolume.h | 6 + indra/llmath/tests/llvolume_test.cpp | 79 ++-------- 3 files changed, 148 insertions(+), 165 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index c73f236232..3f6a5a6805 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -4541,6 +4541,8 @@ LLVolumeFace& LLVolumeFace::operator=(const LLVolumeFace& src) mBeginT = src.mBeginT; mNumS = src.mNumS; mNumT = src.mNumT; + mJoinsS = src.mJoinsS; + mJoinsT = src.mJoinsT; mExtents[0] = src.mExtents[0]; mExtents[1] = src.mExtents[1]; @@ -6373,6 +6375,58 @@ size_t LLVolumeFace::weldVertices(VertexData* destination_vertices, U32* destina namespace { + // Points closer than this are one place: far above the rounding that a + // closed loop carries from its first point around to its last, far below + // the step between any two points the generator means to be apart. + constexpr F32 SAME_PLACE = 1e-5f; + + bool same_place(const LLVector4a& a, const LLVector4a& b) + { + LLVector4a d; + d.setSub(a, b); + return d.dot3(d).getF32() <= SAME_PLACE * SAME_PLACE; + } + + // Whether every row of a grid has the same place in column a as in + // column b. + bool columns_meet(const LLVector4a* pos, S32 num_s, S32 num_t, S32 a, S32 b) + { + for (S32 t = 0; t < num_t; ++t) + { + if (!same_place(pos[num_s * t + a], pos[num_s * t + b])) + { + return false; + } + } + return true; + } + + // Whether every column of a grid has the same place in row a as in row b. + bool rows_meet(const LLVector4a* pos, S32 num_s, S32 a, S32 b) + { + for (S32 s = 0; s < num_s; ++s) + { + if (!same_place(pos[num_s * a + s], pos[num_s * b + s])) + { + return false; + } + } + return true; + } + + // Whether a column of a grid is one place in every row: a pole. + bool column_is_point(const LLVector4a* pos, S32 num_s, S32 num_t, S32 s) + { + for (S32 t = 1; t < num_t; ++t) + { + if (!same_place(pos[s], pos[num_s * t + s])) + { + return false; + } + } + return true; + } + // Adds a grid quad to its corners' normals. Its two triangles, (bl, br, // tr) and (bl, tr, tl), share the diagonal from bl to tr: each gives its // corner off the diagonal its cross product, twice its area along its @@ -6638,135 +6692,103 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) } } - // adjust normals based on wrapping and stitching - - LLVector4a top; - top.setSub(pos[0], pos[mNumS*(mNumT-2)]); - bool s_bottom_converges = (top.dot3(top) < 0.000001f); - - top.setSub(pos[mNumS-1], pos[mNumS*(mNumT-2)+mNumS-1]); - bool s_top_converges = (top.dot3(top) < 0.000001f); - - if (sculpt_stitching == LL_SCULPT_TYPE_NONE) // logic for non-sculpt volumes + // A side's normals join across an edge where its first and last columns + // are one seam, or its first and last rows: a prim's where they are one + // place in every row or column, which the face tells from its own + // positions and not from whether the profile or the path counts as + // closed; a sculpt's where its stitching samples them from one column + // or row of the map. + if (!partial_build) { - if (!volume->getPath().isOpen()) - { //wrap normals on T - for (S32 i = 0; i < mNumS; i++) - { - LLVector4a n; - n.setAdd(norm[i], norm[mNumS*(mNumT-1)+i]); - norm[i] = n; - norm[mNumS*(mNumT-1)+i] = n; - } - } - - if (!volume->getProfile().isOpen() && !s_bottom_converges) - { //wrap normals on S - for (S32 i = 0; i < mNumT; i++) - { - LLVector4a n; - n.setAdd(norm[mNumS*i], norm[mNumS*i+mNumS-1]); - norm[mNumS * i] = n; - norm[mNumS * i+mNumS-1] = n; - } + if (sculpt_stitching == LL_SCULPT_TYPE_NONE) + { + mJoinsS = columns_meet(pos, mNumS, mNumT, 0, mNumS - 1); + mJoinsT = rows_meet(pos, mNumS, 0, mNumT - 1); } - - if (volume->getPathType() == LL_PCODE_PATH_CIRCLE && - ((volume->getProfileType() & LL_PCODE_PROFILE_MASK) == LL_PCODE_PROFILE_CIRCLE_HALF)) + else { - if (s_bottom_converges) - { //all lower S have same normal - for (S32 i = 0; i < mNumT; i++) - { - norm[mNumS*i].set(1,0,0); - } - } - - if (s_top_converges) - { //all upper S have same normal - for (S32 i = 0; i < mNumT; i++) - { - norm[mNumS*i+mNumS-1].set(-1,0,0); - } - } + mJoinsS = sculpt_stitching == LL_SCULPT_TYPE_SPHERE || sculpt_stitching == LL_SCULPT_TYPE_TORUS || + sculpt_stitching == LL_SCULPT_TYPE_CYLINDER; + mJoinsT = sculpt_stitching == LL_SCULPT_TYPE_TORUS; } } - else // logic for sculpt volumes - { - bool average_poles = false; - bool wrap_s = false; - bool wrap_t = false; - - if (sculpt_stitching == LL_SCULPT_TYPE_SPHERE) - average_poles = true; - if ((sculpt_stitching == LL_SCULPT_TYPE_SPHERE) || - (sculpt_stitching == LL_SCULPT_TYPE_TORUS) || - (sculpt_stitching == LL_SCULPT_TYPE_CYLINDER)) - wrap_s = true; - - if (sculpt_stitching == LL_SCULPT_TYPE_TORUS) - wrap_t = true; + if (sculpt_stitching == LL_SCULPT_TYPE_SPHERE) + { + // average normals for north pole + LLVector4a average; + average.clear(); - if (average_poles) + for (S32 i = 0; i < mNumS; i++) { - // average normals for north pole - - LLVector4a average; - average.clear(); - - for (S32 i = 0; i < mNumS; i++) - { - average.add(norm[i]); - } - - // set average - for (S32 i = 0; i < mNumS; i++) - { - norm[i] = average; - } + average.add(norm[i]); + } - // average normals for south pole + // set average + for (S32 i = 0; i < mNumS; i++) + { + norm[i] = average; + } - average.clear(); + // average normals for south pole - for (S32 i = 0; i < mNumS; i++) - { - average.add(norm[i + mNumS * (mNumT - 1)]); - } + average.clear(); - // set average - for (S32 i = 0; i < mNumS; i++) - { - norm[i + mNumS * (mNumT - 1)] = average; - } + for (S32 i = 0; i < mNumS; i++) + { + average.add(norm[i + mNumS * (mNumT - 1)]); + } + // set average + for (S32 i = 0; i < mNumS; i++) + { + norm[i + mNumS * (mNumT - 1)] = average; } + } + if (mJoinsS) + { + for (S32 i = 0; i < mNumT; i++) + { + LLVector4a n; + n.setAdd(norm[mNumS*i], norm[mNumS*i+mNumS-1]); + norm[mNumS * i] = n; + norm[mNumS * i+mNumS-1] = n; + } + } - if (wrap_s) + if (mJoinsT) + { + for (S32 i = 0; i < mNumS; i++) { + LLVector4a n; + n.setAdd(norm[i], norm[mNumS*(mNumT-1)+i]); + norm[i] = n; + norm[mNumS*(mNumT-1)+i] = n; + } + } + + // On a sphere, a column that is one place in every row is a pole on the + // axis the half circle sweeps about, and takes the axis. + if (sculpt_stitching == LL_SCULPT_TYPE_NONE && 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++) { - LLVector4a n; - n.setAdd(norm[mNumS*i], norm[mNumS*i+mNumS-1]); - norm[mNumS * i] = n; - norm[mNumS * i+mNumS-1] = n; + norm[mNumS*i].set(1,0,0); } } - if (wrap_t) - { - for (S32 i = 0; i < mNumS; i++) + if (column_is_point(pos, mNumS, mNumT, mNumS - 1)) + { //all upper S have same normal + for (S32 i = 0; i < mNumT; i++) { - LLVector4a n; - n.setAdd(norm[i], norm[mNumS*(mNumT-1)+i]); - norm[i] = n; - norm[mNumS*(mNumT-1)+i] = n; + norm[mNumS*i+mNumS-1].set(-1,0,0); } } - } return true; diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 396ee64e1b..7084481846 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -941,6 +941,12 @@ class LLVolumeFace S32 mNumS; S32 mNumT; + // Whether a side's first and last columns, and its first and last rows, + // are one seam whose normals are joined. A full build decides them and a + // partial build keeps them. + bool mJoinsS = false; + bool mJoinsT = false; + LLVector4a* mExtents; //minimum and maximum point of face LLVector4a* mCenter; LLVector2 mTexCoordExtents[2]; //minimum and maximum of texture coordinates of the face. diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index a25c04f25c..a2ff197264 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -582,26 +582,12 @@ namespace namespace Regions { - // A side of a closed profile whose first and last columns are - // apart, as each side of a box is, and of more than two rows, where - // the S wrap runs: it averages the side's two edges together. - constexpr U32 OpenSWrap = 1 << 0; - // A side whose path's openness disagrees with its rows. A path - // that counts as closed but whose first and last rows are apart, a - // torus at one and a half turns, has the T wrap sum rows half a - // turn apart; one that counts as open but whose rows meet, a - // skewed circle2 path, is never wrapped. - constexpr U32 TWrapByOpenness = 1 << 1; // A cap with no area, which has no triangle to take a normal // from. constexpr U32 CapNoArea = 1 << 2; // A square hole above 0.707 in a round or triangular profile, // whose corners go through the wall. constexpr U32 SquareHole = 1 << 3; - // A smooth closed side of two rows: the S wrap's test for a pole - // compares the first row with itself and passes, so the seam is - // not wrapped. - constexpr U32 TwoRowSeam = 1 << 4; // A vertex in no triangle with area sums to no normal, or to the // rounding left of one, which createTangents' normalize turns into // NaN or infinity where its copies are exactly one point: the copied @@ -621,14 +607,6 @@ namespace U32 excusedBy(U32 regions) { U32 checks = 0; - if (regions & Regions::OpenSWrap) - { - checks |= Checks::Oracle | Checks::Inversion; - } - if (regions & Regions::TWrapByOpenness) - { - checks |= Checks::Oracle | Checks::Inversion | Checks::SeamsAgree | Checks::HardEdges; - } if (regions & Regions::CapNoArea) { checks |= Checks::Finite | Checks::RawUnit | Checks::CapFacing | Checks::Tangents | Checks::Orthogonal; @@ -637,10 +615,6 @@ namespace { checks |= Checks::Inversion | Checks::CapFacing | Checks::Oracle; } - if (regions & Regions::TwoRowSeam) - { - checks |= Checks::SeamsAgree; - } if (regions & Regions::TwistedCapHub) { checks |= Checks::AffineUV; @@ -700,23 +674,6 @@ namespace regions |= Regions::InsideOutCap; } } - else if (!isSculpt(volume) && isGrid(face)) - { - const bool profile_closed = !volume.getProfile().isOpen(); - const bool columns_meet = columnsMeet(face); - if (profile_closed && face.mNumT > 2 && !columns_meet) - { - regions |= Regions::OpenSWrap; - } - if (volume.getPath().isOpen() == rowsMeet(face)) - { - regions |= Regions::TWrapByOpenness; - } - if (columns_meet && face.mNumT == 2 && !isDoubled(face)) - { - regions |= Regions::TwoRowSeam; - } - } if (std::find(lone.begin(), lone.end(), true) != lone.end()) { regions |= Regions::ZeroNormal; @@ -2637,16 +2594,15 @@ namespace CHECK(report.failures == 0); } - // Every side of a closed profile has its first and last columns - // summed, so on a twisted box or tube each side's two edges take one - // averaged normal. A half cylinder's side, from one pole of its arc to - // the other, sums two opposite rims at LOD 2 and 3, as a skewed - // sphere's sums its poles, and their normals turn over. - TEST_CASE("llvolume: the S wrap joins only edges that meet", "[llvolume][!shouldfail]") + // A side's first and last columns are joined only where they are one + // place. A wrap by the profile's closing alone would average each side + // of a twisted box or tube with its own other edge, sum a half + // cylinder's two opposite rims at LOD 2 and 3, and a skewed sphere's + // two poles, and turn their normals over. + TEST_CASE("llvolume: the S wrap joins only edges that meet", "[llvolume]") { Inspect how; how.checks = Checks::Oracle | Checks::Inversion; - how.only_regions = Regions::OpenSWrap; Report report; S32 volumes = 0; inspectSweep(how, report, volumes); @@ -2664,15 +2620,14 @@ namespace CHECK(report.failures == 0); } - // The T wrap trusts the path's openness, which ignores revolutions: - // at one and a half turns the first and last rows are half a turn - // apart, and their normals are summed. The other way, a skewed circle2 - // path counts as open though its rows meet, and is never wrapped. - TEST_CASE("llvolume: the T wrap follows whether the first and last rows meet", "[llvolume][!shouldfail]") + // A path's openness ignores revolutions: at one and a half turns its + // first and last rows are half a turn apart, and a wrap by openness would + // sum their normals. The other way, a skewed circle2 path counts as open + // though its rows meet, and would never be wrapped. + TEST_CASE("llvolume: the T wrap follows whether the first and last rows meet", "[llvolume]") { Inspect how; how.checks = Checks::Oracle | Checks::Inversion | Checks::SeamsAgree | Checks::HardEdges; - how.only_regions = Regions::TWrapByOpenness; Report report; S32 volumes = 0; inspectSweep(how, report, volumes); @@ -2927,15 +2882,15 @@ namespace CHECK(report.failures == 0); } - // createSide takes a side's first column as a pole where it meets the - // row before the last; on a side of two rows that row is the first, so - // every closed side of two rows passes, and its seam is never wrapped: - // a cylinder's seam at LOD 0 and 1 is two one-sided normals. - TEST_CASE("llvolume: a closed side of two rows has one normal at its seam", "[llvolume][!shouldfail]") + // A pole test that compared a side's first row with the row before its + // last would compare the first row with itself on a side of two rows, + // take every such side's first column for a pole and leave its seam + // unwrapped: a cylinder's seam at LOD 0 and 1 would be two one-sided + // normals. + TEST_CASE("llvolume: a closed side of two rows has one normal at its seam", "[llvolume]") { Inspect how; how.checks = Checks::SeamsAgree; - how.only_regions = Regions::TwoRowSeam; Report report; S32 volumes = 0; inspectSweep(how, report, volumes); From e1098bee0bfb2498b475273059248c5cca78af73 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:52:38 -0400 Subject: [PATCH 03/16] A cap faces away from its wall whatever its first triangle is A cap took its one normal from the first triangle it emitted, and fell back to +/-Z in profile space when that had no area. A sliver or a flipped first triangle turned the whole cap, the fallback is wrong on any circular path, and the uncut cube cap of a box tapered to a point normalized a zero cross product into NaN. A cap's normal is now its end station's plane normal, row 2 of mRot, signed away from the side of the plane its neighbouring station's ring lies on. The path's direction alone cannot sign it: a twisted sphere's path does not move, and its caps faced into the sphere. Where the sign flips, the cap's winding flips with it. The uncut cube cap takes the same rule. A general cap's centre vertex took its position from the bounding box of its positions and its UV from the bounding box of its UVs, which a twist makes different points, sliding the texture up to 0.22 in UV. Its UV is now mapped from its own position. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 318 ++++++++++++++++----------- indra/llmath/llvolume.h | 6 + indra/llmath/tests/llvolume_test.cpp | 66 ++---- 3 files changed, 217 insertions(+), 173 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 3f6a5a6805..78c9cd7437 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -4543,6 +4543,7 @@ LLVolumeFace& LLVolumeFace::operator=(const LLVolumeFace& src) mNumT = src.mNumT; mJoinsS = src.mJoinsS; mJoinsT = src.mJoinsT; + mReversed = src.mReversed; mExtents[0] = src.mExtents[0]; mExtents[1] = src.mExtents[1]; @@ -5542,6 +5543,159 @@ void LLVolumeFace::swapData(LLVolumeFace& rhs) llswap(rhs.mNumIndices, mNumIndices); } +namespace +{ + // Points closer than this are one place: far above the rounding that a + // closed loop carries from its first point around to its last, far below + // the step between any two points the generator means to be apart. + constexpr F32 SAME_PLACE = 1e-5f; + + bool same_place(const LLVector4a& a, const LLVector4a& b) + { + LLVector4a d; + d.setSub(a, b); + return d.dot3(d).getF32() <= SAME_PLACE * SAME_PLACE; + } + + // Whether every row of a grid has the same place in column a as in + // column b. + bool columns_meet(const LLVector4a* pos, S32 num_s, S32 num_t, S32 a, S32 b) + { + for (S32 t = 0; t < num_t; ++t) + { + if (!same_place(pos[num_s * t + a], pos[num_s * t + b])) + { + return false; + } + } + return true; + } + + // Whether every column of a grid has the same place in row a as in row b. + bool rows_meet(const LLVector4a* pos, S32 num_s, S32 a, S32 b) + { + for (S32 s = 0; s < num_s; ++s) + { + if (!same_place(pos[num_s * a + s], pos[num_s * b + s])) + { + return false; + } + } + return true; + } + + // Whether a column of a grid is one place in every row: a pole. + bool column_is_point(const LLVector4a* pos, S32 num_s, S32 num_t, S32 s) + { + for (S32 t = 1; t < num_t; ++t) + { + if (!same_place(pos[s], pos[num_s * t + s])) + { + return false; + } + } + return true; + } + + // Adds a grid quad to its corners' normals. Its two triangles, (bl, br, + // tr) and (bl, tr, tl), share the diagonal from bl to tr: each gives its + // corner off the diagonal its cross product, twice its area along its + // normal, and each corner on the diagonal half of it. A parallelogram + // then gives its four corners one weight, so a vertex has as much from + // either side of it in every row, and no corner takes a normal from a + // triangle it is not in. Each cross product is taken at the corner off + // the diagonal: where a copied or collapsed row or column puts 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. + void add_quad_to_normals(LLVector4a* norm, const LLVector4a* pos, S32 bl, S32 br, S32 tl, S32 tr) + { + LLVector4a bottom, right, top, left; + bottom.setSub(pos[br], pos[bl]); + right.setSub(pos[tr], pos[br]); + top.setSub(pos[tr], pos[tl]); + left.setSub(pos[tl], pos[bl]); + + LLVector4a lower, upper; + lower.setCross3(bottom, right); + upper.setCross3(top, left); + + LLVector4a shared; + shared.setAdd(lower, upper); + shared.mul(0.5f); + + norm[bl].add(shared); + norm[br].add(lower); + norm[tr].add(shared); + norm[tl].add(upper); + } + + // The path station a cap closes: the last for a top cap, the first for a + // bottom one. + S32 cap_station(const LLVolume* volume, bool top) + { + return top ? (S32)volume->getPath().mPath.size() - 1 : 0; + } + + // A cap's normal: the normal of its station's profile plane, the third + // row of the station's rotation, which a taper to a point or a line + // cannot make degenerate as it does a cross product of the cap's own + // edges. A top cap's profile winds to face along that row and a bottom + // cap's against it. Where the ring of the station next to the cap lies + // on the side the winding faces, the cap faces the other way, and + // reversed is set: the end of a twisted sphere turns its half disc to + // the other side of the axis it sweeps about, and the sphere leaves the + // cap the other way. decide is false on a partial build, which keeps + // reversed as the full build left it. + LLVector4a cap_normal(const LLVolume* volume, bool top, bool decide, bool& reversed) + { + const LLAlignedArray& path = volume->getPath().mPath; + const S32 own = cap_station(volume, top); + LLVector4a normal = path[own].mRot.getRow<2>(); + normal.normalize3(); + if (!top) + { + normal.negate(); + } + + if (decide) + { + reversed = false; + const S32 count = (S32)path.size(); + if (count > 1) + { + const S32 next = top ? own - 1 : own + 1; + const S32 ring = volume->getProfile().getTotal(); + const LLAlignedArray& mesh = volume->getMesh(); + LLVector4a toward; + toward.clear(); + for (S32 i = 0; i < ring; ++i) + { + LLVector4a d; + d.setSub(mesh[next * ring + i], mesh[own * ring + i]); + toward.add(d); + } + reversed = toward.dot3(normal).getF32() > SAME_PLACE * ring; + } + } + + if (reversed) + { + normal.negate(); + } + return normal; + } + + // Turns every triangle of an index list over. + void reverse_winding(U16* indices, S32 count) + { + for (S32 i = 0; i + 2 < count; i += 3) + { + std::swap(indices[i + 1], indices[i + 2]); + } + } +} + void LerpPlanarVertex(LLVolumeFace::VertexData& v0, LLVolumeFace::VertexData& v1, LLVolumeFace::VertexData& v2, @@ -5599,20 +5753,9 @@ bool LLVolumeFace::createUnCutCubeCap(LLVolume* volume, bool partial_build) corners[t].mTexCoord.mV[1] = 0.5f - profile[grid_size*t][1]; } - { - LLVector4a lhs; - lhs.setSub(corners[1].getPosition(), corners[0].getPosition()); - LLVector4a rhs; - rhs.setSub(corners[2].getPosition(), corners[1].getPosition()); - baseVert.getNormal().setCross3(lhs, rhs); - baseVert.getNormal().normalize3fast(); - } + baseVert.setNormal(cap_normal(volume, mTypeMask & TOP_MASK, !partial_build, mReversed)); - if(!(mTypeMask & TOP_MASK)) - { - baseVert.getNormal().mul(-1.0f); - } - else + if (mTypeMask & TOP_MASK) { //Swap the UVs on the U(X) axis for top face LLVector2 swap; @@ -5696,6 +5839,11 @@ bool LLVolumeFace::createUnCutCubeCap(LLVolume* volume, bool partial_build) } } } + + if (mReversed) + { + reverse_winding(mIndices, mNumIndices); + } } return true; @@ -5756,9 +5904,6 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) offset = mBeginS; } - // Figure out the normal, assume all caps are flat faces. - // Cross product to get normals. - LLVector2 cuv; LLVector2 min_uv, max_uv; // VFExtents change @@ -5837,15 +5982,35 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) mCenter->setAdd(min, max); mCenter->mul(0.5f); - cuv = (min_uv + max_uv)*0.5f; - - - VertexData vd; - vd.setPosition(*mCenter); - vd.mTexCoord = cuv; - if (!(mTypeMask & HOLLOW_MASK) && !(mTypeMask & OPEN_MASK)) { + // The centre vertex takes the UV of its own position: the profile + // point its station's scale and rotation carry there. A twist turns + // the cap, and the middle of its positions' box is then not where + // the middle of its UVs' box maps. Along an axis the station scales + // to nothing, every point of the cap is one place, and the middle of + // the UVs serves. + cuv = (min_uv + max_uv)*0.5f; + + const LLPath::PathPt& station = volume->getPath().mPath[cap_station(volume, mTypeMask & TOP_MASK)]; + LLVector4a origin = station.mPos; + if (!origin.isFinite3()) + { + origin.clear(); + } + LLVector4a from_origin; + from_origin.setSub(*mCenter, origin); + const F32* scale = station.mScale.getF32ptr(); + if (scale[0] > SAME_PLACE) + { + cuv.mV[0] = from_origin.dot3(station.mRot.getRow<0>()).getF32() / scale[0] + 0.5f; + } + if (scale[1] > SAME_PLACE) + { + const F32 y = from_origin.dot3(station.mRot.getRow<1>()).getF32() / scale[1]; + cuv.mV[1] = (mTypeMask & TOP_MASK) ? y + 0.5f : 0.5f - y; + } + *pos++ = *mCenter; *tc++ = cuv; num_vertices++; @@ -6096,23 +6261,10 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) } - LLVector4a d0,d1; - d0.setSub(mPositions[mIndices[1]], mPositions[mIndices[0]]); - d1.setSub(mPositions[mIndices[2]], mPositions[mIndices[0]]); - - LLVector4a normal; - normal.setCross3(d0,d1); - - if (normal.dot3(normal).getF32() > F_APPROXIMATELY_ZERO) + const LLVector4a normal = cap_normal(volume, mTypeMask & TOP_MASK, !partial_build, mReversed); + if (mReversed) { - normal.normalize3fast(); - } - else - { //degenerate, make up a value - if(normal.getF32ptr()[2] >= 0) - normal.set(0.f,0.f,1.f); - else - normal.set(0.f,0.f,-1.f); + reverse_winding(mIndices, mNumIndices); } llassert(llfinite(normal.getF32ptr()[0])); @@ -6373,94 +6525,6 @@ size_t LLVolumeFace::weldVertices(VertexData* destination_vertices, U32* destina return unique; } -namespace -{ - // Points closer than this are one place: far above the rounding that a - // closed loop carries from its first point around to its last, far below - // the step between any two points the generator means to be apart. - constexpr F32 SAME_PLACE = 1e-5f; - - bool same_place(const LLVector4a& a, const LLVector4a& b) - { - LLVector4a d; - d.setSub(a, b); - return d.dot3(d).getF32() <= SAME_PLACE * SAME_PLACE; - } - - // Whether every row of a grid has the same place in column a as in - // column b. - bool columns_meet(const LLVector4a* pos, S32 num_s, S32 num_t, S32 a, S32 b) - { - for (S32 t = 0; t < num_t; ++t) - { - if (!same_place(pos[num_s * t + a], pos[num_s * t + b])) - { - return false; - } - } - return true; - } - - // Whether every column of a grid has the same place in row a as in row b. - bool rows_meet(const LLVector4a* pos, S32 num_s, S32 a, S32 b) - { - for (S32 s = 0; s < num_s; ++s) - { - if (!same_place(pos[num_s * a + s], pos[num_s * b + s])) - { - return false; - } - } - return true; - } - - // Whether a column of a grid is one place in every row: a pole. - bool column_is_point(const LLVector4a* pos, S32 num_s, S32 num_t, S32 s) - { - for (S32 t = 1; t < num_t; ++t) - { - if (!same_place(pos[s], pos[num_s * t + s])) - { - return false; - } - } - return true; - } - - // Adds a grid quad to its corners' normals. Its two triangles, (bl, br, - // tr) and (bl, tr, tl), share the diagonal from bl to tr: each gives its - // corner off the diagonal its cross product, twice its area along its - // normal, and each corner on the diagonal half of it. A parallelogram - // then gives its four corners one weight, so a vertex has as much from - // either side of it in every row, and no corner takes a normal from a - // triangle it is not in. Each cross product is taken at the corner off - // the diagonal: where a copied or collapsed row or column puts 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. - void add_quad_to_normals(LLVector4a* norm, const LLVector4a* pos, S32 bl, S32 br, S32 tl, S32 tr) - { - LLVector4a bottom, right, top, left; - bottom.setSub(pos[br], pos[bl]); - right.setSub(pos[tr], pos[br]); - top.setSub(pos[tr], pos[tl]); - left.setSub(pos[tl], pos[bl]); - - LLVector4a lower, upper; - lower.setCross3(bottom, right); - upper.setCross3(top, left); - - LLVector4a shared; - shared.setAdd(lower, upper); - shared.mul(0.5f); - - norm[bl].add(shared); - norm[br].add(lower); - norm[tr].add(shared); - norm[tl].add(upper); - } -} - bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 7084481846..83f2cffe44 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -947,6 +947,12 @@ class LLVolumeFace bool mJoinsS = false; bool mJoinsT = false; + // Whether a cap's triangles wind against the way its profile runs, so + // that it faces away from the wall it closes where that wall lies on the + // side its profile's winding faces. A full build decides it and a + // partial build keeps it. + bool mReversed = false; + LLVector4a* mExtents; //minimum and maximum point of face LLVector4a* mCenter; LLVector2 mTexCoordExtents[2]; //minimum and maximum of texture coordinates of the face. diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index a2ff197264..0cc31bd46a 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -582,8 +582,10 @@ namespace namespace Regions { - // A cap with no area, which has no triangle to take a normal - // from. + // A cap with no area. Where its path leaves it within its plane, its + // normal is far from the way its wall runs; and its tangent, from + // triangles with no area, is the one the tangent generator makes up, + // whatever the normal. constexpr U32 CapNoArea = 1 << 2; // A square hole above 0.707 in a round or triangular profile, // whose corners go through the wall. @@ -594,14 +596,6 @@ namespace // last column of a cut flat inner side, a half circle's square or // triangle hole on its axis, the middle of a sculpt's pinch. constexpr U32 ZeroNormal = 1 << 5; - // A cap with a centre vertex on a twisted path: the centre takes the - // middle of the cap's bounding box and the middle of its UVs', which - // a rotation makes two different points. - constexpr U32 TwistedCapHub = 1 << 6; - // A cap of a twisted sphere: the twist turns the half disc to the - // other side of the axis it sweeps about, which reverses the way - // the sphere leaves the cap, but not the cap. - constexpr U32 InsideOutCap = 1 << 7; } U32 excusedBy(U32 regions) @@ -609,20 +603,12 @@ namespace U32 checks = 0; if (regions & Regions::CapNoArea) { - checks |= Checks::Finite | Checks::RawUnit | Checks::CapFacing | Checks::Tangents | Checks::Orthogonal; + checks |= Checks::CapFacing | Checks::Orthogonal; } if (regions & Regions::SquareHole) { checks |= Checks::Inversion | Checks::CapFacing | Checks::Oracle; } - if (regions & Regions::TwistedCapHub) - { - checks |= Checks::AffineUV; - } - if (regions & Regions::InsideOutCap) - { - checks |= Checks::CapFacing; - } return checks; } @@ -654,25 +640,13 @@ namespace const LLVolumeParams& params = volume.getParams(); const U8 profile = params.getProfileParams().getCurveType() & LL_PCODE_PROFILE_MASK; const U8 hole = params.getProfileParams().getCurveType() & LL_PCODE_HOLE_MASK; - const bool twisted = params.getPathParams().getTwistBegin() != params.getPathParams().getTwist(); if (!isSculpt(volume) && hole == LL_PCODE_HOLE_SQUARE && params.getHollow() > 0.70711f && isRoundOrTriangle(profile)) { regions |= Regions::SquareHole; } - if (isCap(face)) + if (isCap(face) && std::all_of(triangles.begin(), triangles.end(), [](const Triangle& t) { return t.degenerate; })) { - if (std::all_of(triangles.begin(), triangles.end(), [](const Triangle& t) { return t.degenerate; })) - { - regions |= Regions::CapNoArea; - } - if (twisted && !(face.mTypeMask & (LLVolumeFace::HOLLOW_MASK | LLVolumeFace::OPEN_MASK))) - { - regions |= Regions::TwistedCapHub; - } - if (twisted && profile == LL_PCODE_PROFILE_CIRCLE_HALF && params.getPathParams().getCurveType() != LL_PCODE_PATH_LINE) - { - regions |= Regions::InsideOutCap; - } + regions |= Regions::CapNoArea; } if (std::find(lone.begin(), lone.end(), true) != lone.end()) { @@ -2645,9 +2619,11 @@ namespace CHECK(report.failures == 0); } - // A cap's normal is the first triangle's, with +/-Z when that has - // no area; the uncut cube cap normalizes a zero cross product. A cap - // with no area, as on a pyramid or a prism, still faces along the path. + // A cap with no area, as on a pyramid or a prism, still faces along the + // path. A cap's normal is its station's plane's, which a circle2 path's + // alternating stations, and a half circle on a circle path tapered to a + // point, leave the cap at 70 to 90 degrees from: their walls run off + // within the plane, not along its normal. TEST_CASE("llvolume: a cap with no area still faces away from its wall", "[llvolume][!shouldfail]") { Inspect how; @@ -2899,14 +2875,13 @@ namespace } // A general cap's centre vertex takes the middle of the cap's bounding - // box for its position and the middle of the UVs' box for its UV. A - // twist rotates the cap, and the two are no longer one point: the - // texture slides at the centre of every twisted cap. - TEST_CASE("llvolume: a twisted cap's centre vertex has the UV of its position", "[llvolume][!shouldfail]") + // box for its position. A twist rotates the cap, and the middle of the + // UVs' box is then another point: a centre that took that UV would + // slide the texture at the centre of every twisted cap. + TEST_CASE("llvolume: a twisted cap's centre vertex has the UV of its position", "[llvolume]") { Inspect how; how.checks = Checks::AffineUV; - how.only_regions = Regions::TwistedCapHub; Report report; S32 volumes = 0; inspectSweep(how, report, volumes); @@ -2916,13 +2891,12 @@ namespace // A sphere's half disc sweeps about the axis it touches. A twist turns // it about its own centre on that axis, to the other side of the axis, - // and the sphere then leaves the cap the other way; the cap's winding - // and normal stay, and the cap faces into the sphere. - TEST_CASE("llvolume: a twisted sphere's caps face out of the sphere", "[llvolume][!shouldfail]") + // and the sphere then leaves the cap the other way: a cap that kept the + // winding and normal its profile gives it would face into the sphere. + TEST_CASE("llvolume: a twisted sphere's caps face out of the sphere", "[llvolume]") { Inspect how; - how.checks = Checks::CapFacing; - how.only_regions = Regions::InsideOutCap; + how.checks = Checks::CapFacing | Checks::Inversion; Report report; S32 volumes = 0; inspectSweep(how, report, volumes); From 8c1037bbc43ac7802d0e724c22db771490828910 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:52:53 -0400 Subject: [PATCH 04/16] Prim normals come out of generation unit length, never zero A side's normals were area sums, made unit length only when tangents were generated, so a prim without tangents put unnormalized normals in its vertex buffer (box sides came out at length 2), and Lengyel's Gram-Schmidt step ran on them before they were normalized, leaving tangents up to 90 degrees off their normals on twisted sides. A vertex whose triangles all had no area kept a zero normal, which the normalize turned into NaN: the copied last column of a cut flat inner side, half circles with square or triangle holes, a sculpt pinch's interior, the empty placeholder. normalize_grid_normals brings every normal to unit length in the accumulation's last pass. A vertex with no normal of its own takes the nearest one up or down its column, then along its row, then around it, only ever one a vertex had of its own. An edge shorter than rounding, as a closed profile's last point to its first, counts as no length, so lone copies no longer take a direction from noise. createTangents no longer normalizes; the mesh path over 65535 vertices normalizes for itself. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 144 ++++++++++++++++++++++-- indra/llmath/tests/llvolume_test.cpp | 162 +++++++++++++++++---------- 2 files changed, 235 insertions(+), 71 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 78c9cd7437..85a824eea7 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -5423,7 +5423,12 @@ bool LLVolumeFace::cacheOptimize(bool gen_tangents) { LL_WARNS_ONCE("LLVOLUME") << "meshopt_generateVertexRemapMulti failed to process a model or model was invalid" << LL_ENDL; } - // blew past the max vertex size limit, use legacy tangent generation which never adds verts + // blew past the max vertex size limit, use legacy tangent generation which never adds verts; + // it takes unit normals, as the bump map and planar projection code do + for (S32 i = 0; i < mNumVertices; i++) + { + mNormals[i].normalize3fast(); + } createTangents(); } } @@ -5606,7 +5611,9 @@ namespace // triangle it is not in. Each cross product is taken at the corner off // the diagonal: where a copied or collapsed row or column puts 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 + // product is exactly zero, fused multiply-add or not. An edge between + // two points that are one place but for rounding, as a closed profile's + // last point and its first, counts as having none. A quad between a // copied row or column adds nothing, so the edge there stays hard. void add_quad_to_normals(LLVector4a* norm, const LLVector4a* pos, S32 bl, S32 br, S32 tl, S32 tr) { @@ -5616,6 +5623,15 @@ namespace top.setSub(pos[tr], pos[tl]); left.setSub(pos[tl], pos[bl]); + const LLVector4a zero = LLVector4a::getZero(); + const LLVector4a same_place(SAME_PLACE * SAME_PLACE); + for (LLVector4a* edge : { &bottom, &right, &top, &left }) + { + LLVector4a length; + length.setAllDot3(*edge, *edge); + edge->setSelectWithMask(length.lessThan(same_place), zero, *edge); + } + LLVector4a lower, upper; lower.setCross3(bottom, right); upper.setCross3(top, left); @@ -5630,6 +5646,121 @@ namespace norm[tl].add(upper); } + // Brings every normal of a grid to unit length. A vertex whose quads all + // have no area, as a copy that closes a cut flat inner side or a row in + // the middle of a sculpt's pinch, has no normal of its own, and takes + // the nearest one up or down its column, or, where the whole column has + // none, along its row, or, where neither has one, as where a copied + // column crosses a copied row, the nearest around it: only a normal a + // vertex had of its own, so that what one such vertex takes does not + // pass on to the next. A grid with no normal at all faces +Z. While they + // wait, such vertices are marked with a w of 1, which every normal of + // its own has as 0. + void normalize_grid_normals(LLVector4a* norm, S32 num_s, S32 num_t) + { + const S32 count = num_s * num_t; + bool any_missing = false; + for (S32 v = 0; v < count; ++v) + { + if (norm[v].dot3(norm[v]).getF32() > FLT_MIN) + { + norm[v].normalize3(); + } + else + { + norm[v].set(0.f, 0.f, 0.f, 1.f); + any_missing = true; + } + } + + if (!any_missing) + { + return; + } + + auto own = [](const LLVector4a& n) { return n[3] == 0.f; }; + if (std::none_of(norm, norm + count, own)) + { + for (S32 v = 0; v < count; ++v) + { + norm[v].set(0.f, 0.f, 1.f, 0.f); + } + return; + } + + for (S32 t = 0; t < num_t; ++t) + { + for (S32 s = 0; s < num_s; ++s) + { + LLVector4a& n = norm[s + num_s * t]; + if (own(n)) + { + continue; + } + const LLVector4a* donor = nullptr; + for (S32 d = 1; d < num_t && !donor; ++d) + { + for (S32 row : { t - d, t + d }) + { + if (row >= 0 && row < num_t && own(norm[s + num_s * row])) + { + donor = &norm[s + num_s * row]; + break; + } + } + } + for (S32 d = 1; d < num_s && !donor; ++d) + { + for (S32 column : { s - d, s + d }) + { + if (column >= 0 && column < num_s && own(norm[column + num_s * t])) + { + donor = &norm[column + num_s * t]; + break; + } + } + } + // 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) + { + for (S32 row = llmax(t - d, 0); row <= llmin(t + d, num_t - 1) && !donor; ++row) + { + if (row == t - d || row == t + d) + { + 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; + } + } + } + } + } + n = donor ? *donor : LLVector4a(0.f, 0.f, 1.f); + n.getF32ptr()[3] = 1.f; + } + } + + for (S32 v = 0; v < count; ++v) + { + norm[v].getF32ptr()[3] = 0.f; + } + } + // The path station a cap closes: the last for a top cap, the first for a // bottom one. S32 cap_station(const LLVolume* volume, bool top) @@ -6301,13 +6432,6 @@ void LLVolumeFace::createTangents() } LLCalculateTangentArray(mNumVertices, mPositions, mNormals, mTexCoords, mNumIndices / 3, mIndices, mTangents); - - //normalize normals - for (S32 i = 0; i < mNumVertices; i++) - { - //bump map/planar projection code requires normals to be normalized - mNormals[i].normalize3fast(); - } } } @@ -6855,6 +6979,8 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) } } + normalize_grid_normals(norm, mNumS, mNumT); + return true; } diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 0cc31bd46a..c246cd91a0 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -573,29 +573,23 @@ namespace constexpr U32 HardEdges = 1 << 8; // a flat inner side is hard at every point constexpr U32 Tangents = 1 << 9; // after genTangents: unit normals, unit tangents, w = +/-1 constexpr U32 Orthogonal = 1 << 10; // after genTangents: tangent orthogonal to normal + constexpr U32 LoneNormals = 1 << 11; // a vertex in no triangle with area takes a normal the face has - // What the sweep holds every face to. RawUnit is broken on every - // side today, and Orthogonal on most curved ones: each has a case of - // its own. - constexpr U32 Sweep = Layout | Finite | Inversion | CapFacing | AffineUV | Oracle | SeamsAgree | HardEdges | Tangents; + // What the sweep holds every face to. Orthogonal is broken wherever + // the tangent generator makes a tangent up, and has a case of its + // own. + constexpr U32 Sweep = + Layout | Finite | RawUnit | Inversion | CapFacing | AffineUV | Oracle | SeamsAgree | HardEdges | Tangents | LoneNormals; } namespace Regions { // A cap with no area. Where its path leaves it within its plane, its - // normal is far from the way its wall runs; and its tangent, from - // triangles with no area, is the one the tangent generator makes up, - // whatever the normal. + // normal is far from the way its wall runs. constexpr U32 CapNoArea = 1 << 2; // A square hole above 0.707 in a round or triangular profile, // whose corners go through the wall. constexpr U32 SquareHole = 1 << 3; - // A vertex in no triangle with area sums to no normal, or to the - // rounding left of one, which createTangents' normalize turns into - // NaN or infinity where its copies are exactly one point: the copied - // last column of a cut flat inner side, a half circle's square or - // triangle hole on its axis, the middle of a sculpt's pinch. - constexpr U32 ZeroNormal = 1 << 5; } U32 excusedBy(U32 regions) @@ -603,7 +597,7 @@ namespace U32 checks = 0; if (regions & Regions::CapNoArea) { - checks |= Checks::CapFacing | Checks::Orthogonal; + checks |= Checks::CapFacing; } if (regions & Regions::SquareHole) { @@ -634,7 +628,7 @@ namespace return (volume.getParams().getSculptType() & LL_SCULPT_TYPE_MASK) != LL_SCULPT_TYPE_NONE; } - U32 regionsOf(const LLVolume& volume, const LLVolumeFace& face, const std::vector& triangles, const std::vector& lone) + U32 regionsOf(const LLVolume& volume, const LLVolumeFace& face, const std::vector& triangles) { U32 regions = 0; const LLVolumeParams& params = volume.getParams(); @@ -648,10 +642,6 @@ namespace { regions |= Regions::CapNoArea; } - if (std::find(lone.begin(), lone.end(), true) != lone.end()) - { - regions |= Regions::ZeroNormal; - } return regions; } @@ -735,16 +725,11 @@ namespace } } - // A vertex in skip is left to the ZeroNormal region's case. - void checkRawUnit(const LLVolumeFace& face, const std::vector& normals, const std::vector& skip, Report& report, const std::string& where) + void checkRawUnit(const LLVolumeFace& face, const std::vector& normals, Report& report, const std::string& where) { for (S32 v = 0; v < face.mNumVertices; ++v) { const F32 length = normals[v].length(); - if (skip[v]) - { - continue; - } if (!(fabsf(length - 1.f) <= UNIT_TOLERANCE)) { report.fail(fmt::format("{}: vertex {} has a normal of length {}", where, v, length)); @@ -1007,8 +992,8 @@ namespace // Where a side's first and last columns are one point, it is one // smooth surface across them, and both take one normal; so with the // rows of a closed path. - void checkSeamsAgree(const LLVolume& volume, const LLVolumeFace& face, const std::vector& normals, const std::vector& skip, - Report& report, const std::string& where) + void checkSeamsAgree(const LLVolume& volume, const LLVolumeFace& face, const std::vector& normals, Report& report, + const std::string& where) { if (!isGrid(face) || isDoubled(face)) { @@ -1021,7 +1006,7 @@ namespace auto agree = [&](S32 a, S32 b, F32& off) { off = degreesBetween(normals[a], normals[b]); - return skip[a] || skip[b] || off <= 1.f; + return off <= 1.f; }; if (wraps_s && columnsMeet(face)) { @@ -1054,19 +1039,22 @@ namespace // profile is cut), so the quads after even columns are the real ones // and the quads after odd columns have no width: a vertex takes its // normal from the one strip of real quads beside it, up and down the - // path, and from no other strip at its point. + // path, and from no other strip at its point. A vertex in no triangle + // with area, as the copies that close the side or an apex's copies, + // is beside no strip. S32 stripOf(S32 column) { return column % 2 == 0 ? column : column - 1; } - void checkHardEdges(const LLVolumeFace& face, const std::vector& triangles, const std::vector& normals, - const std::vector& skip, Report& report, const std::string& where) + void checkHardEdges(const LLVolumeFace& face, const std::vector& triangles, const std::vector& normals, Report& report, + const std::string& where) { if (!isDoubled(face) || !isGrid(face)) { return; } + const std::vector lone = loneVertices(face, triangles); const std::vector groups = weldGroups(face); std::map, std::vector> in_strip; for (size_t t = 0; t < triangles.size(); ++t) @@ -1088,7 +1076,7 @@ namespace for (S32 v = 0; v < face.mNumVertices; ++v) { const S32 column = v % face.mNumS; - if (skip[v]) + if (lone[v]) { continue; } @@ -1117,7 +1105,56 @@ namespace } } - void checkTangents(const LLVolumeFace& face, const std::vector& skip, U32 checks, Report& report, const std::string& where) + // A side's vertex in no triangle with area has no normal of its own, and + // takes the normal of a vertex that has one: never none, never what + // rounding leaves, and never a made-up one while the face has any. A + // sphere's pole, a column that is one place in every row, takes the + // axis instead. + void checkLoneNormals(const LLVolume& volume, const LLVolumeFace& face, const std::vector& triangles, const std::vector& normals, + Report& report, const std::string& where) + { + if (!isGrid(face)) + { + return; + } + const bool sphere = !isSculpt(volume) && (volume.getParams().getProfileParams().getCurveType() & LL_PCODE_PROFILE_MASK) == LL_PCODE_PROFILE_CIRCLE_HALF && + volume.getParams().getPathParams().getCurveType() == LL_PCODE_PATH_CIRCLE; + auto pole = [&](S32 column) + { + for (S32 t = 1; t < face.mNumT; ++t) + { + if (!coincide(face.mPositions[gridIndex(face, column, 0)], face.mPositions[gridIndex(face, column, t)])) + { + return false; + } + } + return true; + }; + const std::vector lone = loneVertices(face, triangles); + std::vector donors; + for (S32 v = 0; v < face.mNumVertices; ++v) + { + if (!lone[v]) + { + donors.push_back(v); + } + } + if (donors.empty()) + { + return; + } + for (S32 v = 0; v < face.mNumVertices; ++v) + { + if (lone[v] && !(sphere && pole(v % face.mNumS)) && + std::none_of(donors.begin(), donors.end(), [&](S32 d) { return degreesBetween(normals[v], normals[d]) <= 0.1f; })) + { + report.fail(fmt::format("{}: vertex {} (column {}, row {}) has none of the face's normals", where, v, v % face.mNumS, v / face.mNumS)); + return; + } + } + } + + void checkTangents(const LLVolumeFace& face, U32 checks, Report& report, const std::string& where) { if (!face.mTangents) { @@ -1128,10 +1165,6 @@ namespace bool orthogonal_failed = false; for (S32 v = 0; v < face.mNumVertices; ++v) { - if (skip[v]) - { - continue; - } const LLVector3 n = v3(face.mNormals[v]); const LLVector3 t = v3(face.mTangents[v]); const F32 w = face.mTangents[v][3]; @@ -1161,7 +1194,8 @@ namespace } // Every check the inspection asks for, on every face of the volume. - // Tangents come last: genTangents normalizes the normals it is given. + // Tangents come last, from genTangents, once every face's normals have + // been checked as generation left them. void inspect(LLVolume& volume, const std::string& name, const Inspect& how, Report& report) { const S32 faces = volume.getNumVolumeFaces(); @@ -1171,7 +1205,6 @@ namespace U32 checks = 0; U32 regions = 0; std::vector raw; - std::vector skip; }; std::vector states(faces); for (S32 i = 0; i < faces; ++i) @@ -1183,9 +1216,8 @@ namespace continue; } const std::vector triangles = trianglesOf(face); - const std::vector lone = loneVertices(face, triangles); State& state = states[i]; - state.regions = regionsOf(volume, face, triangles, lone); + state.regions = regionsOf(volume, face, triangles); if (how.only_regions && !(state.regions & how.only_regions)) { continue; @@ -1199,9 +1231,6 @@ namespace { state.raw[v] = v3(face.mNormals[v]); } - // The ZeroNormal region is of vertices, not faces: everything - // else passes over those vertices alone. - state.skip = (how.only_regions & Regions::ZeroNormal) ? std::vector(face.mNumVertices, false) : lone; if (state.checks & Checks::Finite) { @@ -1209,7 +1238,7 @@ namespace } if (state.checks & Checks::RawUnit) { - checkRawUnit(face, state.raw, state.skip, report, where); + checkRawUnit(face, state.raw, report, where); } if (state.checks & Checks::Inversion) { @@ -1230,11 +1259,15 @@ namespace } if (state.checks & Checks::SeamsAgree) { - checkSeamsAgree(volume, face, state.raw, state.skip, report, where); + checkSeamsAgree(volume, face, state.raw, report, where); } if (state.checks & Checks::HardEdges) { - checkHardEdges(face, triangles, state.raw, state.skip, report, where); + checkHardEdges(face, triangles, state.raw, report, where); + } + if (state.checks & Checks::LoneNormals) + { + checkLoneNormals(volume, face, triangles, state.raw, report, where); } } @@ -1247,7 +1280,7 @@ namespace } volume.genTangents(i); const LLVolumeFace& face = volume.getVolumeFace(i); - checkTangents(face, state.skip, state.checks, report, fmt::format("{}: {}", name, faceName(face))); + checkTangents(face, state.checks, report, fmt::format("{}: {}", name, faceName(face))); } } @@ -2658,9 +2691,12 @@ namespace CHECK(report.failures == 0); } - // Lengyel's Gram-Schmidt step runs on the normals before - // createTangents normalizes them, removing only |n|^2 of the normal's - // part, so a tangent leans out of the plane its normal makes. + // Lengyel's Gram-Schmidt step removes only |n|^2 of the normal's part, + // so it needs unit normals, or a tangent leans out of the plane its + // normal makes. Where a vertex's triangles give its tangent no + // direction, it makes one up, +Z whatever the normal: at a cap with no + // area, a vertex in no triangle with area, and the point where a + // twisted half circle's pole crosses the axis it sweeps about. TEST_CASE("llvolume: every prim tangent is orthogonal to its normal", "[llvolume][!shouldfail]") { Inspect how; @@ -2713,9 +2749,10 @@ namespace } } - // A side's normals are area sums, and are unit length only once - // createTangents has normalized them. - TEST_CASE("llvolume: normals are unit length straight out of generation", "[llvolume][!shouldfail]") + // A side's normals are sums over the quads around each vertex, and must + // leave the build unit length: the vertex buffer takes them as they are + // where a face has no tangents. + TEST_CASE("llvolume: normals are unit length straight out of generation", "[llvolume]") { Inspect how; how.checks = Checks::RawUnit; @@ -2918,16 +2955,17 @@ namespace CHECK(report.failures == 0); } - // A vertex whose every triangle has no area sums to a zero normal, and - // createTangents' normalize3fast makes it NaN: the copied last column - // of a cut flat inner side, the inner side of a half circle with a - // square or triangle hole, the middle of a sculpt's pinch, the whole of - // the empty placeholder. - TEST_CASE("llvolume: a vertex with only zero-area triangles gets a finite normal", "[llvolume][!shouldfail]") + // A vertex whose every triangle has no area sums to a zero normal, which + // a normalize makes NaN: the copied last column of a cut flat inner + // side, the inner side of a half circle with a square or triangle hole, + // the middle of a sculpt's pinch, the whole of the empty placeholder. + // It takes a neighbour's normal instead; where a copy is one place with + // its neighbour only to rounding, the edge between them must count as + // having no length, or it gives the copy a normal of rounding. + TEST_CASE("llvolume: a vertex with only zero-area triangles takes a neighbour's normal", "[llvolume]") { Inspect how; - how.checks = Checks::Tangents; - how.only_regions = Regions::ZeroNormal; + how.checks = Checks::Tangents | Checks::LoneNormals; Report report; S32 volumes = 0; inspectSweep(how, report, volumes); From 2e7574fcb429b90f869c9b684c784b7777cd9c9f Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:53:12 -0400 Subject: [PATCH 05/16] A flexible prim's partial build rewrites its faces in place, tangents included Flexible prims rebuild their volume every frame and keep its topology, so the faces are meant to rewrite positions, normals and UVs in place. Over the years the faces stopped agreeing on it: createSide fell back to a full build when its counts grew but not when they shrank (MAINT-4435), createCap's guards were commented out (MAINT-5042), so general caps reallocated their vertices and indices every frame and the uncut cube cap its vertices, and a side kept mTangents through a partial build, so createTangents returned early and the side's tangents stayed at the first frame's pose while the caps' were rebuilt. createVolumeFaces now decides once: a build is partial only when every face's type mask, profile range and path length match the last one. Every face then writes into the buffers it has and keeps its indices, with no allocation, and computes its normals on that path as on the full one. A partial build marks the tangents stale, and genTangents regenerates them in the buffer they already have. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 143 ++++++++++++++++----------- indra/llmath/llvolume.h | 4 + indra/llmath/tests/llvolume_test.cpp | 14 +-- 3 files changed, 96 insertions(+), 65 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 85a824eea7..528fbe85bb 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -2841,69 +2841,67 @@ void LLVolume::createVolumeFaces() } else { + // A partial build rewrites each face's vertices in the buffers it + // has and keeps its indices, so it is made only where every face is + // laid out as the last build laid it out: the same type, profile + // range and path length, and so the same counts. Anything else is a + // full build of every face. S32 num_faces = getNumFaces(); - bool partial_build = true; - if (num_faces != mVolumeFaces.size()) - { - partial_build = false; - mVolumeFaces.resize(num_faces); - } + bool partial_build = num_faces == (S32)mVolumeFaces.size(); + mVolumeFaces.resize(num_faces); + // Initialize volume faces with parameter data for (S32 i = 0; i < (S32)mVolumeFaces.size(); i++) { LLVolumeFace& vf = mVolumeFaces[i]; LLProfile::Face& face = mProfilep->mFaces[i]; - vf.mBeginS = face.mIndex; - vf.mNumS = face.mCount; - if (vf.mNumS < 0) + S32 num_s = face.mCount; + if (num_s < 0) { LL_ERRS() << "Volume face corruption detected." << LL_ENDL; } - vf.mBeginT = 0; - vf.mNumT= getPath().mPath.size(); - vf.mID = i; - // Set the type mask bits correctly + U32 type_mask = 0; if (mParams.getProfileParams().getHollow() > 0) { - vf.mTypeMask |= LLVolumeFace::HOLLOW_MASK; + type_mask |= LLVolumeFace::HOLLOW_MASK; } if (mProfilep->isOpen()) { - vf.mTypeMask |= LLVolumeFace::OPEN_MASK; + type_mask |= LLVolumeFace::OPEN_MASK; } if (face.mCap) { - vf.mTypeMask |= LLVolumeFace::CAP_MASK; + type_mask |= LLVolumeFace::CAP_MASK; if (face.mFaceID == LL_FACE_PATH_BEGIN) { - vf.mTypeMask |= LLVolumeFace::TOP_MASK; + type_mask |= LLVolumeFace::TOP_MASK; } else { llassert(face.mFaceID == LL_FACE_PATH_END); - vf.mTypeMask |= LLVolumeFace::BOTTOM_MASK; + type_mask |= LLVolumeFace::BOTTOM_MASK; } } else if (face.mFaceID & (LL_FACE_PROFILE_BEGIN | LL_FACE_PROFILE_END)) { - vf.mTypeMask |= LLVolumeFace::FLAT_MASK | LLVolumeFace::END_MASK; + type_mask |= LLVolumeFace::FLAT_MASK | LLVolumeFace::END_MASK; } else { - vf.mTypeMask |= LLVolumeFace::SIDE_MASK; + type_mask |= LLVolumeFace::SIDE_MASK; if (face.mFlat) { - vf.mTypeMask |= LLVolumeFace::FLAT_MASK; + type_mask |= LLVolumeFace::FLAT_MASK; } if (face.mFaceID & LL_FACE_INNER_SIDE) { - vf.mTypeMask |= LLVolumeFace::INNER_MASK; - if (face.mFlat && vf.mNumS > 2) + type_mask |= LLVolumeFace::INNER_MASK; + if (face.mFlat && num_s > 2) { //flat inner faces have to copy vert normals - vf.mNumS = vf.mNumS*2; - if (vf.mNumS < 0) + num_s = num_s*2; + if (num_s < 0) { LL_ERRS() << "Volume face corruption detected." << LL_ENDL; } @@ -2911,9 +2909,22 @@ void LLVolume::createVolumeFaces() } else { - vf.mTypeMask |= LLVolumeFace::OUTER_MASK; + type_mask |= LLVolumeFace::OUTER_MASK; } } + + const S32 num_t = getPath().mPath.size(); + if (vf.mTypeMask != type_mask || vf.mBeginS != face.mIndex || vf.mNumS != num_s || vf.mNumT != num_t) + { + partial_build = false; + } + + vf.mTypeMask = type_mask; + vf.mBeginS = face.mIndex; + vf.mNumS = num_s; + vf.mBeginT = 0; + vf.mNumT = num_t; + vf.mID = i; } for (face_list_t::iterator iter = mVolumeFaces.begin(); @@ -4578,6 +4589,7 @@ LLVolumeFace& LLVolumeFace::operator=(const LLVolumeFace& src) { allocateTangents(src.mNumVertices); LLVector4a::memcpyNonAliased16((F32*) mTangents, (F32*) src.mTangents, vert_size); + mTangentsStale = src.mTangentsStale; } else { @@ -4676,6 +4688,10 @@ bool LLVolumeFace::create(LLVolume* volume, bool partial_build) //tree for this face is no longer valid destroyOctree(); + // A partial build moves the positions under the tangents and keeps their + // buffer; a full build frees it. + mTangentsStale = partial_build; + bool ret = false ; if (mTypeMask & CAP_MASK) { @@ -5542,6 +5558,7 @@ void LLVolumeFace::swapData(LLVolumeFace& rhs) llswap(rhs.mPositions, mPositions); llswap(rhs.mNormals, mNormals); llswap(rhs.mTangents, mTangents); + llswap(rhs.mTangentsStale, mTangentsStale); llswap(rhs.mTexCoords, mTexCoords); llswap(rhs.mIndices,mIndices); llswap(rhs.mNumVertices, mNumVertices); @@ -5898,8 +5915,10 @@ bool LLVolumeFace::createUnCutCubeCap(LLVolume* volume, bool partial_build) corners[2].mTexCoord=swap; } - S32 size = (grid_size+1)*(grid_size+1); - resizeVertices(size); + if (!partial_build) + { + resizeVertices((grid_size+1)*(grid_size+1)); + } LLVector4a* pos = (LLVector4a*) mPositions; LLVector4a* norm = (LLVector4a*) mNormals; @@ -6002,20 +6021,16 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) num_vertices = profile.size(); num_indices = (profile.size() - 2)*3; - if (!(mTypeMask & HOLLOW_MASK) && !(mTypeMask & OPEN_MASK)) + if (!partial_build) { - resizeVertices(num_vertices+1); - - //if (!partial_build) + if (!(mTypeMask & HOLLOW_MASK) && !(mTypeMask & OPEN_MASK)) { + resizeVertices(num_vertices+1); resizeIndices(num_indices+3); } - } - else - { - resizeVertices(num_vertices); - //if (!partial_build) + else { + resizeVertices(num_vertices); resizeIndices(num_indices); } } @@ -6147,10 +6162,26 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) num_vertices++; } - //if (partial_build) - //{ - // return true; - //} + const LLVector4a normal = cap_normal(volume, mTypeMask & TOP_MASK, !partial_build, mReversed); + + llassert(llfinite(normal.getF32ptr()[0])); + llassert(llfinite(normal.getF32ptr()[1])); + llassert(llfinite(normal.getF32ptr()[2])); + + llassert(!llisnan(normal.getF32ptr()[0])); + llassert(!llisnan(normal.getF32ptr()[1])); + llassert(!llisnan(normal.getF32ptr()[2])); + + for (S32 i = 0; i < num_vertices; i++) + { + norm[i].load4a(normal.getF32ptr()); + } + + // A partial build keeps the indices it has. + if (partial_build) + { + return true; + } if (mTypeMask & HOLLOW_MASK) { @@ -6392,25 +6423,11 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) } - const LLVector4a normal = cap_normal(volume, mTypeMask & TOP_MASK, !partial_build, mReversed); if (mReversed) { reverse_winding(mIndices, mNumIndices); } - llassert(llfinite(normal.getF32ptr()[0])); - llassert(llfinite(normal.getF32ptr()[1])); - llassert(llfinite(normal.getF32ptr()[2])); - - llassert(!llisnan(normal.getF32ptr()[0])); - llassert(!llisnan(normal.getF32ptr()[1])); - llassert(!llisnan(normal.getF32ptr()[2])); - - for (S32 i = 0; i < num_vertices; i++) - { - norm[i].load4a(normal.getF32ptr()); - } - return true; } @@ -6418,9 +6435,13 @@ void LLVolumeFace::createTangents() { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; - if (!mTangents) + if (!mTangents || mTangentsStale) { - allocateTangents(mNumVertices); + if (!mTangents) + { + allocateTangents(mNumVertices); + } + mTangentsStale = false; //generate tangents LLVector4a* ptr = (LLVector4a*)mTangents; @@ -6675,7 +6696,13 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) num_vertices = mNumS*mNumT; num_indices = (mNumS-1)*(mNumT-1)*6; - partial_build = (num_vertices > mNumVertices || num_indices > mNumIndices) ? false : partial_build; + // createVolumeFaces builds partially only where the layout, and so the + // counts, are the last build's. + llassert(!partial_build || (num_vertices == mNumVertices && num_indices == mNumIndices)); + if (num_vertices != mNumVertices || num_indices != mNumIndices) + { + partial_build = false; + } if (!partial_build) { diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 83f2cffe44..189f6b6aa4 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -1001,6 +1001,10 @@ class LLVolumeFace LLVolumeOctree* mOctree; LLVolumeTriangle* mOctreeTriangles; + // The tangents were made for positions a partial build has since moved: + // createTangents makes them again, in the buffer they have. + bool mTangentsStale = false; + bool createUnCutCubeCap(LLVolume* volume, bool partial_build = false); bool createCap(LLVolume* volume, bool partial_build = false); bool createSide(LLVolume* volume, bool partial_build = false); diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index c246cd91a0..45d8abaec6 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -1991,9 +1991,9 @@ namespace } // A flexible prim's frame, as the rebuild runs it: the path moves, the - // volume is rebuilt in place, and its faces' tangents are taken again - // where a face lost them. The rebuild must come out as a fresh volume - // built in full on the moved path, in the buffers it had. + // volume is rebuilt in place, and genTangents brings its faces' + // tangents up to the moved positions. The rebuild must come out as a + // fresh volume built in full on the moved path, in the buffers it had. void checkFlexiFrame(U8 profile, U8 hole, F32 hollow, S32 sections, S32 lod, Report& report, const std::string& name) { LLPointer volume = flexiVolume(profile, hole, hollow, lod); @@ -2822,7 +2822,7 @@ namespace // Partial builds: a flexible prim's every frame rebuilds its volume in // place, and must come out as a full build would, in the buffers it had. - TEST_CASE("llvolume: a flexible box's partial build equals a full one", "[llvolume][!shouldfail]") + TEST_CASE("llvolume: a flexible box's partial build equals a full one", "[llvolume]") { Report report; for (S32 sections : { 4, 16 }) @@ -2836,7 +2836,7 @@ namespace CHECK(report.failures == 0); } - TEST_CASE("llvolume: a flexible cylinder's partial build equals a full one", "[llvolume][!shouldfail]") + TEST_CASE("llvolume: a flexible cylinder's partial build equals a full one", "[llvolume]") { Report report; for (S32 sections : { 4, 16 }) @@ -2851,7 +2851,7 @@ namespace CHECK(report.failures == 0); } - TEST_CASE("llvolume: a flexible box with a square hole has a partial build equal to a full one", "[llvolume][!shouldfail]") + TEST_CASE("llvolume: a flexible box with a square hole has a partial build equal to a full one", "[llvolume]") { Report report; for (S32 sections : { 4, 16 }) @@ -2868,7 +2868,7 @@ namespace // 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][!shouldfail]") + TEST_CASE("llvolume: an uncut cube cap's partial build keeps its buffers", "[llvolume]") { Report report; for (S32 lod = 0; lod < LOD_COUNT; ++lod) From 9f9952f3d031a89bcdfc59499ae4b3fd698d8f46 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:53:27 -0400 Subject: [PATCH 06/16] A hole stays inside its wall With the square-hole hollow limit lifted, a square hole above 0.707 in a round profile, and holes in triangle profiles, put their corners past the outer wall (radius 0.53 against 0.5). The cap strip and the inner wall then had no valid layout, and a cylinder with an 0.85 square hole had its caps facing inward. addHole clamps each hole point radially to the outer profile. Where the hole touches the wall, the hollow cap's triangulation treats the strip's rounding-sized triangles as having no area; every profile whose hole does not touch it triangulates as before. The sphere pole override skips cut-end faces, which collapse onto the axis. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 189 +++++++++++++-------------- indra/llmath/llvolume.h | 4 + indra/llmath/tests/llvolume_test.cpp | 45 +++---- 3 files changed, 115 insertions(+), 123 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 528fbe85bb..53e2d32e29 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -601,6 +601,43 @@ LLProfile::Face* LLProfile::addHole(const LLProfileParams& params, bool flat, F3 pt[i].mul(box_hollow); } + // A hole stays inside its wall. A point that would go through it, as a + // square hole's corners do above 0.707 in a round profile or in a + // triangle, is drawn in along its own direction to where it meets the + // wall, which is of no thickness there. The wall is convex about the + // origin, so the ray from the origin through a point leaves it where it + // first crosses the line of any of its edges. + for (S32 i=mTotalOut;i 0.f) + { + nearest = llmin(nearest, s); + } + } + if (nearest < 1.f) + { + pt[i].getF32ptr()[0] = px * nearest; + pt[i].getF32ptr()[1] = py * nearest; + mHoleTouchesWall = true; + } + } + S32 j=mTotal-1; for (S32 i=mTotalOut;i 0.f && area_1a2 <= tolerance)) + { + tri_1a2 = false; + } + if (area_1ba < -tolerance && !(tolerance > 0.f && area_21b <= tolerance)) + { + tri_21b = false; + } + if (!tri_1a2) + { + return false; + } + if (!tri_21b) + { + return true; + } + return outer_shorter; + } + // The path station a cap closes: the last for a top cap, the first for a // bottom one. S32 cap_station(const LLVolume* volume, bool top) @@ -6185,6 +6256,12 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) if (mTypeMask & HOLLOW_MASK) { + // A hole that touches its wall puts inner points on outer ones, and + // the strip's triangles there have no area but what rounding leaves, + // of either sign: the signs alone can then leave the strip only + // triangles facing the wrong way. + const F32 zero_area = volume->getProfile().holeTouchesWall() ? CAP_ZERO_AREA : 0.f; + if (mTypeMask & TOP_MASK) { // HOLLOW TOP @@ -6229,54 +6306,13 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) (paV[0]*pbV[1] - pbV[0]*paV[1]) + (pbV[0]*p2V[1] - p2V[0]*pbV[1]); - bool use_tri1a2 = true; - bool tri_1a2 = true; - bool tri_21b = true; - - if (area_1a2 < 0) - { - tri_1a2 = false; - } - if (area_2ab < 0) - { - // Can't use, because it contains point b - tri_1a2 = false; - } - if (area_21b < 0) - { - tri_21b = false; - } - if (area_1ba < 0) - { - // Can't use, because it contains point b - tri_21b = false; - } - - if (!tri_1a2) - { - use_tri1a2 = false; - } - else if (!tri_21b) - { - use_tri1a2 = true; - } - else - { - LLVector4a d1; - d1.setSub(p1, pa); + LLVector4a d1; + d1.setSub(p1, pa); + LLVector4a d2; + d2.setSub(p2, pb); + const bool outer_shorter = d1.dot3(d1) < d2.dot3(d2); - LLVector4a d2; - d2.setSub(p2, pb); - - if (d1.dot3(d1) < d2.dot3(d2)) - { - use_tri1a2 = true; - } - else - { - use_tri1a2 = false; - } - } + const bool use_tri1a2 = cap_strip_takes_outer(area_1a2, area_2ab, area_21b, area_1ba, outer_shorter, zero_area); if (use_tri1a2) { @@ -6335,53 +6371,13 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) (paV[0]*pbV[1] - pbV[0]*paV[1]) + (pbV[0]*p2V[1] - p2V[0]*pbV[1]); - bool use_tri1a2 = true; - bool tri_1a2 = true; - bool tri_21b = true; - - if (area_1a2 < 0) - { - tri_1a2 = false; - } - if (area_2ab < 0) - { - // Can't use, because it contains point b - tri_1a2 = false; - } - if (area_21b < 0) - { - tri_21b = false; - } - if (area_1ba < 0) - { - // Can't use, because it contains point b - tri_21b = false; - } - - if (!tri_1a2) - { - use_tri1a2 = false; - } - else if (!tri_21b) - { - use_tri1a2 = true; - } - else - { - LLVector4a d1; - d1.setSub(p1,pa); - LLVector4a d2; - d2.setSub(p2,pb); + LLVector4a d1; + d1.setSub(p1, pa); + LLVector4a d2; + d2.setSub(p2, pb); + const bool outer_shorter = d1.dot3(d1) < d2.dot3(d2); - if (d1.dot3(d1) < d2.dot3(d2)) - { - use_tri1a2 = true; - } - else - { - use_tri1a2 = false; - } - } + const bool use_tri1a2 = cap_strip_takes_outer(area_1a2, area_2ab, area_21b, area_1ba, outer_shorter, zero_area); // Flipped backfacing from top if (use_tri1a2) @@ -6984,9 +6980,10 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) } } - // On a sphere, a column that is one place in every row is a pole on the - // axis the half circle sweeps about, and takes the axis. - if (sculpt_stitching == LL_SCULPT_TYPE_NONE && volume->getPathType() == LL_PCODE_PATH_CIRCLE && + // 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. + if (sculpt_stitching == LL_SCULPT_TYPE_NONE && !(mTypeMask & END_MASK) && 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)) diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 189f6b6aa4..6ce72c51bc 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -709,6 +709,9 @@ class LLProfile bool generate(const LLProfileParams& params, bool path_open, F32 detail = 1.0f, S32 split = 0, bool is_sculpted = false, S32 sculpt_size = 0); bool isConcave() const { return mConcave; } + // Whether the hole was drawn in where it would have gone through the + // outer wall, and so touches it there. + bool holeTouchesWall() const { return mHoleTouchesWall; } public: struct Face { @@ -743,6 +746,7 @@ class LLProfile bool mOpen; bool mConcave; bool mDirty; + bool mHoleTouchesWall = false; S32 mTotalOut; S32 mTotal; diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 45d8abaec6..55f61a228d 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -374,11 +374,6 @@ namespace return fmt::format("{} at LOD {}", s.name, lod); } - bool isRoundOrTriangle(U8 profile) - { - return profile != LL_PCODE_PROFILE_SQUARE; - } - bool isTriangle(U8 profile) { return profile == LL_PCODE_PROFILE_ISOTRI || profile == LL_PCODE_PROFILE_EQUALTRI || profile == LL_PCODE_PROFILE_RIGHTTRI; @@ -587,9 +582,6 @@ namespace // A cap with no area. Where its path leaves it within its plane, its // normal is far from the way its wall runs. constexpr U32 CapNoArea = 1 << 2; - // A square hole above 0.707 in a round or triangular profile, - // whose corners go through the wall. - constexpr U32 SquareHole = 1 << 3; } U32 excusedBy(U32 regions) @@ -599,10 +591,6 @@ namespace { checks |= Checks::CapFacing; } - if (regions & Regions::SquareHole) - { - checks |= Checks::Inversion | Checks::CapFacing | Checks::Oracle; - } return checks; } @@ -628,16 +616,9 @@ namespace return (volume.getParams().getSculptType() & LL_SCULPT_TYPE_MASK) != LL_SCULPT_TYPE_NONE; } - U32 regionsOf(const LLVolume& volume, const LLVolumeFace& face, const std::vector& triangles) + U32 regionsOf(const LLVolumeFace& face, const std::vector& triangles) { U32 regions = 0; - const LLVolumeParams& params = volume.getParams(); - const U8 profile = params.getProfileParams().getCurveType() & LL_PCODE_PROFILE_MASK; - const U8 hole = params.getProfileParams().getCurveType() & LL_PCODE_HOLE_MASK; - if (!isSculpt(volume) && hole == LL_PCODE_HOLE_SQUARE && params.getHollow() > 0.70711f && isRoundOrTriangle(profile)) - { - regions |= Regions::SquareHole; - } if (isCap(face) && std::all_of(triangles.begin(), triangles.end(), [](const Triangle& t) { return t.degenerate; })) { regions |= Regions::CapNoArea; @@ -1217,7 +1198,7 @@ namespace } const std::vector triangles = trianglesOf(face); State& state = states[i]; - state.regions = regionsOf(volume, face, triangles); + state.regions = regionsOf(face, triangles); if (how.only_regions && !(state.regions & how.only_regions)) { continue; @@ -2765,9 +2746,9 @@ namespace } // SetHollow no longer holds a square hole in a round profile to - // 0.7, and the hole's corners, at 0.707 of the hollow, go through the - // wall above it: the hole's points must stay inside the wall. - TEST_CASE("llvolume: a square hole above 0.707 stays inside its wall", "[llvolume][!shouldfail]") + // 0.7, and the hole's corners, at 0.707 of the hollow, would go through + // the wall above it: the hole's points must stay inside the wall. + TEST_CASE("llvolume: a square hole above 0.707 stays inside its wall", "[llvolume]") { Report report; for (const Shape& base : { cylinder(), sphere(), prism() }) @@ -2811,11 +2792,21 @@ namespace } } } + // The sweep's square holes above 0.707 in round and triangular + // profiles, cut, twisted and tapered: where hole and wall touch, the + // cap's strip and the inner side still have a layout. Inspect how; how.checks = Checks::Inversion | Checks::CapFacing | Checks::Oracle; - how.only_regions = Regions::SquareHole; - S32 volumes = 0; - inspectSweep(how, report, volumes); + for (const Shape& s : sweepShapes()) + { + if (s.hole == LL_PCODE_HOLE_SQUARE && s.hollow > 0.70711f && s.profile != LL_PCODE_PROFILE_SQUARE) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + inspectPrim(s, lod, how, report); + } + } + } INFO(report.text()); CHECK(report.failures == 0); } From 62931258e7dc14e7c1df426d559b8e22f36f3619 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:53:45 -0400 Subject: [PATCH 07/16] The sculpt sphere placeholder is laid out as the sphere stitching expects sculptGenerateSpherePlaceholder laid the sphere out transposed against the rows and columns the sphere stitching's pole and seam fix-ups assume, which gave it inverted triangles (127 to 158 degrees). It is now generated in that layout, still facing outward. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 10 ++++++---- indra/llmath/tests/llvolume_test.cpp | 11 ++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 53e2d32e29..5714abba1a 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -3092,7 +3092,9 @@ void LLVolume::sculptGenerateEmptyPlaceholder() } } -// create sphere placeholder shape +// create sphere placeholder shape, laid out as a sphere sculpt's map is +// sampled: the profile runs around each row, and the path from pole to pole, +// so that the sphere stitching's pole and seam normals land on it void LLVolume::sculptGenerateSpherePlaceholder() { S32 sizeS = mPathp->mPath.size(); @@ -3108,15 +3110,15 @@ void LLVolume::sculptGenerateSpherePlaceholder() LLVector4a& pt = mMesh[i]; - F32 u = (F32)s / (sizeS - 1); - F32 v = (F32)t / (sizeT - 1); + F32 u = (F32)t / (sizeT - 1); + F32 v = (F32)s / (sizeS - 1); const F32 RADIUS = (F32) 0.3; F32* p = pt.getF32ptr(); 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[1] = (F32)(-sin(F_PI * v) * sin(2.0 * F_PI * u) * RADIUS); p[2] = (F32)(cos(F_PI * v) * RADIUS); llassert(pt.isFinite3()); diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 55f61a228d..860bcb35fb 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -2932,11 +2932,12 @@ namespace CHECK(report.failures == 0); } - // The sphere placeholder runs u around along the path and v pole to - // pole along the profile, the other way round from a sculpt map, but - // gets a sphere sculpt's normal stitching: the poles' average lands on - // one meridian, and the S wrap sums the north pole with the south. - TEST_CASE("llvolume: the sphere placeholder's normals follow its surface", "[llvolume][!shouldfail]") + // 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. + TEST_CASE("llvolume: the sphere placeholder's normals follow its surface", "[llvolume]") { Inspect how; how.checks = Checks::Inversion | Checks::Oracle; From d219c7be82cbf0bcfc368419fa94d1a5d23361c3 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:54:00 -0400 Subject: [PATCH 08/16] A side's normal pass takes each edge once, and a hole clamps only to edges it crosses The normal accumulation computed all four edges of every quad and masked the ones shorter than rounding; it now takes each edge once, passing a quad's right edge on as the next quad's left, and drops a rounding-sized edge with one comparison. addHole's clamp looked for the nearest outer edge along each hole point's ray, and could take an edge the point lies well inside of, on the origin's side, whose line the ray crosses beyond the point; it now skips those. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 111 ++++++++++++++------------- indra/llmath/tests/llvolume_test.cpp | 7 +- 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 5714abba1a..ca336fd630 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -614,17 +614,20 @@ LLProfile::Face* LLProfile::addHole(const LLProfileParams& params, bool flat, F3 F32 nearest = 1.f; for (S32 k = 0; k + 1 < mTotalOut; k++) { - // Where the ray s * p crosses the line a + u * e. + // Where the ray s * p crosses the line a + u * e: at s = toward / + // across. A point well inside the line, on the origin's side, + // crosses it beyond itself. const F32 ax = mProfile[k][0]; const F32 ay = mProfile[k][1]; const F32 ex = mProfile[k + 1][0] - ax; const F32 ey = mProfile[k + 1][1] - ay; + const F32 toward = ax * ey - ay * ex; const F32 across = px * ey - py * ex; - if (across == 0.f) + if ((toward > 0.f && toward - across > 1e-6f) || across == 0.f) { continue; } - const F32 s = (ax * ey - ay * ex) / across; + const F32 s = toward / across; if (s > 0.f) { nearest = llmin(nearest, s); @@ -5659,36 +5662,31 @@ namespace return true; } - // Adds a grid quad to its corners' normals. Its two triangles, (bl, br, - // tr) and (bl, tr, tl), share the diagonal from bl to tr: each gives its - // corner off the diagonal its cross product, twice its area along its - // normal, and each corner on the diagonal half of it. A parallelogram - // then gives its four corners one weight, so a vertex has as much from - // either side of it in every row, and no corner takes a normal from a - // triangle it is not in. Each cross product is taken at the corner off - // the diagonal: where a copied or collapsed row or column puts 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. An edge between - // two points that are one place but for rounding, as a closed profile's - // last point and its first, counts as having none. A quad between a - // copied row or column adds nothing, so the edge there stays hard. - void add_quad_to_normals(LLVector4a* norm, const LLVector4a* pos, S32 bl, S32 br, S32 tl, S32 tr) - { - LLVector4a bottom, right, top, left; - bottom.setSub(pos[br], pos[bl]); - right.setSub(pos[tr], pos[br]); - top.setSub(pos[tr], pos[tl]); - left.setSub(pos[tl], pos[bl]); - - const LLVector4a zero = LLVector4a::getZero(); - const LLVector4a same_place(SAME_PLACE * SAME_PLACE); - for (LLVector4a* edge : { &bottom, &right, &top, &left }) - { - LLVector4a length; - length.setAllDot3(*edge, *edge); - edge->setSelectWithMask(length.lessThan(same_place), zero, *edge); + // An edge between two points that are one place but for rounding, as a + // closed profile's last point and its first, has no length. + inline void drop_rounding(LLVector4a& edge) + { + if (edge.equals3(LLVector4a::getZero(), SAME_PLACE)) + { + edge.clear(); } + } + // Adds a grid quad, given by its edges, to its corners' normals. Its two + // triangles, (bl, br, tr) and (bl, tr, tl), share the diagonal from bl to + // tr: each gives its corner off the diagonal its cross product, twice its + // area along its normal, and each corner on the diagonal half of it. A + // parallelogram then gives its four corners one weight, so a vertex has + // as much from either side of it in every row, and no corner takes a + // normal from a triangle it is not in. Each cross product is taken at the + // corner off the diagonal: where a copied or collapsed row or column puts + // 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. + 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) + { LLVector4a lower, upper; lower.setCross3(bottom, right); upper.setCross3(top, left); @@ -5705,14 +5703,13 @@ namespace // Brings every normal of a grid to unit length. A vertex whose quads all // have no area, as a copy that closes a cut flat inner side or a row in - // the middle of a sculpt's pinch, has no normal of its own, and takes - // the nearest one up or down its column, or, where the whole column has - // none, along its row, or, where neither has one, as where a copied - // column crosses a copied row, the nearest around it: only a normal a - // vertex had of its own, so that what one such vertex takes does not - // pass on to the next. A grid with no normal at all faces +Z. While they - // wait, such vertices are marked with a w of 1, which every normal of - // its own has as 0. + // 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. void normalize_grid_normals(LLVector4a* norm, S32 num_s, S32 num_t) { const S32 count = num_s * num_t; @@ -6258,10 +6255,8 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) if (mTypeMask & HOLLOW_MASK) { - // A hole that touches its wall puts inner points on outer ones, and - // the strip's triangles there have no area but what rounding leaves, - // of either sign: the signs alone can then leave the strip only - // triangles facing the wrong way. + // Where the hole touches the wall, the strip's triangles have no area + // but what rounding leaves, of either sign. const F32 zero_area = volume->getProfile().holeTouchesWall() ? CAP_ZERO_AREA : 0.f; if (mTypeMask & TOP_MASK) @@ -6897,20 +6892,30 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) for (t = 0; t < mNumT - 1; ++t) { const S32 row = mNumS * t; + LLVector4a left; + left.setSub(pos[row + mNumS], pos[row]); + drop_rounding(left); for (s = 0; s < mNumS - 1; ++s) { const S32 bl = row + s; const S32 tl = bl + mNumS; - add_quad_to_normals(norm, pos, bl, bl + 1, tl, tl + 1); - } - } - - // A side's normals join across an edge where its first and last columns - // are one seam, or its first and last rows: a prim's where they are one - // place in every row or column, which the face tells from its own - // positions and not from whether the profile or the path counts as - // closed; a sculpt's where its stitching samples them from one column - // or row of the map. + LLVector4a bottom, right, top; + bottom.setSub(pos[bl + 1], pos[bl]); + right.setSub(pos[tl + 1], pos[bl + 1]); + top.setSub(pos[tl + 1], pos[tl]); + drop_rounding(bottom); + drop_rounding(right); + drop_rounding(top); + add_quad_to_normals(norm, bl, bl + 1, tl, tl + 1, bottom, right, top, left); + left = right; + } + } + + // A side's normals join across its first and last columns where they are + // 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. if (!partial_build) { if (sculpt_stitching == LL_SCULPT_TYPE_NONE) diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 860bcb35fb..2e4773dd0a 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -1098,8 +1098,9 @@ namespace { return; } - const bool sphere = !isSculpt(volume) && (volume.getParams().getProfileParams().getCurveType() & LL_PCODE_PROFILE_MASK) == LL_PCODE_PROFILE_CIRCLE_HALF && - volume.getParams().getPathParams().getCurveType() == LL_PCODE_PATH_CIRCLE; + const LLVolumeParams& params = volume.getParams(); + const bool sphere = !isSculpt(volume) && (params.getProfileParams().getCurveType() & LL_PCODE_PROFILE_MASK) == LL_PCODE_PROFILE_CIRCLE_HALF && + params.getPathParams().getCurveType() == LL_PCODE_PATH_CIRCLE; auto pole = [&](S32 column) { for (S32 t = 1; t < face.mNumT; ++t) @@ -2492,7 +2493,7 @@ namespace } // ------------------------------------------------------------------ - // Today's defects + // Defects, a case each: tagged [!shouldfail] while the generator has one // A cylinder side's quads are rectangles of one size, so a vertex has // as much area on either side of it, in every row: a weight that went to From 414577f3c8b7b9debbb34351ae7a731c101e8442 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:54:19 -0400 Subject: [PATCH 09/16] A cap with no area is held to a finite normal that faces out A cap with no area, on a pyramid, a cone's tip or the end of a circle2 or tapered half-circle path, has nothing drawn on it, and its station's plane leaves it 70 to 90 degrees from its wall on the last two. Its case now asks for a finite, unit normal facing away from the wall rather than one within 60 degrees of it. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/llvolume_test.cpp | 64 ++++++++++------------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 2e4773dd0a..5ffc348831 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -33,13 +33,11 @@ // mesh corpus is built here and goes through cacheOptimize as a decoded LOD // does. // -// What the generator gets wrong today is a case of its own, tagged -// [!shouldfail]: the case holds the generator to what it should do, so it -// fails, as expected, while the defect stands, and the fix that clears the -// defect removes the tag. Where a defect breaks one of the sweep's checks on -// some faces, the sweep leaves that check on those faces to the defect's -// case: a region below names the faces by what they are, not by how they -// come out, and the fix removes the region with the tag. +// What the generator gets wrong is a case of its own, tagged [!shouldfail]: +// the case holds the generator to what it should do, so it fails, as +// expected, while the defect stands, and the fix that clears the defect +// removes the tag. A case may inspect only the faces of a region, which +// names them by what they are, not by how they come out. #include "linden_common.h" @@ -579,21 +577,10 @@ namespace namespace Regions { - // A cap with no area. Where its path leaves it within its plane, its - // normal is far from the way its wall runs. + // A cap with no area. constexpr U32 CapNoArea = 1 << 2; } - U32 excusedBy(U32 regions) - { - U32 checks = 0; - if (regions & Regions::CapNoArea) - { - checks |= Checks::CapFacing; - } - return checks; - } - // Vertices that no triangle with area uses. std::vector loneVertices(const LLVolumeFace& face, const std::vector& triangles) { @@ -629,13 +616,8 @@ namespace struct Inspect { U32 checks = Checks::Sweep; - // Nonzero: inspect only the faces in these regions, nothing of them - // excused. Either way, on each face the checks that its other - // regions break are excused. + // Nonzero: inspect only the faces in these regions. U32 only_regions = 0; - // How far a cap may lean from the direction its wall runs in: the - // cosine of the widest angle allowed. - F32 cap_facing = 0.f; // Check inversion also where the surface folds at a point. bool inversion_any_spread = false; }; @@ -779,10 +761,10 @@ namespace } } - // A cap faces along the path, away from the wall it closes: from the - // ring of the station next to it toward its own. - void checkCapFacing(const LLVolume& volume, const LLVolumeFace& face, const std::vector& normals, F32 min_cos, Report& report, - const std::string& where) + // A cap faces away from the wall it closes: its normal is on the side of + // its plane the path leaves the wall by, from the ring of the station + // next to it toward its own. + void checkCapFacing(const LLVolume& volume, const LLVolumeFace& face, const std::vector& normals, Report& report, const std::string& where) { const LLAlignedArray& mesh = volume.getMesh(); const S32 ring = volume.getProfile().getTotal(); @@ -806,7 +788,7 @@ namespace for (S32 v = 0; v < face.mNumVertices; ++v) { const LLVector3& n = normals[v]; - if (!(n * away > min_cos * n.length() * away.length())) + if (!(n * away > 0.f)) { report.fail(fmt::format("{}: vertex {}'s normal is {:.1f} degrees from the way the path leaves its wall", where, v, degreesBetween(n, away))); return; @@ -1205,9 +1187,7 @@ namespace continue; } state.inspected = true; - // A face in another defect's region still has that defect's - // checks excused. - state.checks = how.checks & ~excusedBy(state.regions & ~how.only_regions); + state.checks = how.checks; state.raw.resize(face.mNumVertices); for (S32 v = 0; v < face.mNumVertices; ++v) { @@ -1228,7 +1208,7 @@ namespace } if ((state.checks & Checks::CapFacing) && isCap(face)) { - checkCapFacing(volume, face, state.raw, how.cap_facing, report, where); + checkCapFacing(volume, face, state.raw, report, where); } // A sculpt's UVs are its grid, wherever the map puts the points. if ((state.checks & Checks::AffineUV) && !isSculpt(volume)) @@ -2634,17 +2614,17 @@ namespace CHECK(report.failures == 0); } - // A cap with no area, as on a pyramid or a prism, still faces along the - // path. A cap's normal is its station's plane's, which a circle2 path's - // alternating stations, and a half circle on a circle path tapered to a - // point, leave the cap at 70 to 90 degrees from: their walls run off - // within the plane, not along its normal. - TEST_CASE("llvolume: a cap with no area still faces away from its wall", "[llvolume][!shouldfail]") + // A cap with no area, as on a pyramid or a prism, has no triangle to + // take a normal from, and still has a unit normal facing away from its + // wall. Where a circle2 path's alternating stations, or a half circle + // on a circle path tapered to a point, leave the cap within its plane, + // that normal is close to square to the way the wall runs, but on its + // far side. + TEST_CASE("llvolume: a cap with no area still faces away from its wall", "[llvolume]") { Inspect how; - how.checks = Checks::Finite | Checks::CapFacing | Checks::Tangents; + how.checks = Checks::Finite | Checks::RawUnit | Checks::CapFacing | Checks::Tangents; how.only_regions = Regions::CapNoArea; - how.cap_facing = 0.5f; Report report; S32 volumes = 0; inspectSweep(how, report, volumes); From 5551f6cf4997db97fb2b06994acaa76eec5ea3dd Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:54:34 -0400 Subject: [PATCH 10/16] Prim and sculpt tangents take meshoptimizer's weighting, eight lanes at a time Prims and sculpts used Lengyel's tangents: an unweighted sum of face tangents, an invented 1024x ratio for a triangle with no UV area, and a +Z fallback whatever the normal. Mesh assets use MikkTSpace. Prims and sculpts now take the arithmetic of meshoptimizer's default tangent mode: each triangle's tangent by the UV chain rule, projected onto the vertex normal at each corner, weighted by the corner's angle in the tangent plane times the product of its edge lengths, with w from the UV orientation. A prim's or sculpt's vertex is one tangent group, so the sum goes straight to vertices, with no weld, no adjacency and no allocation. alsimd::grid_tangents does a side's grid and alsimd::indexed_tangents a cap, eight lanes wide on AVX2. A triangle adds nothing where its area, in positions or in UVs, is under 1e-5 of its edges squared, so rounding slivers on hollow caps and flat inner seams no longer make noise tangents. Faces cacheOptimize has handled keep their own path. A test holds the kernels to meshopt_generateTangents over the whole sweep and every sculpt, within a rounding bound modelled per vertex. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/alsimdkernels.cpp | 13 + indra/llmath/alsimdkernels.h | 32 +- indra/llmath/alsimdkernels.inl | 766 +++++++++++++++++++++- indra/llmath/llvolume.cpp | 30 +- indra/llmath/tests/alsimdkernels_test.cpp | 144 ++++ indra/llmath/tests/llvolume_bench.cpp | 38 +- indra/llmath/tests/llvolume_test.cpp | 318 ++++++++- 7 files changed, 1312 insertions(+), 29 deletions(-) diff --git a/indra/llmath/alsimdkernels.cpp b/indra/llmath/alsimdkernels.cpp index c3780042a6..1594f66576 100644 --- a/indra/llmath/alsimdkernels.cpp +++ b/indra/llmath/alsimdkernels.cpp @@ -95,4 +95,17 @@ void morph_apply(const MorphApply& m) kernels::morph_apply_impl(m); } +void grid_tangents(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t columns, size_t rows, + LLVector4a* tangents) +{ + thread_local std::vector scratch; + kernels::grid_tangents_impl(positions, normals, uvs, columns, rows, tangents, scratch); +} + +void indexed_tangents(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t vertex_count, + const U16* indices, size_t index_count, LLVector4a* tangents) +{ + kernels::indexed_tangents_impl(positions, normals, uvs, vertex_count, indices, index_count, tangents); +} + } // namespace alsimd diff --git a/indra/llmath/alsimdkernels.h b/indra/llmath/alsimdkernels.h index e87f2eca28..72c090d420 100644 --- a/indra/llmath/alsimdkernels.h +++ b/indra/llmath/alsimdkernels.h @@ -1,8 +1,8 @@ /** * @file alsimdkernels.h * @brief Batch kernels over arrays of vectors: transforms, dequantization, - * index offsets, skinning and bounds, at the register width the - * build was compiled for. + * index offsets, skinning, bounds and tangents, at the register + * width the build was compiled for. * * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Alchemy Viewer Source Code @@ -120,6 +120,34 @@ struct MorphApply void morph_apply(const MorphApply& m); +// Every vertex's tangent by meshoptimizer's default weighting, summed into +// the vertex itself rather than into groups of its corners. A triangle's +// tangent is dP/du by the chain rule. It adds nothing where the triangle +// has no area, in positions or in UVs, to within rounding: twice the area +// under 1e-5 of the sum of the squares of the two edges from its first +// corner, as when two corners are one point. At each corner it is put in +// the plane of that corner's normal, renormalized, and weighted by the +// angle between the corner's edges in that plane times the product of +// their lengths there. Each vertex's sum is put back in its normal's plane +// and normalized; where nothing was added it is +x in that plane, or +y +// where the normal is within 45 degrees of x. w is the sign of the UV +// area of the vertex's triangles in index order, the sign of the most of +// them where they differ; a vertex in none of them takes the face's, and +// -1 where the face has none either. The normals are taken as unit length. +// +// The grid form is a grid of columns by rows, vertex s + columns * t, each +// quad two triangles: bottom left, top right, top left and bottom left, +// bottom right, top right, as LLVolumeFace::createSide indexes them. It +// needs no index buffer, and keeps the grid as lanes in a scratch buffer +// of each thread's, which grows to the largest grid the thread has made +// tangents for. +void grid_tangents(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t columns, size_t rows, + LLVector4a* tangents); + +// The same over triangles from an index buffer. +void indexed_tangents(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t vertex_count, + const U16* indices, size_t index_count, LLVector4a* tangents); + } // namespace alsimd #endif // AL_SIMDKERNELS_H diff --git a/indra/llmath/alsimdkernels.inl b/indra/llmath/alsimdkernels.inl index 4307492c07..27d5288b03 100644 --- a/indra/llmath/alsimdkernels.inl +++ b/indra/llmath/alsimdkernels.inl @@ -28,7 +28,9 @@ #include "alsimdkernels.h" +#include #include +#include // A width type is a register holding `lanes` four-float vectors side by // side and the lane-replicated operations on it: every shuffle, splat and @@ -38,7 +40,10 @@ // everywhere; ALSimd8 needs AVX, ALSimd16 AVX-512. The wide types are the // x86 intrinsics under every compiler: they exist on no other // architecture, and the intrinsics are the one spelling all three -// compilers share. +// compilers share. ALSimd4 and ALSimd8 also carry what the lane kernels +// need, which keep one value in each float lane: lane-wise arithmetic, +// comparisons to a mask, selects, and loads and stores that turn vectors +// into lanes and back. namespace alsimd { @@ -71,6 +76,81 @@ struct ALSimd4 static AL_SIMD_INLINE f32x4 reduce_max(reg v) { return v; } // whether a > b in any lane whose bits in keep are set static AL_SIMD_INLINE bool any_greater(reg a, reg b, reg keep) { return alsimd::any(alsimd::and_(alsimd::cmpgt(a, b), alsimd::as_mask(keep))); } + + // Lane by lane, for the kernels that keep one value in each float lane + // rather than one vector in each four. + using mask = alsimd::mask4; + static constexpr int floats = 4; + static AL_SIMD_INLINE reg loadu(const F32* p) { return alsimd::loadu(p); } + static AL_SIMD_INLINE void storeu(F32* p, reg v) { alsimd::storeu(p, v); } + static AL_SIMD_INLINE reg zero() { return alsimd::zero(); } + static AL_SIMD_INLINE reg ramp() { return alsimd::set(0.f, 1.f, 2.f, 3.f); } + static AL_SIMD_INLINE reg sub(reg a, reg b) { return alsimd::sub(a, b); } + static AL_SIMD_INLINE reg div(reg a, reg b) { return alsimd::div(a, b); } + static AL_SIMD_INLINE reg sqrt(reg a) { return alsimd::sqrt(a); } + static AL_SIMD_INLINE reg rsqrt(reg a) { return alsimd::rsqrt(a); } + static AL_SIMD_INLINE reg floor(reg a) { return alsimd::floor(a); } + static AL_SIMD_INLINE reg abs(reg a) { return alsimd::abs(a); } + static AL_SIMD_INLINE reg neg(reg a) { return alsimd::neg(a); } + static AL_SIMD_INLINE mask cmpeq(reg a, reg b) { return alsimd::cmpeq(a, b); } + static AL_SIMD_INLINE mask cmpne(reg a, reg b) { return alsimd::cmpne(a, b); } + static AL_SIMD_INLINE mask cmplt(reg a, reg b) { return alsimd::cmplt(a, b); } + static AL_SIMD_INLINE mask cmple(reg a, reg b) { return alsimd::cmple(a, b); } + static AL_SIMD_INLINE mask cmpgt(reg a, reg b) { return alsimd::cmpgt(a, b); } + static AL_SIMD_INLINE mask and_(mask a, mask b) { return alsimd::and_(a, b); } + static AL_SIMD_INLINE mask or_(mask a, mask b) { return alsimd::or_(a, b); } + static AL_SIMD_INLINE mask none() { return alsimd::mask_none(); } + static AL_SIMD_INLINE reg select(mask m, reg t, reg f) { return alsimd::select(m, t, f); } + static AL_SIMD_INLINE bool any(mask m) { return alsimd::any(m); } + + // The vectors at at(0) to at(3) as their x, y, z and w lanes. at gives + // each vector's address, which the transpose takes where it needs it. + template + static AL_SIMD_INLINE void load_columns(At at, reg& x, reg& y, reg& z, reg& w) + { + const reg a = alsimd::load(at(0)); + const reg b = alsimd::load(at(1)); + const reg c = alsimd::load(at(2)); + const reg d = alsimd::load(at(3)); + const reg ab_low = alsimd::unpacklo(a, b); + const reg ab_high = alsimd::unpackhi(a, b); + const reg cd_low = alsimd::unpacklo(c, d); + const reg cd_high = alsimd::unpackhi(c, d); + x = alsimd::movelh(ab_low, cd_low); + y = alsimd::movehl(cd_low, ab_low); + z = alsimd::movelh(ab_high, cd_high); + w = alsimd::movehl(cd_high, ab_high); + } + + // The lanes back as the vectors at at(0) to at(3). + template + static AL_SIMD_INLINE void store_columns(reg x, reg y, reg z, reg w, At at) + { + const reg xy_low = alsimd::unpacklo(x, y); + const reg xy_high = alsimd::unpackhi(x, y); + const reg zw_low = alsimd::unpacklo(z, w); + const reg zw_high = alsimd::unpackhi(z, w); + alsimd::store(at(0), alsimd::movelh(xy_low, zw_low)); + alsimd::store(at(1), alsimd::movehl(zw_low, xy_low)); + alsimd::store(at(2), alsimd::movelh(xy_high, zw_high)); + alsimd::store(at(3), alsimd::movehl(zw_high, xy_high)); + } + + // The float pairs at at(0) to at(3), which need only float alignment, + // as their first and second lanes. + template + static AL_SIMD_INLINE void load_pairs(At at, reg& u, reg& v) + { +#if AL_SIMD_NEON + const reg a = vcombine_f32(vld1_f32(at(0)), vld1_f32(at(1))); + const reg b = vcombine_f32(vld1_f32(at(2)), vld1_f32(at(3))); +#else + const reg a = _mm_castpd_ps(_mm_loadh_pd(_mm_load_sd(reinterpret_cast(at(0))), reinterpret_cast(at(1)))); + const reg b = _mm_castpd_ps(_mm_loadh_pd(_mm_load_sd(reinterpret_cast(at(2))), reinterpret_cast(at(3)))); +#endif + u = alsimd::shuffle2<0, 2, 0, 2>(a, b); + v = alsimd::shuffle2<1, 3, 1, 3>(a, b); + } }; #if AL_SIMD_AVX @@ -111,6 +191,94 @@ struct ALSimd8 static AL_SIMD_INLINE f32x4 reduce_min(reg v) { return _mm_min_ps(_mm256_castps256_ps128(v), _mm256_extractf128_ps(v, 1)); } static AL_SIMD_INLINE f32x4 reduce_max(reg v) { return _mm_max_ps(_mm256_castps256_ps128(v), _mm256_extractf128_ps(v, 1)); } static AL_SIMD_INLINE bool any_greater(reg a, reg b, reg keep) { return _mm256_movemask_ps(_mm256_and_ps(_mm256_cmp_ps(a, b, _CMP_GT_OQ), keep)) != 0; } + + using mask = __m256; + static constexpr int floats = 8; + static AL_SIMD_INLINE reg loadu(const F32* p) { return _mm256_loadu_ps(p); } + static AL_SIMD_INLINE void storeu(F32* p, reg v) { _mm256_storeu_ps(p, v); } + static AL_SIMD_INLINE reg zero() { return _mm256_setzero_ps(); } + static AL_SIMD_INLINE reg ramp() { return _mm256_setr_ps(0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f); } + static AL_SIMD_INLINE reg sub(reg a, reg b) { return _mm256_sub_ps(a, b); } + static AL_SIMD_INLINE reg div(reg a, reg b) { return _mm256_div_ps(a, b); } + static AL_SIMD_INLINE reg sqrt(reg a) { return _mm256_sqrt_ps(a); } + static AL_SIMD_INLINE reg floor(reg a) { return _mm256_floor_ps(a); } + // To at least 22 bits, one Newton-Raphson step past the estimate, as + // the ops layer's rsqrt. + static AL_SIMD_INLINE reg rsqrt(reg a) + { +#if AL_SIMD_AVX512 + const reg r = _mm256_rsqrt14_ps(a); +#else + const reg r = _mm256_rsqrt_ps(a); +#endif + const reg t = fnmadd(mul(a, r), r, set1(3.f)); + return mul(mul(set1(0.5f), r), t); + } + static AL_SIMD_INLINE reg abs(reg a) { return _mm256_andnot_ps(_mm256_set1_ps(-0.f), a); } + static AL_SIMD_INLINE reg neg(reg a) { return _mm256_xor_ps(a, _mm256_set1_ps(-0.f)); } + static AL_SIMD_INLINE mask cmpeq(reg a, reg b) { return _mm256_cmp_ps(a, b, _CMP_EQ_OQ); } + static AL_SIMD_INLINE mask cmpne(reg a, reg b) { return _mm256_cmp_ps(a, b, _CMP_NEQ_UQ); } + static AL_SIMD_INLINE mask cmplt(reg a, reg b) { return _mm256_cmp_ps(a, b, _CMP_LT_OQ); } + static AL_SIMD_INLINE mask cmple(reg a, reg b) { return _mm256_cmp_ps(a, b, _CMP_LE_OQ); } + static AL_SIMD_INLINE mask cmpgt(reg a, reg b) { return _mm256_cmp_ps(a, b, _CMP_GT_OQ); } + static AL_SIMD_INLINE mask and_(mask a, mask b) { return _mm256_and_ps(a, b); } + static AL_SIMD_INLINE mask or_(mask a, mask b) { return _mm256_or_ps(a, b); } + static AL_SIMD_INLINE mask none() { return _mm256_setzero_ps(); } + static AL_SIMD_INLINE reg select(mask m, reg t, reg f) { return _mm256_blendv_ps(f, t, m); } + static AL_SIMD_INLINE bool any(mask m) { return _mm256_movemask_ps(m) != 0; } + + // The vectors at at(0) to at(7) as their x, y, z and w lanes: vectors + // 0 to 3 through the low half, 4 to 7 through the high. + template + static AL_SIMD_INLINE void load_columns(At at, reg& x, reg& y, reg& z, reg& w) + { + const reg a = _mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(at(0))), _mm_load_ps(at(4)), 1); + const reg b = _mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(at(1))), _mm_load_ps(at(5)), 1); + const reg c = _mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(at(2))), _mm_load_ps(at(6)), 1); + const reg d = _mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(at(3))), _mm_load_ps(at(7)), 1); + const reg ab_low = _mm256_unpacklo_ps(a, b); + const reg ab_high = _mm256_unpackhi_ps(a, b); + const reg cd_low = _mm256_unpacklo_ps(c, d); + const reg cd_high = _mm256_unpackhi_ps(c, d); + x = _mm256_shuffle_ps(ab_low, cd_low, _MM_SHUFFLE(1, 0, 1, 0)); + y = _mm256_shuffle_ps(ab_low, cd_low, _MM_SHUFFLE(3, 2, 3, 2)); + z = _mm256_shuffle_ps(ab_high, cd_high, _MM_SHUFFLE(1, 0, 1, 0)); + w = _mm256_shuffle_ps(ab_high, cd_high, _MM_SHUFFLE(3, 2, 3, 2)); + } + + template + static AL_SIMD_INLINE void store_columns(reg x, reg y, reg z, reg w, At at) + { + const reg xy_low = _mm256_unpacklo_ps(x, y); + const reg xy_high = _mm256_unpackhi_ps(x, y); + const reg zw_low = _mm256_unpacklo_ps(z, w); + const reg zw_high = _mm256_unpackhi_ps(z, w); + const reg v04 = _mm256_shuffle_ps(xy_low, zw_low, _MM_SHUFFLE(1, 0, 1, 0)); + const reg v15 = _mm256_shuffle_ps(xy_low, zw_low, _MM_SHUFFLE(3, 2, 3, 2)); + const reg v26 = _mm256_shuffle_ps(xy_high, zw_high, _MM_SHUFFLE(1, 0, 1, 0)); + const reg v37 = _mm256_shuffle_ps(xy_high, zw_high, _MM_SHUFFLE(3, 2, 3, 2)); + _mm_store_ps(at(0), _mm256_castps256_ps128(v04)); + _mm_store_ps(at(1), _mm256_castps256_ps128(v15)); + _mm_store_ps(at(2), _mm256_castps256_ps128(v26)); + _mm_store_ps(at(3), _mm256_castps256_ps128(v37)); + _mm_store_ps(at(4), _mm256_extractf128_ps(v04, 1)); + _mm_store_ps(at(5), _mm256_extractf128_ps(v15, 1)); + _mm_store_ps(at(6), _mm256_extractf128_ps(v26, 1)); + _mm_store_ps(at(7), _mm256_extractf128_ps(v37, 1)); + } + + template + static AL_SIMD_INLINE void load_pairs(At at, reg& u, reg& v) + { + const __m128 a = _mm_castpd_ps(_mm_loadh_pd(_mm_load_sd(reinterpret_cast(at(0))), reinterpret_cast(at(1)))); + const __m128 b = _mm_castpd_ps(_mm_loadh_pd(_mm_load_sd(reinterpret_cast(at(2))), reinterpret_cast(at(3)))); + const __m128 c = _mm_castpd_ps(_mm_loadh_pd(_mm_load_sd(reinterpret_cast(at(4))), reinterpret_cast(at(5)))); + const __m128 d = _mm_castpd_ps(_mm_loadh_pd(_mm_load_sd(reinterpret_cast(at(6))), reinterpret_cast(at(7)))); + const reg ac = _mm256_insertf128_ps(_mm256_castps128_ps256(a), c, 1); + const reg bd = _mm256_insertf128_ps(_mm256_castps128_ps256(b), d, 1); + u = _mm256_shuffle_ps(ac, bd, _MM_SHUFFLE(2, 0, 2, 0)); + v = _mm256_shuffle_ps(ac, bd, _MM_SHUFFLE(3, 1, 3, 1)); + } }; #endif @@ -163,6 +331,15 @@ using ALSimdWide = ALSimd8; using ALSimdWide = ALSimd4; #endif +// The widest the lane kernels run at, which keep one value in each float +// lane: eight floats where the build has AVX, AVX-512 included, since the +// rows they walk are rarely wide enough to fill sixteen. +#if AL_SIMD_AVX +using ALSimdLanes = ALSimd8; +#else +using ALSimdLanes = ALSimd4; +#endif + namespace kernels { @@ -577,6 +754,593 @@ AL_SIMD_INLINE void morph_apply_impl(const MorphApply& apply) } } +//////////////////////////////////// +// Tangents +//////////////////////////////////// + +// Three registers to a vector, one triangle, corner or vertex a lane. +// Every product that is added to something is added in a fused +// multiply-add of its own: nothing is left for the compiler to fuse, which +// it would do differently at each width. +template +struct Lanes3 +{ + typename W::reg x, y, z; +}; + +// A corner's share of a tangent, as its direction and its weight. +template +struct TangentShare +{ + Lanes3 direction; + typename W::reg weight; +}; + +// sum + share.direction * share.weight +template +AL_SIMD_INLINE Lanes3 add_share(const Lanes3& sum, const TangentShare& share) +{ + return { W::fmadd(share.direction.x, share.weight, sum.x), W::fmadd(share.direction.y, share.weight, sum.y), + W::fmadd(share.direction.z, share.weight, sum.z) }; +} + +template +AL_SIMD_INLINE Lanes3 load_lanes3(F32* const* rows, size_t at) +{ + return { W::loadu(rows[0] + at), W::loadu(rows[1] + at), W::loadu(rows[2] + at) }; +} + +template +AL_SIMD_INLINE Lanes3 sub_lanes3(const Lanes3& a, const Lanes3& b) +{ + return { W::sub(a.x, b.x), W::sub(a.y, b.y), W::sub(a.z, b.z) }; +} + +template +AL_SIMD_INLINE Lanes3 add_lanes3(const Lanes3& a, const Lanes3& b) +{ + return { W::add(a.x, b.x), W::add(a.y, b.y), W::add(a.z, b.z) }; +} + +template +AL_SIMD_INLINE Lanes3 scale_lanes3(const Lanes3& a, typename W::reg s) +{ + return { W::mul(a.x, s), W::mul(a.y, s), W::mul(a.z, s) }; +} + +template +AL_SIMD_INLINE typename W::reg dot_lanes3(const Lanes3& a, const Lanes3& b) +{ + return W::fmadd(a.z, b.z, W::fmadd(a.y, b.y, W::mul(a.x, b.x))); +} + +// v - n * (v . n): v in the plane of a unit n. +template +AL_SIMD_INLINE Lanes3 in_plane_lanes3(const Lanes3& v, const Lanes3& n) +{ + const typename W::reg d = dot_lanes3(v, n); + return { W::fnmadd(n.x, d, v.x), W::fnmadd(n.y, d, v.y), W::fnmadd(n.z, d, v.z) }; +} + +// 1 / sqrt(x) to within an ulp or two, and large but finite where x is 0 +// or denormal, which the estimate would read as 0 and make infinite: each +// use multiplies it by something that is 0 where x is. +template +AL_SIMD_INLINE typename W::reg inverse_root(typename W::reg x) +{ + return W::rsqrt(W::max(x, W::set1(FLT_MIN))); +} + +// acos to within about 5e-4: a quadratic in |x| times sqrt(1 - |x|), +// reflected about pi/2 for negative x. +template +AL_SIMD_INLINE typename W::reg tangent_acos(typename W::reg x) +{ + using reg = typename W::reg; + const reg ax = W::min(W::abs(x), W::set1(1.f)); + reg r = W::fmadd(ax, W::set1(0.05147786f), W::set1(-0.2053972f)); + r = W::fmadd(ax, r, W::set1(1.570337f)); + const reg root = W::sqrt(W::sub(W::set1(1.f), ax)); + return W::select(W::cmplt(x, W::zero()), W::fnmadd(r, root, W::set1(3.1415926f)), W::mul(r, root)); +} + +// A triangle is flat where twice its area, in positions or in UVs, is less +// than this fraction of the sum of the squares of the two edges from its +// first corner: its corners are in a line, or two are one point, to within +// rounding, and the direction its edges give is rounding's. +constexpr F32 TANGENT_FLAT = 1e-5f; + +// A triangle's tangent, dP/du by the chain rule at unit length, and the +// sign of its UV area, from the edges ab and ac from its first corner, +// the sum of their squares, and their UV differences. Both are zero where +// the triangle is flat or `real` is 0, and the tangent is zero where the +// UVs give it no length. +template +AL_SIMD_INLINE void triangle_tangent(const Lanes3& ab, const Lanes3& ac, typename W::reg edges, typename W::reg ab_u, + typename W::reg ab_v, typename W::reg ac_u, typename W::reg ac_v, typename W::reg real, + Lanes3& tangent, typename W::reg& sign) +{ + using reg = typename W::reg; + const reg zero = W::zero(); + const reg flat2 = W::set1(TANGENT_FLAT * TANGENT_FLAT); + const Lanes3 normal = { W::fnmadd(ab.z, ac.y, W::mul(ab.y, ac.z)), W::fnmadd(ab.x, ac.z, W::mul(ab.z, ac.x)), + W::fnmadd(ab.y, ac.x, W::mul(ab.x, ac.y)) }; + const reg uv_edges = W::fmadd(ab_v, ab_v, W::fmadd(ab_u, ab_u, W::fmadd(ac_v, ac_v, W::mul(ac_u, ac_u)))); + const Lanes3 r = { W::fnmadd(ab_v, ac.x, W::mul(ac_v, ab.x)), W::fnmadd(ab_v, ac.y, W::mul(ac_v, ab.y)), + W::fnmadd(ab_v, ac.z, W::mul(ac_v, ab.z)) }; + const reg area = W::fnmadd(ab_v, ac_u, W::mul(ab_u, ac_v)); + const typename W::mask flat = W::or_(W::cmple(dot_lanes3(normal, normal), W::mul(flat2, W::mul(edges, edges))), + W::cmple(W::mul(area, area), W::mul(flat2, W::mul(uv_edges, uv_edges)))); + sign = W::select(W::cmpgt(area, zero), W::set1(1.f), zero); + sign = W::select(W::cmplt(area, zero), W::set1(-1.f), sign); + sign = W::mul(W::select(flat, zero, sign), real); + // Unit length, or none where r is too short for the estimate to read. + const reg r2 = dot_lanes3(r, r); + tangent = scale_lanes3(r, W::mul(sign, W::select(W::cmplt(r2, W::set1(FLT_MIN)), zero, W::rsqrt(r2)))); +} + +// A corner's share of its triangle's tangent: the tangent in the plane of +// the corner's normal n, at unit length, weighted by the angle between the +// corner's edges e1 and e2 in that plane times the product of their +// lengths there. The edges come as their dots: each with n, each with +// itself, and with each other; for a unit n, an edge e in n's plane has +// the square |e|^2 - (e.n)^2, and two have the dot e1.e2 - (e1.n)(e2.n). +// NEGATE is for a corner whose second edge is -e2. Nothing where the +// tangent or either edge has no length in the plane. +template +AL_SIMD_INLINE TangentShare corner_share(const Lanes3& tangent, const Lanes3& n, typename W::reg e1_n, typename W::reg e2_n, + typename W::reg e1_e1, typename W::reg e2_e2, typename W::reg e1_e2) +{ + using reg = typename W::reg; + const reg length1 = W::max(W::fnmadd(e1_n, e1_n, e1_e1), W::zero()); + const reg length2 = W::max(W::fnmadd(e2_n, e2_n, e2_e2), W::zero()); + reg between = W::fnmadd(e1_n, e2_n, e1_e2); + if constexpr (NEGATE) + { + between = W::neg(between); + } + // 1 / (|p1| |p2|): the cosine is their dot times it, and |p1| |p2| is + // the product of their squares times it. + const reg product = W::mul(length1, length2); + const reg inverse = inverse_root(product); + const reg angle = tangent_acos(W::mul(between, inverse)); + const Lanes3 s = in_plane_lanes3(tangent, n); + return { s, W::mul(W::mul(angle, W::mul(product, inverse)), inverse_root(dot_lanes3(s, s))) }; +} + +// A vertex's sum of shares made its tangent: put back in the plane of its +// normal and normalized, or, where nothing was added, +x in that plane, +// +y where the normal is within 45 degrees of x. count, the sum of the +// signs of the vertex's triangles, becomes +1 or -1 as it leans, and 0 +// where it is 0. +template +AL_SIMD_INLINE void finish_tangent(Lanes3& sum, typename W::reg& count, const Lanes3& n) +{ + using reg = typename W::reg; + using mask = typename W::mask; + const reg zero = W::zero(); + const reg one = W::set1(1.f); + sum = in_plane_lanes3(sum, n); + const mask near_x = W::cmpgt(W::mul(n.x, n.x), W::set1(0.5f)); + const Lanes3 axis = { W::select(near_x, zero, one), W::select(near_x, one, zero), zero }; + const Lanes3 fallback = in_plane_lanes3(axis, n); + const mask none = W::cmplt(dot_lanes3(sum, sum), W::set1(FLT_MIN)); + sum = { W::select(none, fallback.x, sum.x), W::select(none, fallback.y, sum.y), W::select(none, fallback.z, sum.z) }; + sum = scale_lanes3(sum, W::rsqrt(dot_lanes3(sum, sum))); + count = W::select(W::cmplt(count, zero), W::set1(-1.f), W::select(W::cmpgt(count, zero), one, zero)); +} + +// A register's lanes added up. +template +AL_SIMD_INLINE F32 lane_sum(typename K::reg v) +{ + F32 lanes[K::floats]; + K::storeu(lanes, v); + F32 sum = 0.f; + for (F32 lane : lanes) + { + sum += lane; + } + return sum; +} + +// A tangent finished with no sign takes the face's: the sign of the sum +// of its triangles' signs, or -1 where that is 0. +inline void sign_faceless(F32 total, LLVector4a* tangents, size_t n) +{ + const F32 sign = total > 0.f ? 1.f : -1.f; + for (size_t i = 0; i < n; ++i) + { + if (tangents[i][3] == 0.f) + { + tangents[i].getF32ptr()[3] = sign; + } + } +} + +// Finishes the sums held as vectors in tangents, x, y and z the sum and w +// the count, W::floats at a time. +template +void finish_vectors(const LLVector4a* normals, LLVector4a* tangents, size_t n, F32 total) +{ + using reg = typename W::reg; + constexpr size_t F = W::floats; + typename W::mask faceless = W::none(); + alignas(16) F32 spill[4]; + for (size_t first = 0; first < n; first += F) + { + // Lanes past the last vertex read it and write nowhere it matters. + const auto vertex = [&](int i) { return first + i < n ? first + i : n - 1; }; + Lanes3 sum, nrm; + reg count, unused; + W::load_columns([&](int i) { return tangents[vertex(i)].getF32ptr(); }, sum.x, sum.y, sum.z, count); + W::load_columns([&](int i) { return normals[vertex(i)].getF32ptr(); }, nrm.x, nrm.y, nrm.z, unused); + finish_tangent(sum, count, nrm); + const typename W::mask real = W::cmplt(W::add(W::ramp(), W::set1(F32(first))), W::set1(F32(n))); + faceless = W::or_(faceless, W::and_(real, W::cmpeq(count, W::zero()))); + W::store_columns(sum.x, sum.y, sum.z, count, [&](int i) { return first + i < n ? tangents[first + i].getF32ptr() : spill; }); + } + if (W::any(faceless)) + { + sign_faceless(total, tangents, n); + } +} + +// count triangles, up to K::floats, from triangle `first`: their shares +// added to their vertices' sums in tangents, and their signs to total. +template +AL_SIMD_INLINE void indexed_triangles(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, const U16* indices, + size_t first, size_t count, LLVector4a* tangents, typename K::reg& total) +{ + using reg = typename K::reg; + constexpr size_t F = K::floats; + // A lane past the last triangle is vertex 0 three times: a point, + // which adds nothing. + U32 corner[3][F]; + for (size_t i = 0; i < F; ++i) + { + for (int k = 0; k < 3; ++k) + { + corner[k][i] = i < count ? indices[(first + i) * 3 + k] : 0; + } + } + Lanes3 pa, pb, pc, na, nb, nc; + reg unused, ua, va, ub, vb, uc, vc; + K::load_columns([&](int i) { return positions[corner[0][i]].getF32ptr(); }, pa.x, pa.y, pa.z, unused); + K::load_columns([&](int i) { return positions[corner[1][i]].getF32ptr(); }, pb.x, pb.y, pb.z, unused); + K::load_columns([&](int i) { return positions[corner[2][i]].getF32ptr(); }, pc.x, pc.y, pc.z, unused); + K::load_columns([&](int i) { return normals[corner[0][i]].getF32ptr(); }, na.x, na.y, na.z, unused); + K::load_columns([&](int i) { return normals[corner[1][i]].getF32ptr(); }, nb.x, nb.y, nb.z, unused); + K::load_columns([&](int i) { return normals[corner[2][i]].getF32ptr(); }, nc.x, nc.y, nc.z, unused); + K::load_pairs([&](int i) { return uvs[corner[0][i]].mV; }, ua, va); + K::load_pairs([&](int i) { return uvs[corner[1][i]].mV; }, ub, vb); + K::load_pairs([&](int i) { return uvs[corner[2][i]].mV; }, uc, vc); + + const Lanes3 ab = sub_lanes3(pb, pa); + const Lanes3 ac = sub_lanes3(pc, pa); + const Lanes3 bc = sub_lanes3(pc, pb); + const reg ab_ab = dot_lanes3(ab, ab); + const reg ac_ac = dot_lanes3(ac, ac); + const reg bc_bc = dot_lanes3(bc, bc); + Lanes3 tangent; + reg sign; + triangle_tangent(ab, ac, K::add(ab_ab, ac_ac), K::sub(ub, ua), K::sub(vb, va), K::sub(uc, ua), K::sub(vc, va), K::set1(1.f), + tangent, sign); + total = K::add(total, sign); + + const TangentShare share[3] = { + corner_share(tangent, na, dot_lanes3(ab, na), dot_lanes3(ac, na), ab_ab, ac_ac, dot_lanes3(ab, ac)), + corner_share(tangent, nb, dot_lanes3(bc, nb), dot_lanes3(ab, nb), bc_bc, ab_ab, dot_lanes3(bc, ab)), + corner_share(tangent, nc, dot_lanes3(ac, nc), dot_lanes3(bc, nc), ac_ac, bc_bc, dot_lanes3(ac, bc)), + }; + LLVector4a shares[3][F]; + for (int k = 0; k < 3; ++k) + { + const Lanes3 scaled = scale_lanes3(share[k].direction, share[k].weight); + K::store_columns(scaled.x, scaled.y, scaled.z, sign, [&](int i) { return shares[k][i].getF32ptr(); }); + } + // The shares go to their vertices in index order, one corner at a + // time: one vertex can be a corner of several triangles in a + // register, and the order of its sum is then the same at any width. + for (size_t i = 0; i < count; ++i) + { + for (int k = 0; k < 3; ++k) + { + tangents[corner[k][i]].add(shares[k][i]); + } + } +} + +// Triangles W::floats at a time, and the last half register's worth or +// less four at a time. +template +void indexed_tangents_impl(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t vertex_count, + const U16* indices, size_t index_count, LLVector4a* tangents) +{ + constexpr size_t F = W::floats; + if (!vertex_count) + { + return; + } + std::memset(static_cast(tangents), 0, vertex_count * sizeof(LLVector4a)); + const size_t triangles = index_count / 3; + typename W::reg total = W::zero(); + typename ALSimd4::reg total4 = ALSimd4::zero(); + size_t first = 0; + for (; first < triangles && triangles - first > F / 2; first += F) + { + indexed_triangles(positions, normals, uvs, indices, first, triangles - first < F ? triangles - first : F, tangents, total); + } + if (first < triangles) + { + indexed_triangles(positions, normals, uvs, indices, first, triangles - first, tangents, total4); + } + finish_vectors(normals, tangents, vertex_count, lane_sum(total) + lane_sum(total4)); +} + +// The grid as lanes, one vertex a lane: its positions, normals and UVs, +// and the sums of each vertex's shares from the quads it is the bottom +// left, bottom right, top left and top right corner of, kept apart so +// that the sums one register of quads writes never overlap. +struct TangentLanes +{ + F32* position[3]; + F32* normal[3]; + F32* uv[2]; + F32* sums[4][4]; +}; + +// The quads whose bottom left corners are lanes q to q + K::floats - 1: +// each is two triangles, bottom left, bottom right, top right and bottom +// 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. +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) +{ + using reg = typename K::reg; + const reg across_f = K::set1(F32(across)); + const size_t bl_at = q; + const size_t br_at = q + 1; + 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 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); + const Lanes3 br = load_lanes3(lanes.position, br_at); + const Lanes3 tl = load_lanes3(lanes.position, tl_at); + const Lanes3 tr = load_lanes3(lanes.position, tr_at); + const reg bl_u = K::loadu(lanes.uv[0] + bl_at); + const reg bl_v = K::loadu(lanes.uv[1] + bl_at); + const reg br_u = K::loadu(lanes.uv[0] + br_at); + const reg br_v = K::loadu(lanes.uv[1] + br_at); + const reg tl_u = K::loadu(lanes.uv[0] + tl_at); + const reg tl_v = K::loadu(lanes.uv[1] + tl_at); + const reg tr_u = K::loadu(lanes.uv[0] + tr_at); + const reg tr_v = K::loadu(lanes.uv[1] + tr_at); + + const Lanes3 bottom = sub_lanes3(br, bl); + const Lanes3 right = sub_lanes3(tr, br); + const Lanes3 diagonal = sub_lanes3(tr, bl); + const Lanes3 left = sub_lanes3(tl, bl); + const Lanes3 top = sub_lanes3(tr, tl); + + const reg bottom2 = dot_lanes3(bottom, bottom); + const reg right2 = dot_lanes3(right, right); + const reg diagonal2 = dot_lanes3(diagonal, diagonal); + const reg left2 = dot_lanes3(left, left); + const reg top2 = dot_lanes3(top, top); + + Lanes3 lower, upper; + reg lower_sign, upper_sign; + triangle_tangent(bottom, diagonal, K::add(bottom2, diagonal2), K::sub(br_u, bl_u), K::sub(br_v, bl_v), K::sub(tr_u, bl_u), + K::sub(tr_v, bl_v), real, lower, lower_sign); + triangle_tangent(diagonal, left, K::add(diagonal2, left2), K::sub(tr_u, bl_u), K::sub(tr_v, bl_v), K::sub(tl_u, bl_u), + K::sub(tl_v, bl_v), real, upper, upper_sign); + lower_sign = K::mul(lower_sign, K::set1(winding)); + upper_sign = K::mul(upper_sign, K::set1(winding)); + const reg both_signs = K::add(lower_sign, upper_sign); + total = K::add(total, both_signs); + + const auto store = [](F32* const* sums, size_t at, const Lanes3& sum, reg sign) + { + K::storeu(sums[0] + at, sum.x); + K::storeu(sums[1] + at, sum.y); + K::storeu(sums[2] + at, sum.z); + K::storeu(sums[3] + at, K::add(K::loadu(sums[3] + at), sign)); + }; + { + const Lanes3 n = load_lanes3(lanes.normal, bl_at); + const reg bottom_n = dot_lanes3(bottom, n); + const reg diagonal_n = dot_lanes3(diagonal, n); + const reg left_n = dot_lanes3(left, n); + Lanes3 sum = add_share(load_lanes3(lanes.sums[0], bl_at), + corner_share(lower, n, bottom_n, diagonal_n, bottom2, diagonal2, dot_lanes3(bottom, diagonal))); + sum = add_share(sum, corner_share(upper, n, diagonal_n, left_n, diagonal2, left2, dot_lanes3(diagonal, left))); + store(lanes.sums[0], bl_at, sum, both_signs); + } + { + const Lanes3 n = load_lanes3(lanes.normal, br_at); + const Lanes3 sum = add_share(load_lanes3(lanes.sums[1], br_at), + corner_share(lower, n, dot_lanes3(right, n), dot_lanes3(bottom, n), right2, bottom2, + dot_lanes3(right, bottom))); + store(lanes.sums[1], br_at, sum, lower_sign); + } + { + const Lanes3 n = load_lanes3(lanes.normal, tr_at); + const reg diagonal_n = dot_lanes3(diagonal, n); + const reg right_n = dot_lanes3(right, n); + const reg top_n = dot_lanes3(top, n); + Lanes3 sum = add_share(load_lanes3(lanes.sums[3], tr_at), + corner_share(lower, n, diagonal_n, right_n, diagonal2, right2, dot_lanes3(diagonal, right))); + sum = add_share(sum, corner_share(upper, n, top_n, diagonal_n, top2, diagonal2, dot_lanes3(top, diagonal))); + store(lanes.sums[3], tr_at, sum, both_signs); + } + { + const Lanes3 n = load_lanes3(lanes.normal, tl_at); + const Lanes3 sum = add_share(load_lanes3(lanes.sums[2], tl_at), + corner_share(upper, n, dot_lanes3(left, n), dot_lanes3(top, n), left2, top2, + dot_lanes3(left, top))); + store(lanes.sums[2], tl_at, sum, upper_sign); + } +} + +template +void grid_tangents_impl(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t columns, size_t rows, + LLVector4a* tangents, std::vector& scratch) +{ + using reg = typename W::reg; + using mask = typename W::mask; + constexpr size_t F = W::floats; + if (!columns || !rows) + { + return; + } + // The grid's lines laid end to end, a line a row, or a column where + // that leaves at least a quarter fewer lanes to take. A quad's lane is + // its bottom left corner's, and the lane at the end of each line, whose + // quad would wrap onto the next, holds none. Down a column, the quads' + // corners are the grid's with left and right turned to bottom and top, + // which makes the same two triangles wound the other way: the same + // tangents, and the opposite signs. + const bool down = 4 * rows * (columns - 1) < 3 * columns * (rows - 1); + const size_t across = down ? rows : columns; + const size_t lines = down ? columns : rows; + const size_t vertices = across * lines; + const size_t quads = across * (lines - 1); + const F32 winding = down ? -1.f : 1.f; + + // The lanes run on past the grid by at least a register, filled with + // its last vertex: a quad's top right corner is a line and a lane on. + const size_t stride = (vertices + 2 * F - 1) / F * F; + if (scratch.size() < 24 * stride) + { + scratch.resize(24 * stride); + } + TangentLanes lanes; + F32* next = scratch.data(); + for (F32*& lane : lanes.position) { lane = next; next += stride; } + for (F32*& lane : lanes.normal) { lane = next; next += stride; } + for (F32*& lane : lanes.uv) { lane = next; next += stride; } + for (auto& corner : lanes.sums) + { + for (F32*& lane : corner) { lane = next; next += stride; } + } + + // Lane v's vertex: v itself along the rows, and down the columns its + // place along its column times the grid's width, plus the column. + size_t place = 0; + size_t line = 0; + const auto vertex_of = [&](size_t v) + { + size_t index = vertices - 1; + if (!down) + { + index = v < vertices ? v : vertices - 1; + } + else if (v < vertices) + { + index = place * columns + line; + if (++place == across) + { + place = 0; + ++line; + } + } + return index; + }; + size_t first = 0; + for (; first < vertices; first += F) + { + size_t index[F]; + for (size_t i = 0; i < F; ++i) + { + index[i] = vertex_of(first + i); + } + reg x, y, z, w, u, v; + W::load_columns([&](int i) { return positions[index[i]].getF32ptr(); }, x, y, z, w); + W::storeu(lanes.position[0] + first, x); + W::storeu(lanes.position[1] + first, y); + W::storeu(lanes.position[2] + first, z); + W::load_columns([&](int i) { return normals[index[i]].getF32ptr(); }, x, y, z, w); + W::storeu(lanes.normal[0] + first, x); + W::storeu(lanes.normal[1] + first, y); + W::storeu(lanes.normal[2] + first, z); + W::load_pairs([&](int i) { return uvs[index[i]].mV; }, u, v); + W::storeu(lanes.uv[0] + first, u); + W::storeu(lanes.uv[1] + first, v); + for (auto& corner : lanes.sums) + { + for (F32* lane : corner) + { + W::storeu(lane + first, W::zero()); + } + } + } + // The lanes past the grid: its last vertex, which is the last of the + // walk either way. + for (; first < stride; first += F) + { + for (int k = 0; k < 3; ++k) + { + W::storeu(lanes.position[k] + first, W::set1(positions[vertices - 1][k])); + W::storeu(lanes.normal[k] + first, W::set1(normals[vertices - 1][k])); + } + W::storeu(lanes.uv[0] + first, W::set1(uvs[vertices - 1].mV[0])); + W::storeu(lanes.uv[1] + first, W::set1(uvs[vertices - 1].mV[1])); + for (auto& corner : lanes.sums) + { + for (F32* lane : corner) + { + W::storeu(lane + first, W::zero()); + } + } + } + + reg total = W::zero(); + typename ALSimd4::reg total4 = ALSimd4::zero(); + size_t q = 0; + for (; q < quads && quads - q > F / 2; q += F) + { + grid_quads(lanes, q, across, quads, winding, total); + } + if (q < quads) + { + grid_quads(lanes, q, across, quads, winding, total4); + } + + // The sums made tangents, back at their vertices. + mask faceless = W::none(); + alignas(16) F32 spill[4]; + place = 0; + line = 0; + for (size_t first = 0; first < vertices; first += F) + { + Lanes3 sum = add_lanes3(add_lanes3(load_lanes3(lanes.sums[0], first), load_lanes3(lanes.sums[1], first)), + add_lanes3(load_lanes3(lanes.sums[2], first), load_lanes3(lanes.sums[3], first))); + reg count = W::add(W::add(W::loadu(lanes.sums[0][3] + first), W::loadu(lanes.sums[1][3] + first)), + W::add(W::loadu(lanes.sums[2][3] + first), W::loadu(lanes.sums[3][3] + first))); + finish_tangent(sum, count, load_lanes3(lanes.normal, first)); + const mask real = W::cmplt(W::add(W::ramp(), W::set1(F32(first))), W::set1(F32(vertices))); + faceless = W::or_(faceless, W::and_(real, W::cmpeq(count, W::zero()))); + size_t index[F]; + for (size_t i = 0; i < F; ++i) + { + index[i] = vertex_of(first + i); + } + W::store_columns(sum.x, sum.y, sum.z, count, + [&](int i) { return first + i < vertices ? tangents[index[i]].getF32ptr() : spill; }); + } + if (W::any(faceless)) + { + sign_faceless(lane_sum(total) + lane_sum(total4), tangents, vertices); + } +} + } // namespace kernels } // namespace alsimd diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index ca336fd630..cd8049b6b6 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -6436,18 +6436,32 @@ void LLVolumeFace::createTangents() } mTangentsStale = false; - //generate tangents - LLVector4a* ptr = (LLVector4a*)mTangents; + if (mOptimized) + { + // A face cacheOptimize has taken is a mesh LOD's, too large + // for its weld or not welded at all: Lengyel's tangents, which + // add no vertices. + LLVector4a* ptr = (LLVector4a*)mTangents; - LLVector4a* end = mTangents + mNumVertices; - while (ptr < end) + LLVector4a* end = mTangents + mNumVertices; + while (ptr < end) + { + (*ptr++).clear(); + } + + LLCalculateTangentArray(mNumVertices, mPositions, mNormals, mTexCoords, mNumIndices / 3, mIndices, mTangents); + } + else if (!(mTypeMask & CAP_MASK) && mNumS > 0 && mNumT > 0 && mNumS * mNumT == mNumVertices && + mNumIndices == (mNumS - 1) * (mNumT - 1) * 6) { - (*ptr++).clear(); + // A side is createSide's grid. + alsimd::grid_tangents(mPositions, mNormals, mTexCoords, mNumS, mNumT, mTangents); + } + else + { + alsimd::indexed_tangents(mPositions, mNormals, mTexCoords, mNumVertices, mIndices, mNumIndices, mTangents); } - - LLCalculateTangentArray(mNumVertices, mPositions, mNormals, mTexCoords, mNumIndices / 3, mIndices, mTangents); } - } void LLVolumeFace::resizeVertices(S32 num_verts) diff --git a/indra/llmath/tests/alsimdkernels_test.cpp b/indra/llmath/tests/alsimdkernels_test.cpp index d20a597d4b..069c635c22 100644 --- a/indra/llmath/tests/alsimdkernels_test.cpp +++ b/indra/llmath/tests/alsimdkernels_test.cpp @@ -751,4 +751,148 @@ namespace } } } + + // ------------------------------------------------------------------ + // Tangents + + // The widths the lane kernels have. + template + void for_each_lane_width(Body body) + { + body.template operator()("ALSimd4"); +#if AL_SIMD_AVX + body.template operator()("ALSimd8"); +#endif + } + + // A grid of columns by rows over a wavy sheet, with the sheet's normals + // and the grid's UVs, indexed as createSide indexes one. Column 2 lies + // on column 1 and row 2 has row 1's UVs, so that some quads are lines + // and some triangles have no UV area. + struct TangentGrid + { + std::vector positions; + std::vector normals; + std::vector uvs; + std::vector indices; + }; + + TangentGrid tangent_grid(size_t columns, size_t rows, U32 seed) + { + std::mt19937 rng(seed); + std::uniform_real_distribution jitter(-0.2f, 0.2f); + TangentGrid g; + g.positions.resize(columns * rows); + g.normals.resize(columns * rows); + g.uvs.resize(columns * rows); + for (size_t t = 0; t < rows; ++t) + { + for (size_t s = 0; s < columns; ++s) + { + const size_t at = s + columns * t; + const size_t column = (s == 2) ? 1 : s; + const F32 x = F32(column) * 0.3f + (column == s ? jitter(rng) * 0.3f : 0.f); + const F32 y = F32(t) * 0.25f + jitter(rng) * 0.25f; + g.positions[at].set(x, y, 0.3f * sinf(x) * cosf(y), 0.f); + LLVector4a normal(-0.3f * cosf(x) * cosf(y), 0.3f * sinf(x) * sinf(y), 1.f, 0.f); + normal.normalize3(); + g.normals[at] = normal; + const size_t row = (t == 2) ? 1 : t; + g.uvs[at].set(F32(s) / F32(llmax(columns, size_t(2)) - 1), F32(row) / F32(llmax(rows, size_t(2)) - 1)); + } + if (columns > 2) + { + g.positions[2 + columns * t] = g.positions[1 + columns * t]; + } + } + 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); + for (U16 i : { bl, tr, tl, bl, br, tr }) + { + g.indices.push_back(i); + } + } + } + return g; + } + + F64 degrees_apart(const LLVector4a& a, const LLVector4a& b) + { + const F64 cx = (F64)a[1] * b[2] - (F64)a[2] * b[1]; + const F64 cy = (F64)a[2] * b[0] - (F64)a[0] * b[2]; + const F64 cz = (F64)a[0] * b[1] - (F64)a[1] * b[0]; + const F64 dot = (F64)a[0] * b[0] + (F64)a[1] * b[1] + (F64)a[2] * b[2]; + return std::atan2(std::sqrt(cx * cx + cy * cy + cz * cz), dot) * 180.0 / F_PI; + } + + // The grid and indexed forms at every width against the 128-bit ones, + // over grids that leave every tail: each lane runs the same + // instructions, but the compiler may reassociate the reciprocal square + // root's refinement differently in each, so to within a few ulps, with + // the same w. And against each other, which differ only in the order of + // their sums: within rounding, the same w, unit length and orthogonal + // to the normal. Nothing is written past the grid. + TEST_CASE("alsimdkernels: tangents at every width, and the grid form against the indexed", "[alsimdkernels]") + { + std::vector scratch; + U32 seed = 20; + for (size_t columns : { 1, 2, 3, 4, 5, 8, 9, 16, 17, 23 }) + { + for (size_t rows : { 1, 2, 3, 6 }) + { + const TangentGrid g = tangent_grid(columns, rows, ++seed); + const size_t n = columns * rows; + std::vector grid(n + 1, SENTINEL); + std::vector indexed(n + 1, SENTINEL); + grid_tangents_impl(g.positions.data(), g.normals.data(), g.uvs.data(), columns, rows, grid.data(), scratch); + indexed_tangents_impl(g.positions.data(), g.normals.data(), g.uvs.data(), n, g.indices.data(), g.indices.size(), + indexed.data()); + CAPTURE(columns, rows); + require_same_bits("past the grid form's end", grid[n], SENTINEL); + require_same_bits("past the indexed form's end", indexed[n], SENTINEL); + for (size_t v = 0; v < n; ++v) + { + CAPTURE(v); + const F64 apart = degrees_apart(grid[v], indexed[v]); + INFO("grid (" << precise(grid[v][0]) << ", " << precise(grid[v][1]) << ", " << precise(grid[v][2]) << ", " << grid[v][3] + << ") and indexed (" << precise(indexed[v][0]) << ", " << precise(indexed[v][1]) << ", " + << precise(indexed[v][2]) << ", " << indexed[v][3] << ") are " << precise(apart) << " degrees apart"); + REQUIRE(apart <= 1e-3); + REQUIRE(grid[v][3] == indexed[v][3]); + REQUIRE((grid[v][3] == 1.f || grid[v][3] == -1.f)); + REQUIRE(std::fabs(grid[v].getLength3().getF32() - 1.f) <= 1e-5f); + REQUIRE(std::fabs(grid[v].dot3(g.normals[v]).getF32()) <= 1e-5f); + } + + for_each_lane_width([&](const char* width) + { + CAPTURE(width); + std::vector wide_grid(n + 1, SENTINEL); + std::vector wide_indexed(n + 1, SENTINEL); + grid_tangents_impl(g.positions.data(), g.normals.data(), g.uvs.data(), columns, rows, wide_grid.data(), scratch); + indexed_tangents_impl(g.positions.data(), g.normals.data(), g.uvs.data(), n, g.indices.data(), g.indices.size(), + wide_indexed.data()); + require_same_bits("past the grid form's end", wide_grid[n], SENTINEL); + require_same_bits("past the indexed form's end", wide_indexed[n], SENTINEL); + for (size_t v = 0; v < n; ++v) + { + CAPTURE(v); + for (int lane = 0; lane < 3; ++lane) + { + require_close("grid form", wide_grid[v][lane], grid[v][lane], 4.0 * std::numeric_limits::epsilon()); + require_close("indexed form", wide_indexed[v][lane], indexed[v][lane], 4.0 * std::numeric_limits::epsilon()); + } + REQUIRE(wide_grid[v][3] == grid[v][3]); + REQUIRE(wide_indexed[v][3] == indexed[v][3]); + } + }); + } + } + } } diff --git a/indra/llmath/tests/llvolume_bench.cpp b/indra/llmath/tests/llvolume_bench.cpp index 4292ba1435..3942ec41a4 100644 --- a/indra/llmath/tests/llvolume_bench.cpp +++ b/indra/llmath/tests/llvolume_bench.cpp @@ -368,6 +368,21 @@ namespace } }; + // A face's tangents by Lengyel's generator, into a buffer of their + // own, allocated and cleared as createTangents did. + F32 lengyel(const LLVolumeFace& face) + { + LLVector4a* tangents = (LLVector4a*)ll_aligned_malloc_16(face.mNumVertices * sizeof(LLVector4a)); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + tangents[v].clear(); + } + LLCalculateTangentArray(face.mNumVertices, face.mPositions, face.mNormals, face.mTexCoords, face.mNumIndices / 3, face.mIndices, tangents); + const F32 first = tangents[0][0]; + ll_aligned_free_16(tangents); + return first; + } + struct FaceClass { std::string name; @@ -592,23 +607,30 @@ namespace } } - // createTangents is Lengyel's generator; meshoptimizer's default mode - // weights by angle times the edges' lengths, its compatible mode by - // angle alone, as MikkTSpace does. Each row makes one face's tangents - // a vertex. + // createTangents sums meshoptimizer's default weighting a vertex at a + // time, over the grid on a side and over the indices elsewhere; Lengyel + // is the generator a face cacheOptimize has taken keeps, and the one + // createTangents was. meshoptimizer's default mode weights by angle + // times the edges' lengths a corner at a time, its compatible mode by + // angle alone, as MikkTSpace does. Each row makes one face's tangents a + // vertex, the allocation included where the generator makes one. TEST_CASE("llvolume_bench: one face's tangents from each generator", "[llvolume_bench][!benchmark]") { ll_test::skipUnlessOptimised(); for (const FaceClass& face_class : faceClasses()) { LLVolumeFace& face = face_class.volume->getVolumeFace(face_class.face); - BENCHMARK(fmt::format("Lengyel (createTangents), {}", face_class.name)) + BENCHMARK(fmt::format("createTangents, {}", face_class.name)) { ll_aligned_free_16(face.mTangents); face.mTangents = nullptr; face.createTangents(); return face.mTangents[0][0]; }; + BENCHMARK(fmt::format("Lengyel, {}", face_class.name)) + { + return lengyel(face); + }; TangentInputs inputs(face); BENCHMARK(fmt::format("meshoptimizer default, {}", face_class.name)) { @@ -623,13 +645,17 @@ namespace { LLVolumeFace face = mesh.face(); const std::string name = fmt::format("{}, {} vertices", mesh.name, face.mNumVertices); - BENCHMARK(fmt::format("Lengyel (createTangents), {}", name)) + BENCHMARK(fmt::format("createTangents, {}", name)) { ll_aligned_free_16(face.mTangents); face.mTangents = nullptr; face.createTangents(); return face.mTangents[0][0]; }; + BENCHMARK(fmt::format("Lengyel, {}", name)) + { + return lengyel(face); + }; TangentInputs inputs(face); BENCHMARK(fmt::format("meshoptimizer default, {}", name)) { diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 5ffc348831..b283c6aa56 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -567,6 +567,7 @@ namespace constexpr U32 Tangents = 1 << 9; // after genTangents: unit normals, unit tangents, w = +/-1 constexpr U32 Orthogonal = 1 << 10; // after genTangents: tangent orthogonal to normal constexpr U32 LoneNormals = 1 << 11; // a vertex in no triangle with area takes a normal the face has + constexpr U32 Meshopt = 1 << 12; // after genTangents: meshoptimizer's tangents, where it gives a vertex one // What the sweep holds every face to. Orthogonal is broken wherever // the tangent generator makes a tangent up, and has a case of its @@ -620,6 +621,8 @@ namespace U32 only_regions = 0; // Check inversion also where the surface folds at a point. bool inversion_any_spread = false; + // Where Checks::Meshopt keeps what it found. + struct TangentAgreement* agreement = nullptr; }; // ------------------------------------------------------------------ @@ -1157,6 +1160,271 @@ namespace } } + // What comparing tangents with meshoptimizer's found over a run. + struct TangentAgreement + { + S32 compared = 0; // vertices it gives one tangent with a length + S32 split = 0; // vertices whose corners it gives more than one + S32 lone = 0; // vertices it gives no tangent + S32 unsure = 0; // vertices whose tangent rounding decides + F64 widest = 0.0; // degrees between the two, at most + std::string widest_at; + F64 nearest_bound = 0.0; // the most of a vertex's bound the two came apart by + std::string nearest_bound_at; + }; + + F64 degreesApart(const LLVector3& a, const LLVector3& b) + { + const F64 cx = (F64)a.mV[1] * b.mV[2] - (F64)a.mV[2] * b.mV[1]; + const F64 cy = (F64)a.mV[2] * b.mV[0] - (F64)a.mV[0] * b.mV[2]; + const F64 cz = (F64)a.mV[0] * b.mV[1] - (F64)a.mV[1] * b.mV[0]; + const F64 dot = (F64)a.mV[0] * b.mV[0] + (F64)a.mV[1] * b.mV[1] + (F64)a.mV[2] * b.mV[2]; + return atan2(sqrt(cx * cx + cy * cy + cz * cz), dot) * 180.0 / F_PI; + } + + // Whether a triangle adds nothing to tangents: twice its area, in + // positions or in UVs, under 1e-5 of the sum of the squares of the two + // edges from its first corner. + bool tangentFlat(const LLVolumeFace& face, U32 a, U32 b, U32 c) + { + const LLVector3 pa = v3(face.mPositions[a]); + const LLVector3 ab = v3(face.mPositions[b]) - pa; + const LLVector3 ac = v3(face.mPositions[c]) - pa; + const LLVector2 uv_ab = face.mTexCoords[b] - face.mTexCoords[a]; + const LLVector2 uv_ac = face.mTexCoords[c] - face.mTexCoords[a]; + const F64 edges = (F64)ab.lengthSquared() + ac.lengthSquared(); + const F64 uv_edges = (F64)uv_ab.lengthSquared() + uv_ac.lengthSquared(); + const F64 area = (F64)uv_ab.mV[0] * uv_ac.mV[1] - (F64)uv_ab.mV[1] * uv_ac.mV[0]; + return (F64)(ab % ac).lengthSquared() <= 1e-10 * edges * edges || area * area <= 1e-10 * uv_edges * uv_edges; + } + + // How far rounding can turn each vertex's tangent, in degrees: its sum + // of corner shares worked again in double, as meshoptimizer's default + // mode makes them, and each share's size times how far rounding can + // move the share, against the size of the sum. A share's direction + // moves as far as its triangle's tangent can, from the rounding of the + // positions and UVs it is made from and of the chain rule's two terms, + // over the share's length in the normal's plane, short where the + // tangent nearly lies along the normal. Its weight moves as far as its + // edges' lengths in that plane can: short where an edge nearly lies + // along the normal, and shorter still to round where the square is + // taken as the edge's own less its square along the normal, as + // createTangents takes it. And as far as their angle can: near 0 or + // pi, where the arc cosine is a square root, by the square root of the + // cosine's rounding, however small the weight; and at a right angle by + // the step its arc cosine takes there, about 1e-3, where rounding + // decides the cosine's sign. 180 where the sum is + // nothing, or where a triangle's UVs wind one way or the other only by + // rounding, or it is nearly flat enough for one of the two to leave out + // and not the other. + std::vector tangentRoundingDegrees(const LLVolumeFace& face) + { + constexpr F64 EPSILON = 1.0 / (1 << 24); + using V = std::array; + auto at = [](const LLVector4a& v) { return V{ v[0], v[1], v[2] }; }; + auto sub = [](const V& a, const V& b) { return V{ a[0] - b[0], a[1] - b[1], a[2] - b[2] }; }; + auto dot = [](const V& a, const V& b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }; + auto size = [&](const V& a) { return sqrt(dot(a, a)); }; + auto less = [&](const V& v, const V& n) + { + const F64 d = dot(v, n); + return V{ v[0] - n[0] * d, v[1] - n[1] * d, v[2] - n[2] * d }; + }; + std::vector sums(face.mNumVertices, { 0.0, 0.0, 0.0 }); + std::vector spread(face.mNumVertices, 0.0); + std::vector unsure(face.mNumVertices, false); + for (S32 i = 0; i + 2 < face.mNumIndices; i += 3) + { + const U32 corner[3] = { face.mIndices[i], face.mIndices[i + 1], face.mIndices[i + 2] }; + const V pa = at(face.mPositions[corner[0]]); + const V pb = at(face.mPositions[corner[1]]); + const V pc = at(face.mPositions[corner[2]]); + const LLVector2& ta = face.mTexCoords[corner[0]]; + const LLVector2& tb = face.mTexCoords[corner[1]]; + const LLVector2& tc = face.mTexCoords[corner[2]]; + const V dp1 = sub(pb, pa); + const V dp2 = sub(pc, pa); + const F64 dt1x = (F64)tb.mV[0] - ta.mV[0]; + const F64 dt1y = (F64)tb.mV[1] - ta.mV[1]; + const F64 dt2x = (F64)tc.mV[0] - ta.mV[0]; + const F64 dt2y = (F64)tc.mV[1] - ta.mV[1]; + + // How flat it is, in positions and in UVs, against the flat + // triangle's bound. + const V normal = { dp1[1] * dp2[2] - dp1[2] * dp2[1], dp1[2] * dp2[0] - dp1[0] * dp2[2], dp1[0] * dp2[1] - dp1[1] * dp2[0] }; + const F64 det = dt1x * dt2y - dt1y * dt2x; + const F64 flat = llmin(size(normal) / (dot(dp1, dp1) + dot(dp2, dp2)), fabs(det) / (dt1x * dt1x + dt1y * dt1y + dt2x * dt2x + dt2y * dt2y)); + // How far the UVs' rounding can move the UV area. + const F64 det_noise = EPSILON * ((fabs(ta.mV[0]) + fabs(tb.mV[0])) * fabs(dt2y) + fabs(dt1x) * (fabs(ta.mV[1]) + fabs(tc.mV[1])) + + (fabs(ta.mV[1]) + fabs(tb.mV[1])) * fabs(dt2x) + fabs(dt1y) * (fabs(ta.mV[0]) + fabs(tc.mV[0])) + + 2.0 * fabs(dt1x * dt2y) + 2.0 * fabs(dt1y * dt2x)); + const bool uncertain = (flat > 0.25e-5 && flat < 4e-5) || (fabs(det) <= 4.0 * det_noise && !(flat <= 0.25e-5)); + if (uncertain) + { + for (U32 v : corner) + { + unsure[v] = true; + } + } + if (flat <= 1e-5) + { + continue; + } + + const V r = { dt2y * dp1[0] - dt1y * dp2[0], dt2y * dp1[1] - dt1y * dp2[1], dt2y * dp1[2] - dt1y * dp2[2] }; + const F64 length = size(r); + if (length == 0.0) + { + continue; + } + const F64 scale = (det > 0.0 ? 1.0 : -1.0) / length; + const V t = { r[0] * scale, r[1] * scale, r[2] * scale }; + // How far r can turn, against its length, in units of EPSILON: + // the rounding of its terms and of the differences they are + // made from. + const F64 noise_dt1y = fabs(ta.mV[1]) + fabs(tb.mV[1]) + fabs(dt1y); + const F64 noise_dt2y = fabs(ta.mV[1]) + fabs(tc.mV[1]) + fabs(dt2y); + const F64 noise_dp1 = size(pa) + size(pb) + size(dp1); + const F64 noise_dp2 = size(pa) + size(pc) + size(dp2); + const F64 turn = (noise_dt2y * size(dp1) + fabs(dt2y) * noise_dp1 + noise_dt1y * size(dp2) + fabs(dt1y) * noise_dp2 + + 2.0 * (fabs(dt2y) * size(dp1) + fabs(dt1y) * size(dp2))) / length; + for (int k = 0; k < 3; ++k) + { + const U32 v = corner[k]; + const V n = at(face.mNormals[v]); + const V e1 = sub(at(face.mPositions[corner[(k + 1) % 3]]), at(face.mPositions[v])); + const V e2 = sub(at(face.mPositions[corner[(k + 2) % 3]]), at(face.mPositions[v])); + const V st = less(t, n); + const V p1 = less(e1, n); + const V p2 = less(e2, n); + const F64 sl = size(st); + const F64 l1 = size(p1); + const F64 l2 = size(p2); + if (sl == 0.0 || l1 == 0.0 || l2 == 0.0) + { + continue; + } + const F64 cosine = llclamp(dot(p1, p2) / (l1 * l2), -1.0, 1.0); + const F64 ax = fabs(cosine); + F64 angle = (1.570337 + ax * (-0.2053972 + ax * 0.05147786)) * sqrt(1.0 - ax); + angle = cosine < 0.0 ? 3.1415926 - angle : angle; + const F64 weight = angle * l1 * l2; + for (int j = 0; j < 3; ++j) + { + sums[v][j] += st[j] * weight / sl; + } + const F64 lengths = 1.0 + (size(e1) + size(at(face.mPositions[v]))) / l1 + (size(e2) + size(at(face.mPositions[v]))) / l2 + + dot(e1, e1) / (l1 * l1) + dot(e2, e2) / (l2 * l2); + const F64 cosine_noise = 2.0 * EPSILON * lengths; + const F64 angle_noise = cosine_noise + 1.570337 * (sqrt(1.0 - ax + cosine_noise) - sqrt(1.0 - ax)) + + (ax <= cosine_noise ? 3.1415926 - 2.0 * 1.570337 : 0.0); + spread[v] += weight * EPSILON * ((turn + 4.0) / sl + lengths) + l1 * l2 * angle_noise; + } + } + std::vector degrees(face.mNumVertices, 180.0); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + const F64 length = size(sums[v]); + if (length > 0.0 && !unsure[v]) + { + degrees[v] = llmin(180.0, 4.0 * spread[v] / length * 180.0 / F_PI); + } + } + return degrees; + } + + // A face's tangents against meshoptimizer's default mode over its + // triangles that are not flat, which makes one tangent for each corner, + // and none where nothing adds to it: a vertex whose corners it gives + // one tangent must have that tangent's w, and its direction to within + // rounding, the larger of a thousandth of a degree and how far + // rounding can turn the vertex's sum. + constexpr F64 MESHOPT_TANGENT_DEGREES = 0.001; + + void checkMeshoptTangents(const LLVolumeFace& face, TangentAgreement& agreement, Report& report, const std::string& where) + { + if (!face.mTangents || !face.mNumIndices) + { + return; + } + std::vector indices; + for (S32 i = 0; i + 2 < face.mNumIndices; i += 3) + { + if (!tangentFlat(face, face.mIndices[i], face.mIndices[i + 1], face.mIndices[i + 2])) + { + indices.insert(indices.end(), face.mIndices + i, face.mIndices + i + 3); + } + } + if (indices.empty()) + { + return; + } + std::vector corners(indices.size() * 4); + meshopt_generateTangents(corners.data(), indices.data(), indices.size(), face.mPositions[0].getF32ptr(), face.mNumVertices, + sizeof(LLVector4a), face.mNormals[0].getF32ptr(), sizeof(LLVector4a), face.mTexCoords[0].mV, + sizeof(LLVector2), meshopt_TangentZeroFallback); + const std::vector rounding = tangentRoundingDegrees(face); + std::vector first(face.mNumVertices, -1); + std::vector split(face.mNumVertices, false); + for (size_t c = 0; c < indices.size(); ++c) + { + const U32 v = indices[c]; + if (first[v] < 0) + { + first[v] = (S32)c; + } + else if (std::memcmp(&corners[c * 4], &corners[first[v] * 4], 4 * sizeof(F32)) != 0) + { + split[v] = true; + } + } + bool failed = false; + for (S32 v = 0; v < face.mNumVertices; ++v) + { + if (first[v] < 0) + { + continue; + } + if (split[v]) + { + ++agreement.split; + continue; + } + const F32* theirs = &corners[first[v] * 4]; + const LLVector3 direction(theirs[0], theirs[1], theirs[2]); + if (direction.isExactlyZero()) + { + ++agreement.lone; + continue; + } + if (rounding[v] >= 180.0) + { + ++agreement.unsure; + continue; + } + ++agreement.compared; + const F64 apart = degreesApart(v3(face.mTangents[v]), direction); + const F64 bound = llmax(MESHOPT_TANGENT_DEGREES, rounding[v]); + if (apart > agreement.widest) + { + agreement.widest = apart; + agreement.widest_at = fmt::format("{}: vertex {}, bound {} degrees", where, v, bound); + } + if (apart / bound > agreement.nearest_bound) + { + agreement.nearest_bound = apart / bound; + agreement.nearest_bound_at = fmt::format("{}: vertex {}, {} of {} degrees", where, v, apart, bound); + } + if (!failed && (!(apart <= bound) || (bound < 180.0 && face.mTangents[v][3] != theirs[3]))) + { + report.fail(fmt::format("{}: vertex {} has the tangent ({}, {}, {}, {}), {} degrees from meshoptimizer's ({}, {}, {}, {}), past {}", where, v, + face.mTangents[v][0], face.mTangents[v][1], face.mTangents[v][2], face.mTangents[v][3], apart, + theirs[0], theirs[1], theirs[2], theirs[3], bound)); + failed = true; + } + } + } + // Every check the inspection asks for, on every face of the volume. // Tangents come last, from genTangents, once every face's normals have // been checked as generation left them. @@ -1236,13 +1504,17 @@ namespace for (S32 i = 0; i < faces; ++i) { const State& state = states[i]; - if (!state.inspected || !(state.checks & (Checks::Tangents | Checks::Orthogonal))) + if (!state.inspected || !(state.checks & (Checks::Tangents | Checks::Orthogonal | Checks::Meshopt))) { continue; } volume.genTangents(i); const LLVolumeFace& face = volume.getVolumeFace(i); checkTangents(face, state.checks, report, fmt::format("{}: {}", name, faceName(face))); + if (state.checks & Checks::Meshopt) + { + checkMeshoptTangents(face, *how.agreement, report, fmt::format("{}: {}", name, faceName(face))); + } } } @@ -2653,13 +2925,13 @@ namespace CHECK(report.failures == 0); } - // Lengyel's Gram-Schmidt step removes only |n|^2 of the normal's part, - // so it needs unit normals, or a tangent leans out of the plane its - // normal makes. Where a vertex's triangles give its tangent no - // direction, it makes one up, +Z whatever the normal: at a cap with no - // area, a vertex in no triangle with area, and the point where a - // twisted half circle's pole crosses the axis it sweeps about. - TEST_CASE("llvolume: every prim tangent is orthogonal to its normal", "[llvolume][!shouldfail]") + // A tangent is a sum of shares each in the plane of its vertex's + // normal, put back in that plane once summed; where nothing adds to it, + // it is an axis put in that plane. Either way it is orthogonal to the + // normal: at a cap with no area, at a vertex in no triangle with area, + // and at the point where a twisted half circle's pole crosses the axis + // it sweeps about. + TEST_CASE("llvolume: every prim tangent is orthogonal to its normal", "[llvolume]") { Inspect how; how.checks = Checks::Orthogonal; @@ -2671,10 +2943,32 @@ namespace CHECK(report.failures == 0); } - // A triangle whose UVs have no area gets an invented ratio of - // 1024 and outweighs its real neighbours, and one whose positions have - // no area still adds a tangent. Neither has a tangent to give. - TEST_CASE("llvolume: a triangle with no area or no UV area adds nothing to a tangent", "[llvolume][!shouldfail]") + // Prims' and sculpts' tangents are meshoptimizer's default mode summed + // a vertex at a time, which is its sum over the vertex's corners + // wherever it gives them all one tangent. + TEST_CASE("llvolume: prim and sculpt tangents agree with meshoptimizer's", "[llvolume]") + { + TangentAgreement agreement; + Inspect how; + how.checks = Checks::Meshopt; + how.agreement = &agreement; + Report report; + S32 volumes = 0; + inspectSweep(how, report, volumes); + inspectSculpts(how, report); + INFO(fmt::format("{} vertices compared, {} whose corners meshoptimizer splits, {} it gives no tangent, {} whose tangent rounding " + "decides; at most {} degrees apart, at {}; at most {} of a vertex's bound, at {}", + agreement.compared, agreement.split, agreement.lone, agreement.unsure, agreement.widest, agreement.widest_at, + agreement.nearest_bound, agreement.nearest_bound_at)); + INFO(report.text()); + CHECK(report.failures == 0); + CHECK(agreement.compared > 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. + TEST_CASE("llvolume: a triangle with no area or no UV area adds nothing to a tangent", "[llvolume]") { // Vertex 0 is in one real triangle, whose tangent is +X, and in one // other: a triangle whose UVs lie in a line, or one whose first two From 0439c771642c352903c99d891493aac0a47fed0f Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:54:44 -0400 Subject: [PATCH 11/16] Prim generation's per-vertex loops run on the SIMD kernels - createSide copies its vertices from a per-face column table, about six times faster than the per-vertex index arithmetic it replaces. - alsimd::extents computes a face's position and UV bounds. - alsimd::transform_directions_offset transforms each station's profile into the mesh, bit for bit what rotate plus add gave. - alsimd::normalize3_or_mark normalizes a grid's normals and marks the ones with no length for normalize_grid_normals to fill. - addHole skips its clamp when the hole cannot reach any edge. Positions, UVs and indices are byte-identical to before. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/alsimdkernels.cpp | 23 ++- indra/llmath/alsimdkernels.h | 11 ++ indra/llmath/alsimdkernels.inl | 67 +++++++ indra/llmath/llvolume.cpp | 207 ++++++++++------------ indra/llmath/tests/alsimdkernels_test.cpp | 101 +++++++++++ 5 files changed, 293 insertions(+), 116 deletions(-) diff --git a/indra/llmath/alsimdkernels.cpp b/indra/llmath/alsimdkernels.cpp index 1594f66576..52f5fcb1fe 100644 --- a/indra/llmath/alsimdkernels.cpp +++ b/indra/llmath/alsimdkernels.cpp @@ -52,6 +52,11 @@ void transform_directions_keep_w(const LLMatrix4a& m, const LLVector4a* src, LLV kernels::transform_impl(m, src, dst, n, 0.f); } +void transform_directions_offset(const LLMatrix4a& m, const LLVector4a& offset, const LLVector4a* src, LLVector4a* dst, size_t n) +{ + kernels::transform_offset_impl(m, offset, src, dst, n); +} + void transform_texcoords(const F32* src, F32* dst, size_t n, const LLVector4a& trans, const LLVector4a& rot0, const LLVector4a& rot1, const LLVector4a& scale, const LLVector4a& offset) @@ -95,11 +100,20 @@ void morph_apply(const MorphApply& m) kernels::morph_apply_impl(m); } +namespace +{ + // grid_tangents' lanes, each thread's own. + std::vector& tangent_scratch() + { + thread_local std::vector scratch; + return scratch; + } +} + void grid_tangents(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t columns, size_t rows, LLVector4a* tangents) { - thread_local std::vector scratch; - kernels::grid_tangents_impl(positions, normals, uvs, columns, rows, tangents, scratch); + kernels::grid_tangents_impl(positions, normals, uvs, columns, rows, tangents, tangent_scratch()); } void indexed_tangents(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t vertex_count, @@ -108,4 +122,9 @@ void indexed_tangents(const LLVector4a* positions, const LLVector4a* normals, co kernels::indexed_tangents_impl(positions, normals, uvs, vertex_count, indices, index_count, tangents); } +bool normalize3_or_mark(LLVector4a* v, size_t n) +{ + return kernels::normalize3_or_mark_impl(v, n); +} + } // namespace alsimd diff --git a/indra/llmath/alsimdkernels.h b/indra/llmath/alsimdkernels.h index 72c090d420..fb8100a1ce 100644 --- a/indra/llmath/alsimdkernels.h +++ b/indra/llmath/alsimdkernels.h @@ -54,6 +54,12 @@ void transform_directions(const LLMatrix4a& m, const LLVector4a* src, LLVector4a // The same, with lane w of every result taken from its source. void transform_directions_keep_w(const LLMatrix4a& m, const LLVector4a* src, LLVector4a* dst, size_t n); +// Every direction through the upper three rows, then offset added: a point +// turned and scaled about the origin and then moved, rounded as +// LLMatrix4a::rotate and an add round it, where transform_points folds the +// move into the rows' sum. +void transform_directions_offset(const LLMatrix4a& m, const LLVector4a& offset, const LLVector4a* src, LLVector4a* dst, size_t n); + // Two texture coordinates per vector, : translated by // trans, rotated by rot0 and rot1 ( and ), scaled, then offset. n counts vectors. @@ -148,6 +154,11 @@ void grid_tangents(const LLVector4a* positions, const LLVector4a* normals, const void indexed_tangents(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t vertex_count, const U16* indices, size_t index_count, 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. +bool normalize3_or_mark(LLVector4a* v, size_t n); + } // namespace alsimd #endif // AL_SIMDKERNELS_H diff --git a/indra/llmath/alsimdkernels.inl b/indra/llmath/alsimdkernels.inl index 27d5288b03..bedea2338a 100644 --- a/indra/llmath/alsimdkernels.inl +++ b/indra/llmath/alsimdkernels.inl @@ -419,6 +419,42 @@ void transform_impl(const LLMatrix4a& m, const LLVector4a* src, LLVector4a* dst, }); } +// One register of directions through rows m, then offset added. +template +AL_SIMD_INLINE void transform_offset_step(const typename K::reg* m, typename K::reg offset, const F32* in, F32* out) +{ + const typename K::reg v = K::load(in); + typename K::reg r = K::mul(K::template splat<0>(v), m[0]); + r = K::template fmadd_lane<1>(m[1], v, r); + r = K::template fmadd_lane<2>(m[2], v, r); + K::store(out, K::add(r, offset)); +} + +template +void transform_offset_impl(const LLMatrix4a& m, const LLVector4a& offset, const LLVector4a* src, LLVector4a* dst, size_t n) +{ + const f32x4 m4[3] = {m.getRow<0>(), m.getRow<1>(), m.getRow<2>()}; + const typename W::reg mw[3] = {W::broadcast(m4[0]), W::broadcast(m4[1]), W::broadcast(m4[2])}; + const f32x4 o4 = offset; + const typename W::reg ow = W::broadcast(o4); + const F32* s = reinterpret_cast(src); + F32* d = reinterpret_cast(dst); + + for_vectors(n, + [&](size_t i) + { + for (int u = 0; u < W::unroll; ++u) + { + const size_t at = 4 * (i + size_t(u) * W::lanes); + transform_offset_step(mw, ow, s + at, d + at); + } + }, + [&](size_t i) + { + transform_offset_step(m4, o4, s + 4 * i, d + 4 * i); + }); +} + // + trans, then rot0 * + rot1 * , then scaled and offset. template @@ -1341,6 +1377,37 @@ void grid_tangents_impl(const LLVector4a* positions, const LLVector4a* normals, } } +//////////////////////////////////// +// Normalizing +//////////////////////////////////// + +template +bool normalize3_or_mark_impl(LLVector4a* v, size_t n) +{ + using reg = typename W::reg; + using mask = typename W::mask; + constexpr size_t F = W::floats; + mask marked = W::none(); + alignas(16) F32 spill[4]; + for (size_t first = 0; first < n; first += F) + { + const auto at = [&](int i) { return first + i < n ? first + i : n - 1; }; + Lanes3 a; + reg w; + W::load_columns([&](int i) { return v[at(i)].getF32ptr(); }, a.x, a.y, a.z, w); + const reg length2 = dot_lanes3(a, a); + const mask none = W::cmple(length2, W::set1(FLT_MIN)); + 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)); + 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; }); + } + return W::any(marked); +} + } // namespace kernels } // namespace alsimd diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index cd8049b6b6..61749501f3 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -606,8 +606,30 @@ LLProfile::Face* LLProfile::addHole(const LLProfileParams& params, bool flat, F3 // triangle, is drawn in along its own direction to where it meets the // wall, which is of no thickness there. The wall is convex about the // origin, so the ray from the origin through a point leaves it where it - // first crosses the line of any of its edges. + // first crosses the line of any of its edges. A point nearer the origin + // than an edge's line cannot cross it, and an edge of no length or one + // whose line runs through the origin stops no ray, so the points are + // only tried where the hole's farthest reaches the line of some edge. + F32 farthest2 = 0.f; for (S32 i=mTotalOut;i= toward * toward; + } + for (S32 i=mTotalOut;imProfile.mArray; - LLVector4a* end_profile = profile+sizeT; LLVector4a offset = mPathp->mPath[s].mPos; // hack to work around MAINT-5660 for debug until we can suss out @@ -2155,15 +2175,9 @@ bool LLVolume::generate() offset.clear(); } - LLVector4a tmp; - // Run along the profile. - while (profile < end_profile) - { - rot_mat.rotate(*profile++, tmp); - dst->setAdd(tmp,offset); - ++dst; - } + alsimd::transform_directions_offset(rot_mat, offset, mProfilep->mProfile.mArray, dst, sizeT); + dst += sizeT; } for (std::vector::iterator iter = mProfilep->mFaces.begin(); @@ -5713,21 +5727,7 @@ namespace void normalize_grid_normals(LLVector4a* norm, S32 num_s, S32 num_t) { const S32 count = num_s * num_t; - bool any_missing = false; - for (S32 v = 0; v < count; ++v) - { - if (norm[v].dot3(norm[v]).getF32() > FLT_MIN) - { - norm[v].normalize3(); - } - else - { - norm[v].set(0.f, 0.f, 0.f, 1.f); - any_missing = true; - } - } - - if (!any_missing) + if (!alsimd::normalize3_or_mark(norm, count)) { return; } @@ -6697,7 +6697,7 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) S32 max_s = volume->getProfile().getTotal(); - S32 s, t, i; + S32 s, t; F32 ss, tt; num_vertices = mNumS*mNumT; @@ -6726,89 +6726,87 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) S32 end_t = mBeginT+mNumT; bool test = (mTypeMask & INNER_MASK) && (mTypeMask & FLAT_MASK) && mNumS > 2; - // Copy the vertices into the array + // Copy the vertices into the array. Every row takes the same columns of + // its mesh row with the same s texture coordinates, and the path's t for + // its own: the columns and their s are worked out once, so the rows are + // copies. { LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("llvfcs - copy verts"); - for (t = mBeginT; t < end_t; t++) + struct Column { - tt = path_data[t].mTexT; - for (s = 0; s < num_s; s++) + S32 mesh; + F32 s; + }; + constexpr S32 STACK_COLUMNS = 256; + Column stack_columns[STACK_COLUMNS]; + std::vector heap_columns; + Column* columns = stack_columns; + if (mNumS > STACK_COLUMNS) + { + heap_columns.resize(mNumS); + columns = heap_columns.data(); + } + S32 column_count = 0; + for (s = 0; s < num_s; s++) + { + if (mTypeMask & END_MASK) { - if (mTypeMask & END_MASK) + if (s) { - if (s) - { - ss = 1.f; - } - else - { - ss = 0.f; - } + ss = 1.f; } else { - // Get s value for tex-coord. - S32 index = mBeginS + s; - if (index >= (S32)profile.size()) - { - // edge? - ss = flat ? 1.f - begin_stex : 1.f; - } - else if (!flat) - { - ss = profile[index][2]; - } - else - { - ss = profile[index][2] - begin_stex; - } + ss = 0.f; } - - if (sculpt_reverse_horizontal) + } + else + { + // Get s value for tex-coord. + S32 index = mBeginS + s; + if (index >= (S32)profile.size()) { - ss = 1.f - ss; + // edge? + ss = flat ? 1.f - begin_stex : 1.f; } - - // Check to see if this triangle wraps around the array. - if (mBeginS + s >= max_s) + else if (!flat) { - // We're wrapping - i = mBeginS + s + max_s*(t-1); + ss = profile[index][2]; } else { - i = mBeginS + s + max_s*t; - } - - mesh[i].store4a((F32*)(pos+cur_vertex)); - tc[cur_vertex].set(ss,tt); - - cur_vertex++; - - if (test && s > 0) - { - mesh[i].store4a((F32*)(pos+cur_vertex)); - tc[cur_vertex].set(ss,tt); - cur_vertex++; + ss = profile[index][2] - begin_stex; } } - if ((mTypeMask & INNER_MASK) && (mTypeMask & FLAT_MASK) && mNumS > 2) + if (sculpt_reverse_horizontal) { - if (mTypeMask & OPEN_MASK) - { - s = num_s-1; - } - else - { - s = 0; - } - - i = mBeginS + s + max_s*t; - ss = profile[mBeginS + s][2] - begin_stex; + ss = 1.f - ss; + } - mesh[i].store4a((F32*)(pos+cur_vertex)); - tc[cur_vertex].set(ss,tt); + // A column past the profile's end wraps to its start. + const S32 column = mBeginS + s >= max_s ? mBeginS + s - max_s : mBeginS + s; + columns[column_count++] = { column, ss }; + if (test && s > 0) + { + columns[column_count++] = { column, ss }; + } + } + if (test) + { + s = (mTypeMask & OPEN_MASK) ? num_s - 1 : 0; + columns[column_count++] = { mBeginS + s, profile[mBeginS + s][2] - begin_stex }; + } + llassert(column_count == mNumS); + const LLVector4a* mesh_points = mesh.data(); + for (t = mBeginT; t < end_t; t++) + { + const LLVector4a* row = mesh_points + max_s * t; + tt = path_data[t].mTexT; + for (S32 k = 0; k < column_count; ++k) + { + pos[cur_vertex] = row[columns[k].mesh]; + tc[cur_vertex].set(columns[k].s, tt); cur_vertex++; } } @@ -6823,12 +6821,8 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) LLVector4a face_min; LLVector4a face_max; - face_min = face_max = *cur_pos++; - - while (cur_pos < end_pos) - { - update_min_max(face_min, face_max, *cur_pos++); - } + face_min = face_max = *cur_pos; + alsimd::extents(cur_pos, end_pos - cur_pos, face_min, face_max); // VFExtents change mExtents[0] = face_min; mExtents[1] = face_max; @@ -6840,26 +6834,11 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) mTexCoords[mNumVertices] = mTexCoords[mNumVertices-1]; } - // mTexCoords is LLVector2* into a 16-byte-aligned slab; iterate as - // floats 4-at-a-time and let load4a build each LLVector4a, avoiding - // a cast to LLVector4a* of unrelated-class storage. - const F32* cur_tc = (const F32*) mTexCoords; - const F32* const end_tc = (const F32*) (mTexCoords + tc_count); - + // mTexCoords is LLVector2* into a 16-byte-aligned slab, two to a + // vector; the kernel reads it as floats. LLVector4a tc_min; LLVector4a tc_max; - - tc_min.load4a(cur_tc); - cur_tc += 4; - tc_max = tc_min; - - while (cur_tc < end_tc) - { - LLVector4a tc; - tc.load4a(cur_tc); - cur_tc += 4; - update_min_max(tc_min, tc_max, tc); - } + alsimd::extents(reinterpret_cast(mTexCoords), tc_count / 2, tc_min, tc_max); F32* minp = tc_min.getF32ptr(); F32* maxp = tc_max.getF32ptr(); diff --git a/indra/llmath/tests/alsimdkernels_test.cpp b/indra/llmath/tests/alsimdkernels_test.cpp index 069c635c22..a3c550c6a9 100644 --- a/indra/llmath/tests/alsimdkernels_test.cpp +++ b/indra/llmath/tests/alsimdkernels_test.cpp @@ -265,6 +265,51 @@ namespace } } + // Directions turned and then moved, against rotate and an add, bit for + // bit at every count and width; nothing written past the count. + TEST_CASE("alsimdkernels: transform_directions_offset against rotate and add at every count and width", "[alsimdkernels]") + { + const LLMatrix4a m = some_matrix(); + const LLVector4a offset(1.f, -20.f, 300.f, 0.25f); + for (size_t n : COUNTS) + { + const std::vector src = some_vectors(n, 3); + std::vector expected(n + 1, SENTINEL); + for (size_t i = 0; i < n; ++i) + { + LLVector4a turned; + m.rotate(src[i], turned); + expected[i].setAdd(turned, offset); + } + + for_each_width([&](const char* width) + { + std::vector dst(n + 1, SENTINEL); + transform_offset_impl(m, offset, src.data(), dst.data(), n); + for (size_t i = 0; i <= n; ++i) + { + CAPTURE(width, n, i); + require_same_bits("transform_directions_offset", dst[i], expected[i]); + } + }); + + // The library is built at the tree's level, which may fuse + // where this file does not. + std::vector dst(n + 1, SENTINEL); + alsimd::transform_directions_offset(m, offset, src.data(), dst.data(), n); + require_same_bits("past the count through the library", dst[n], SENTINEL); + for (size_t i = 0; i < n; ++i) + { + INFO("through the library"); + CAPTURE(n, i); + for (int lane = 0; lane < 4; ++lane) + { + require_close("transform_directions_offset", dst[i][lane], expected[i][lane], 1e-6, 1000.0); + } + } + } + } + // Texture coordinates: two per vector, against the scalar arithmetic // in double within the two roundings the fused form takes, and every // width bit for bit against the first. @@ -895,4 +940,60 @@ 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. + TEST_CASE("alsimdkernels: normalize3_or_mark against normalize3 at every width", "[alsimdkernels]") + { + for (size_t n : COUNTS) + { + std::vector src = some_vectors(n, 12); + for (size_t i = 0; i < n; i += 5) + { + src[i].set(0.f, 0.f, 0.f, 0.f); + } + for (size_t i = 3; i < n; i += 7) + { + src[i].set(1e-25f, 0.f, 0.f, 0.f); + } + for (size_t i = 0; i < n; ++i) + { + src[i].getF32ptr()[3] = 0.f; + } + bool any_expected = false; + std::vector expected(src.begin(), src.begin() + n); + for (LLVector4a& e : expected) + { + if (e.dot3(e).getF32() > FLT_MIN) + { + e.normalize3(); + } + else + { + e.set(0.f, 0.f, 0.f, 1.f); + any_expected = true; + } + } + for_each_lane_width([&](const char* width) + { + std::vector v(src.begin(), src.end()); + v[n] = SENTINEL; + const bool any = normalize3_or_mark_impl(v.data(), n); + CAPTURE(width, n); + REQUIRE(any == any_expected); + require_same_bits("past the end", v[n], SENTINEL); + for (size_t i = 0; i < n; ++i) + { + CAPTURE(i); + for (int lane = 0; lane < 4; ++lane) + { + require_close("normalized", v[i][lane], expected[i][lane], 4.0 * std::numeric_limits::epsilon()); + } + } + }); + } + } } From 3491dbdd52b828bef6fdf343cec09e628ccb955a Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:55:09 -0400 Subject: [PATCH 12/16] A circle path ends on one station, and Lengyel's fallback tangent lies in its normal's plane A circle path's stations step around it, and the steps add up with rounding: genNGon could place a station a rounding short of the end cut, leaving a row with no height before the end's on twisted circle paths. A station within a hundredth of a step of the end is now the end's own. The point counts ARC reads are left as they were. Lengyel's tangents, which a mesh LOD too large for meshoptimizer's weld still takes, gave a vertex no triangle gives a direction (0, 0, 1) whatever its normal. It now takes +x, or +y where the normal is within 45 degrees of x, put in the plane of its normal. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 31 +++++++++++-- indra/llmath/tests/llvolume_test.cpp | 66 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 61749501f3..a571660e6e 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -1380,8 +1380,12 @@ void LLPath::genNGon(const LLPathParams& params, S32 sides, F32 startOff, F32 en // affect most sample points. t = ((S32)(t * sides)) / (F32)sides; - // Run through the non-cut dependent points. - while (t < params.getEnd()) + // 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; + while (t < end_before) { pt = mPath.append(1); @@ -7105,8 +7109,27 @@ void LLCalculateTangentArray(U32 vertexCount, const LLVector4a *vertex, const LL tangent[a] = tsubn; } else - { //degenerate, make up a value - tangent[a].set(0,0,1,1); + { + // Nothing gives the tangent a direction: +x in the plane of the + // normal, or +y where the normal is within 45 degrees of x, as + // the prims' tangents take it. + const LLVector4a& normal_a = normal[a]; + const F32 nx = normal_a[0]; + LLVector4a axis; + if (nx * nx > 0.5f) + { + axis.set(0.f, 1.f, 0.f); + } + else + { + axis.set(1.f, 0.f, 0.f); + } + LLVector4a along = normal_a; + along.mul(axis.dot3(normal_a).getF32()); + axis.sub(along); + axis.normalize3fast(); + axis.getF32ptr()[3] = 1.f; + tangent[a] = axis; } } diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index b283c6aa56..2da08f7275 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -2965,6 +2965,72 @@ namespace CHECK(agreement.compared > 0); } + // Lengyel's tangents, which a mesh LOD too large for meshoptimizer's + // weld still takes, give a vertex no triangle gives a direction an axis + // put in the plane of its normal: unit length and orthogonal to it, + // whichever way the normal points. + TEST_CASE("llvolume: Lengyel's tangent with no direction lies in the plane of its normal", "[llvolume]") + { + // Two triangles whose UVs are one point, so that neither gives a + // tangent, with a normal along each axis and between them. + const LLVector3 positions[] = { { 0, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }, { 1, 1, 0 } }; + const LLVector2 uvs[] = { { 0.5f, 0.5f }, { 0.5f, 0.5f }, { 0.5f, 0.5f }, { 0.5f, 0.5f } }; + const U16 indices[] = { 0, 1, 2, 2, 1, 3 }; + const LLVector3 normals[] = { { 0, 0, 1 }, { 1, 0, 0 }, { 0, 1, 0 }, { 0.6f, 0.f, 0.8f }, { 0.8f, -0.6f, 0.f } }; + for (const LLVector3& normal : normals) + { + LLVector4a positions4[4], normals4[4], tangents[4]; + for (S32 v = 0; v < 4; ++v) + { + positions4[v].load3(positions[v].mV); + normals4[v].load3(normal.mV); + tangents[v].clear(); + } + LLCalculateTangentArray(4, positions4, normals4, uvs, 2, indices, tangents); + for (S32 v = 0; v < 4; ++v) + { + const LLVector3 t = v3(tangents[v]); + INFO(fmt::format("normal ({}, {}, {}): vertex {} has the tangent ({}, {}, {}, {})", normal.mV[0], normal.mV[1], normal.mV[2], v, + t.mV[0], t.mV[1], t.mV[2], tangents[v][3])); + CHECK(fabsf(t.length() - 1.f) <= UNIT_TOLERANCE); + CHECK(fabsf(t * normal) <= 2e-3f); + CHECK((tangents[v][3] == 1.f || tangents[v][3] == -1.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 + // stations are within rounding of each other. + TEST_CASE("llvolume: a circle path's stations are apart", "[llvolume]") + { + Report report; + for (const Shape& s : sweepShapes()) + { + if (s.path == LL_PCODE_PATH_LINE) + { + continue; + } + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLPointer volume = build(s, lod); + const auto& path = volume->getPath().mPath; + for (S32 i = 0; i + 1 < (S32)path.size(); ++i) + { + if (!(path[i + 1].mTexT - path[i].mTexT > 1e-4f)) + { + report.fail(fmt::format("{}: stations {} and {} of {} are at t {} and {}", lodName(s, lod), i, i + 1, path.size(), + path[i].mTexT, path[i + 1].mTexT)); + break; + } + } + } + } + INFO(report.text()); + CHECK(report.failures == 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. From 65f595463c680547517e57fd1a9866d81b35398a Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:55:11 -0400 Subject: [PATCH 13/16] Media zoom faces the face, not the camera getApproximateFaceNormal summed the face's normals into a second ret declared inside its if, and returned the outer one, always zero, so media focus always fell back to the camera's direction. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llvovolume.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 2ad27db941..5ec1f6eaa7 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -2643,7 +2643,7 @@ LLVector3 LLVOVolume::getApproximateFaceNormal(U8 face_id) result.add(face.mNormals[i]); } - LLVector3 ret(result.getF32ptr()); + ret.set(result.getF32ptr()); ret = volumeDirectionToAgent(ret); ret.normVec(); } From 20ddd28fd60c17ff351e49003a106b4771aab666 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 11:55:13 -0400 Subject: [PATCH 14/16] Flexible prims take cheap tangents, and small prims skip what they do not need A flexible prim now regenerates its tangents with its frames, which it never did before, and with meshoptimizer's weighting that cost up to 50% on a flexi frame. A flexible face (mFlexible) takes cheap tangents on every build instead, so they do not change in kind between its first build and its frames: a side's by central difference along its rows, made orthogonal to the normal (alsimd::grid_tangents_central), with a neighbour's where that has no length; a cap's the one tangent its end station's frame gives (mCapTangent). Caps and straight sides agree with the weighted tangent to within 0.004 degrees; on a bent path the two differ by up to the path's turn per section. Every flexi frame is now faster than before these changes. Line-path and flexible caps take their facing from the path's direction and skip the neighbouring-ring test. addHole's pre-test compares the distance to each edge's line, so it also skips on clockwise profiles. The hollow cap strip compares edge lengths only when both of its candidate triangles qualify. Box at LOD 0 and 1 builds within a few percent of before. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/alsimdkernels.cpp | 6 + indra/llmath/alsimdkernels.h | 11 + indra/llmath/alsimdkernels.inl | 77 +++++++ indra/llmath/llvolume.cpp | 244 ++++++++++++++++++---- indra/llmath/llvolume.h | 8 + indra/llmath/tests/alsimdkernels_test.cpp | 96 +++++++++ indra/llmath/tests/llvolume_test.cpp | 163 +++++++++++++++ 7 files changed, 565 insertions(+), 40 deletions(-) diff --git a/indra/llmath/alsimdkernels.cpp b/indra/llmath/alsimdkernels.cpp index 52f5fcb1fe..3656f81d69 100644 --- a/indra/llmath/alsimdkernels.cpp +++ b/indra/llmath/alsimdkernels.cpp @@ -122,6 +122,12 @@ void indexed_tangents(const LLVector4a* positions, const LLVector4a* normals, co kernels::indexed_tangents_impl(positions, normals, uvs, vertex_count, indices, index_count, tangents); } +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) +{ + return kernels::grid_tangents_central_impl(positions, normals, columns, rows, joins, same, sign, w, tangents); +} + bool normalize3_or_mark(LLVector4a* v, size_t n) { return kernels::normalize3_or_mark_impl(v, n); diff --git a/indra/llmath/alsimdkernels.h b/indra/llmath/alsimdkernels.h index fb8100a1ce..79ddfd4ddf 100644 --- a/indra/llmath/alsimdkernels.h +++ b/indra/llmath/alsimdkernels.h @@ -154,6 +154,17 @@ void grid_tangents(const LLVector4a* positions, const LLVector4a* normals, const void indexed_tangents(const LLVector4a* positions, const LLVector4a* normals, const LLVector2* uvs, size_t vertex_count, const U16* indices, size_t index_count, LLVector4a* tangents); +// A grid's tangents along its rows, cheaply: each vertex's is the chord +// from its left neighbour in its row to its right one, times sign, put in +// the plane of its normal and normalized, with w set to w. At a row's +// ends the chord reaches across the seam to the column beside the other +// end where joins is set, and is one-sided where it is not. A chord whose +// part in that plane is shorter than `same`, or nearly along the normal, +// gives no direction: the vertex is left with w 0, for the caller to fill. +// Returns whether any was. The grid is laid out as grid_tangents takes it. +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. diff --git a/indra/llmath/alsimdkernels.inl b/indra/llmath/alsimdkernels.inl index bedea2338a..88d012212e 100644 --- a/indra/llmath/alsimdkernels.inl +++ b/indra/llmath/alsimdkernels.inl @@ -1377,6 +1377,83 @@ void grid_tangents_impl(const LLVector4a* positions, const LLVector4a* normals, } } +//////////////////////////////////// +// Tangents by central difference +//////////////////////////////////// + +// a . b in x, y and z, in every lane of each vector. +template +AL_SIMD_INLINE typename K::reg dot3_replicated(typename K::reg a, typename K::reg b) +{ + const typename K::reg m = K::mul(a, b); + return K::add(K::add(K::template splat<0>(m), K::template splat<1>(m)), K::template splat<2>(m)); +} + +// The tangents of K::lanes vertices from their chords' ends, as +// grid_tangents_central makes them; true in a vector whose chord gives no +// direction. +template +AL_SIMD_INLINE typename K::mask central_tangent(const F32* left, const F32* right, const F32* normal, F32* out, typename K::reg sign, + typename K::reg w, typename K::reg same2) +{ + using reg = typename K::reg; + const reg chord = K::mul(K::sub(K::loadu(right), K::loadu(left)), sign); + const reg n = K::loadu(normal); + const reg in_plane = K::fnmadd(n, dot3_replicated(chord, n), chord); + const reg length2 = dot3_replicated(in_plane, in_plane); + // Short, or within about a tenth of a degree of the normal. + const typename K::mask none = K::cmple(length2, K::max(same2, K::mul(K::set1(1e-6f), dot3_replicated(chord, chord)))); + const reg tangent = K::mul(in_plane, K::rsqrt(K::max(length2, K::set1(FLT_MIN)))); + K::storeu(out, K::select_w(tangent, K::select(none, K::zero(), w))); + return none; +} + +template +bool grid_tangents_central_impl(const LLVector4a* positions, const LLVector4a* normals, size_t columns, size_t rows, bool joins, F32 same, + F32 sign, F32 w, LLVector4a* tangents) +{ + using K4 = ALSimd4; + const auto at = [](const LLVector4a* v, size_t i) { return v[i].getF32ptr(); }; + const typename K4::reg sign4 = K4::set1(sign); + const typename K4::reg w4 = K4::set1(w); + const typename K4::reg same4 = K4::set1(same * same); + const typename W::reg sign_w = W::set1(sign); + const typename W::reg w_w = W::set1(w); + const typename W::reg same_w = W::set1(same * same); + typename K4::mask none4 = K4::none(); + typename W::mask none_w = W::none(); + if (columns < 2) + { + for (size_t v = 0; v < columns * rows; ++v) + { + tangents[v].set(0.f, 0.f, 0.f, 0.f); + } + return columns * rows > 0; + } + const size_t last = columns - 1; + for (size_t t = 0; t < rows; ++t) + { + const size_t row = t * columns; + const LLVector4a* p = positions + row; + const LLVector4a* n = normals + row; + LLVector4a* out = tangents + row; + // The ends: across the seam to the column beside the other end, or + // from the end itself. + none4 = K4::or_(none4, central_tangent(at(p, joins ? last - 1 : 0), at(p, 1), at(n, 0), out[0].getF32ptr(), sign4, w4, same4)); + none4 = K4::or_(none4, central_tangent(at(p, last - 1), at(p, joins ? 1 : last), at(n, last), out[last].getF32ptr(), sign4, w4, same4)); + size_t s = 1; + for (; s + W::lanes <= last; s += W::lanes) + { + none_w = W::or_(none_w, central_tangent(at(p, s - 1), at(p, s + 1), at(n, s), out[s].getF32ptr(), sign_w, w_w, same_w)); + } + for (; s < last; ++s) + { + none4 = K4::or_(none4, central_tangent(at(p, s - 1), at(p, s + 1), at(n, s), out[s].getF32ptr(), sign4, w4, same4)); + } + } + return K4::any(none4) || W::any(none_w); +} + //////////////////////////////////// // Normalizing //////////////////////////////////// diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index a571660e6e..2b6d709c3d 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -626,8 +626,9 @@ LLProfile::Face* LLProfile::addHole(const LLProfileParams& params, bool flat, F3 { continue; } - // The line is toward / |e| from the origin. - may_reach = toward < 0.f || farthest2 * length2 * 1.0001f >= toward * toward; + // The line is |toward| / |e| from the origin, whichever way the + // profile winds. + may_reach = farthest2 * length2 * 1.0001f >= toward * toward; } for (S32 i=mTotalOut;igetParams().getPathParams().getCurveType() == LL_PCODE_PATH_FLEXIBLE; bool ret = false ; if (mTypeMask & CAP_MASK) @@ -5827,9 +5831,11 @@ namespace // outer chain, rather than (2, 1, b), a step along its inner one. A // triangle that faces the wrong way will not do, nor one that contains // the other chain's next point; of two that will, the one whose new edge - // is shorter. Areas within the tolerance count as none, and so, with a - // tolerance, a triangle of no area contains nothing. - bool cap_strip_takes_outer(F32 area_1a2, F32 area_2ab, F32 area_21b, F32 area_1ba, bool outer_shorter, F32 tolerance) + // is shorter, which outer_shorter says where it is asked. Areas within the + // tolerance count as none, and so, with a tolerance, a triangle of no + // area contains nothing. + template + bool cap_strip_takes_outer(F32 area_1a2, F32 area_2ab, F32 area_21b, F32 area_1ba, OuterShorter outer_shorter, F32 tolerance) { bool tri_1a2 = !(area_1a2 < -tolerance); bool tri_21b = !(area_21b < -tolerance); @@ -5849,7 +5855,7 @@ namespace { return true; } - return outer_shorter; + return outer_shorter(); } // The path station a cap closes: the last for a top cap, the first for a @@ -5883,8 +5889,12 @@ namespace if (decide) { reversed = false; + // A line's or a flexible path's end faces along the path, away + // from the ring beside it, whatever the shear, taper or twist: + // only a circle's end can turn past the axis it sweeps about. + const U8 curve = volume->getParams().getPathParams().getCurveType(); const S32 count = (S32)path.size(); - if (count > 1) + if (count > 1 && curve != LL_PCODE_PATH_LINE && curve != LL_PCODE_PATH_FLEXIBLE) { const S32 next = top ? own - 1 : own + 1; const S32 ring = volume->getProfile().getTotal(); @@ -5908,6 +5918,137 @@ namespace return normal; } + // +x in the plane of a normal, or +y where the normal is within 45 + // degrees of x: a tangent where nothing gives one a direction, unit + // length and orthogonal to the normal. w is 1. + LLVector4a plane_axis(const LLVector4a& normal) + { + LLVector4a axis; + if (normal[0] * normal[0] > 0.5f) + { + axis.set(0.f, 1.f, 0.f); + } + else + { + axis.set(1.f, 0.f, 0.f); + } + LLVector4a along = normal; + along.mul(axis.dot3(normal).getF32()); + axis.sub(along); + axis.normalize3fast(); + axis.getF32ptr()[3] = 1.f; + return axis; + } + + // A flexible cap's tangent: dP/du of its end station's frame, u running + // along the profile's x as the cap's UVs map it, put in the plane of the + // cap's normal, and w the side of the normal x dP/du that dP/dv lies on, + // v running along the profile's y on a top cap and against it on a + // bottom one. Where the station scales x to nothing, an axis in the + // plane. + LLVector4a cap_tangent(const LLVolume* volume, bool top, const LLVector4a& normal) + { + const LLPath::PathPt& station = volume->getPath().mPath[cap_station(volume, top)]; + const F32* scale = station.mScale.getF32ptr(); + LLVector4a du = station.mRot.getRow<0>(); + du.mul(scale[0]); + LLVector4a dv = station.mRot.getRow<1>(); + dv.mul(top ? scale[1] : -scale[1]); + + LLVector4a tangent = du; + LLVector4a along = normal; + along.mul(du.dot3(normal).getF32()); + tangent.sub(along); + if (tangent.dot3(tangent).getF32() <= SAME_PLACE * SAME_PLACE) + { + return plane_axis(normal); + } + tangent.normalize3fast(); + LLVector4a turned; + turned.setCross3(normal, tangent); + tangent.getF32ptr()[3] = turned.dot3(dv).getF32() < 0.f ? -1.f : 1.f; + return tangent; + } + + // Gives each vertex of a grid whose tangent has no direction, marked by + // a w of 0, the nearest own tangent up or down its column, else along + // its row, else around it, put in the plane of its normal, or an axis + // in that plane where none serves; w becomes w. A donor is a tangent of + // the grid's own, never one given, so that none is passed on twice. + void fill_grid_tangents(LLVector4a* tangents, const LLVector4a* normals, S32 num_s, S32 num_t, F32 w) + { + const auto own = [](const LLVector4a& t) { return t[3] == 1.f || t[3] == -1.f; }; + for (S32 t = 0; t < num_t; ++t) + { + for (S32 s = 0; s < num_s; ++s) + { + LLVector4a& tangent = tangents[s + num_s * t]; + if (own(tangent)) + { + continue; + } + const LLVector4a* donor = nullptr; + for (S32 d = 1; d < num_t && !donor; ++d) + { + for (S32 row : { t - d, t + d }) + { + if (row >= 0 && row < num_t && own(tangents[s + num_s * row])) + { + donor = &tangents[s + num_s * row]; + break; + } + } + } + for (S32 d = 1; d < num_s && !donor; ++d) + { + for (S32 column : { s - d, s + d }) + { + if (column >= 0 && column < num_s && own(tangents[column + num_s * t])) + { + donor = &tangents[column + num_s * t]; + break; + } + } + } + for (S32 d = 1; d < llmax(num_s, num_t) && !donor; ++d) + { + for (S32 row = llmax(t - d, 0); row <= llmin(t + d, num_t - 1) && !donor; ++row) + { + for (S32 column = llmax(s - d, 0); column <= llmin(s + d, num_s - 1); ++column) + { + if (own(tangents[column + num_s * row])) + { + donor = &tangents[column + num_s * row]; + break; + } + } + } + } + const LLVector4a& normal = normals[s + num_s * t]; + LLVector4a given = plane_axis(normal); + if (donor) + { + LLVector4a in_plane = *donor; + LLVector4a along = normal; + along.mul(in_plane.dot3(normal).getF32()); + in_plane.sub(along); + if (in_plane.dot3(in_plane).getF32() > SAME_PLACE * SAME_PLACE) + { + in_plane.normalize3fast(); + given = in_plane; + } + } + // Marked as given, so that it is no donor. + given.getF32ptr()[3] = 2.f * w; + tangent = given; + } + } + for (S32 v = 0; v < num_s * num_t; ++v) + { + tangents[v].getF32ptr()[3] = w; + } + } + // Turns every triangle of an index list over. void reverse_winding(U16* indices, S32 count) { @@ -6237,6 +6378,12 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) } const LLVector4a normal = cap_normal(volume, mTypeMask & TOP_MASK, !partial_build, mReversed); + if (mFlexible) + { + // mCapTangent is plain floats, not aligned as a vector stores. + const LLVector4a tangent = cap_tangent(volume, mTypeMask & TOP_MASK, normal); + std::copy_n(tangent.getF32ptr(), 4, mCapTangent); + } llassert(llfinite(normal.getF32ptr()[0])); llassert(llfinite(normal.getF32ptr()[1])); @@ -6307,12 +6454,14 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) (paV[0]*pbV[1] - pbV[0]*paV[1]) + (pbV[0]*p2V[1] - p2V[0]*pbV[1]); - LLVector4a d1; - d1.setSub(p1, pa); - LLVector4a d2; - d2.setSub(p2, pb); - const bool outer_shorter = d1.dot3(d1) < d2.dot3(d2); - + const auto outer_shorter = [&]() + { + LLVector4a d1; + d1.setSub(p1, pa); + LLVector4a d2; + d2.setSub(p2, pb); + return d1.dot3(d1) < d2.dot3(d2); + }; const bool use_tri1a2 = cap_strip_takes_outer(area_1a2, area_2ab, area_21b, area_1ba, outer_shorter, zero_area); if (use_tri1a2) @@ -6372,12 +6521,14 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) (paV[0]*pbV[1] - pbV[0]*paV[1]) + (pbV[0]*p2V[1] - p2V[0]*pbV[1]); - LLVector4a d1; - d1.setSub(p1, pa); - LLVector4a d2; - d2.setSub(p2, pb); - const bool outer_shorter = d1.dot3(d1) < d2.dot3(d2); - + const auto outer_shorter = [&]() + { + LLVector4a d1; + d1.setSub(p1, pa); + LLVector4a d2; + d2.setSub(p2, pb); + return d1.dot3(d1) < d2.dot3(d2); + }; const bool use_tri1a2 = cap_strip_takes_outer(area_1a2, area_2ab, area_21b, area_1ba, outer_shorter, zero_area); // Flipped backfacing from top @@ -6455,11 +6606,41 @@ void LLVolumeFace::createTangents() LLCalculateTangentArray(mNumVertices, mPositions, mNormals, mTexCoords, mNumIndices / 3, mIndices, mTangents); } + else if (mFlexible && (mTypeMask & CAP_MASK)) + { + LLVector4a tangent; + tangent.loadua(mCapTangent); + std::fill(mTangents, mTangents + mNumVertices, tangent); + } else if (!(mTypeMask & CAP_MASK) && mNumS > 0 && mNumT > 0 && mNumS * mNumT == mNumVertices && mNumIndices == (mNumS - 1) * (mNumT - 1) * 6) { // A side is createSide's grid. - alsimd::grid_tangents(mPositions, mNormals, mTexCoords, mNumS, mNumT, mTangents); + if (mFlexible) + { + // Along the rows, the way u runs across the face, and w as + // the face's UVs wind: u runs along a row and v down a + // column, each the one way over the grid, but for a closing + // copy whose u starts the profile over: the way most of a + // row's steps run. + S32 rising = 0; + for (S32 s = 0; s + 1 < mNumS; ++s) + { + const F32 step = mTexCoords[s + 1].mV[0] - mTexCoords[s].mV[0]; + rising += step > 0.f ? 1 : (step < 0.f ? -1 : 0); + } + const F32 dv = mTexCoords[mNumS * (mNumT - 1)].mV[1] - mTexCoords[0].mV[1]; + const F32 sign = rising < 0 ? -1.f : 1.f; + const F32 w = (rising < 0) == (dv < 0.f) ? 1.f : -1.f; + if (alsimd::grid_tangents_central(mPositions, mNormals, mNumS, mNumT, mJoinsS, SAME_PLACE, sign, w, mTangents)) + { + fill_grid_tangents(mTangents, mNormals, mNumS, mNumT, w); + } + } + else + { + alsimd::grid_tangents(mPositions, mNormals, mTexCoords, mNumS, mNumT, mTangents); + } } else { @@ -7110,26 +7291,9 @@ void LLCalculateTangentArray(U32 vertexCount, const LLVector4a *vertex, const LL } else { - // Nothing gives the tangent a direction: +x in the plane of the - // normal, or +y where the normal is within 45 degrees of x, as - // the prims' tangents take it. - const LLVector4a& normal_a = normal[a]; - const F32 nx = normal_a[0]; - LLVector4a axis; - if (nx * nx > 0.5f) - { - axis.set(0.f, 1.f, 0.f); - } - else - { - axis.set(1.f, 0.f, 0.f); - } - LLVector4a along = normal_a; - along.mul(axis.dot3(normal_a).getF32()); - axis.sub(along); - axis.normalize3fast(); - axis.getF32ptr()[3] = 1.f; - tangent[a] = axis; + // Nothing gives the tangent a direction: an axis in the plane + // of the normal, as the prims' tangents take it. + tangent[a] = plane_axis(normal[a]); } } diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 6ce72c51bc..d9fa739d9c 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -957,6 +957,14 @@ class LLVolumeFace // partial build keeps it. bool mReversed = false; + // Whether the face is a flexible prim's. Its tangents are then the cheap + // ones, on every build, so that they do not change in kind between its + // first build and the partial builds of its frames: along a side's rows + // by central difference, and over a cap the one tangent its end + // station's frame gives, which every build of the cap sets here. + bool mFlexible = false; + F32 mCapTangent[4] = { 1.f, 0.f, 0.f, 1.f }; + LLVector4a* mExtents; //minimum and maximum point of face LLVector4a* mCenter; LLVector2 mTexCoordExtents[2]; //minimum and maximum of texture coordinates of the face. diff --git a/indra/llmath/tests/alsimdkernels_test.cpp b/indra/llmath/tests/alsimdkernels_test.cpp index a3c550c6a9..60a71af603 100644 --- a/indra/llmath/tests/alsimdkernels_test.cpp +++ b/indra/llmath/tests/alsimdkernels_test.cpp @@ -941,6 +941,102 @@ namespace } } + // ------------------------------------------------------------------ + // Tangents by central difference + + // The chord tangents as the rule gives them, a vertex at a time in + // double: the chord between a vertex's row neighbours, across the seam + // or one-sided at the ends, in the plane of its normal; none where that + // is shorter than `same` or within 1e-3 of the chord's length. + std::vector central_tangents(const TangentGrid& g, size_t columns, size_t rows, bool joins, F32 same, F32 sign, F32 w) + { + std::vector out(columns * rows); + for (size_t t = 0; t < rows; ++t) + { + for (size_t s = 0; s < columns; ++s) + { + size_t left = s == 0 ? (joins ? columns - 2 : 0) : s - 1; + size_t right = s == columns - 1 ? (joins ? 1 : columns - 1) : s + 1; + if (columns < 2) + { + left = right = 0; + } + const LLVector4a& a = g.positions[left + columns * t]; + const LLVector4a& b = g.positions[right + columns * t]; + const LLVector4a& n = g.normals[s + columns * t]; + F64 chord[3], length2 = 0.0, chord2 = 0.0, dn = 0.0; + for (int k = 0; k < 3; ++k) + { + chord[k] = ((F64)b[k] - a[k]) * sign; + dn += chord[k] * n[k]; + chord2 += chord[k] * chord[k]; + } + for (int k = 0; k < 3; ++k) + { + chord[k] -= n[k] * dn; + length2 += chord[k] * chord[k]; + } + if (length2 <= std::max((F64)same * same, 1e-6 * chord2)) + { + out[s + columns * t].set(0.f, 0.f, 0.f, 0.f); + continue; + } + const F64 scale = 1.0 / std::sqrt(length2); + out[s + columns * t].set(F32(chord[0] * scale), F32(chord[1] * scale), F32(chord[2] * scale), w); + } + } + return out; + } + + // The chord tangents at every width against the rule worked in double, + // with and without a seam, either way along the rows: directions to + // within rounding, w as given, and w 0 where the chord gives no + // direction, as where both neighbours are one point in a row of three + // with a seam, or a row of one. Nothing is written past the grid. + TEST_CASE("alsimdkernels: grid_tangents_central against the rule in double at every width", "[alsimdkernels]") + { + U32 seed = 90; + for (size_t columns : { 1, 2, 3, 4, 5, 8, 9, 16, 17 }) + { + for (size_t rows : { 1, 2, 5 }) + { + const TangentGrid g = tangent_grid(columns, rows, ++seed); + const size_t n = columns * rows; + for (bool joins : { false, true }) + { + for (F32 sign : { 1.f, -1.f }) + { + const std::vector expected = central_tangents(g, columns, rows, joins, 1e-5f, sign, -sign); + bool any_expected = false; + for (const LLVector4a& e : expected) + { + any_expected = any_expected || e[3] == 0.f; + } + for_each_lane_width([&](const char* width) + { + std::vector got(n + 1, SENTINEL); + const bool any = grid_tangents_central_impl(g.positions.data(), g.normals.data(), columns, rows, joins, 1e-5f, sign, -sign, + got.data()); + CAPTURE(width, columns, rows, joins, sign); + REQUIRE(any == any_expected); + require_same_bits("past the grid", got[n], SENTINEL); + for (size_t v = 0; v < n; ++v) + { + CAPTURE(v); + REQUIRE(got[v][3] == expected[v][3]); + if (expected[v][3] != 0.f) + { + REQUIRE(degrees_apart(got[v], expected[v]) <= 1e-3); + REQUIRE(std::fabs(got[v].getLength3().getF32() - 1.f) <= 1e-5f); + } + } + }); + } + } + } + } + } + // ------------------------------------------------------------------ // Normalizing diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 2da08f7275..a1c6ad6bab 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -49,6 +49,7 @@ #include "v3math.h" #include "v4math.h" #include "llvector4a.h" +#include "alsimdkernels.h" #include #include @@ -2965,6 +2966,168 @@ namespace CHECK(agreement.compared > 0); } + // The flexible prims the partial-build cases build: the sweep's + // line-path shapes, plain and profile cut, on paths of 4 and 16 + // sections, straight and bent, at every LOD, rebuilt in place once. + template + void forEachFlexiFrame(Each each) + { + for (const Shape& base : sweepShapes()) + { + if (base.path != LL_PCODE_PATH_LINE || (base.name.find(", plain") == std::string::npos && base.name.find(", profile cut") == std::string::npos)) + { + continue; + } + for (S32 sections : { 4, 16 }) + { + for (F32 bend : { 0.f, 1.1f }) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLPointer volume = flexiVolume(base, lod); + bendPath(*volume, sections, 0.4f); + volume->regen(); + genAllTangents(*volume); + bendPath(*volume, sections, bend); + volume->regen(); + genAllTangents(*volume); + each(*volume, fmt::format("flexible {}, {} sections bent {} at LOD {}", base.name, sections, bend, lod), sections, bend); + } + } + } + } + } + + // A flexible prim's tangents are its cheap ones: unit length, finite, + // orthogonal to their normals, and with the w each triangle's UVs wind + // for, where the triangle has area in positions and in UVs. + TEST_CASE("llvolume: a flexible prim's tangents are unit and orthogonal and wind with its UVs", "[llvolume]") + { + Report report; + S32 faces = 0; + forEachFlexiFrame([&](LLVolume& volume, const std::string& name, S32, F32) + { + for (S32 i = 0; i < volume.getNumVolumeFaces(); ++i) + { + const LLVolumeFace& face = volume.getVolumeFace(i); + const std::string where = fmt::format("{}: {}", name, faceName(face)); + ++faces; + checkTangents(face, Checks::Tangents | Checks::Orthogonal, report, where); + for (S32 t = 0; t + 2 < face.mNumIndices; t += 3) + { + const U16 a = face.mIndices[t], b = face.mIndices[t + 1], c = face.mIndices[t + 2]; + // The generator takes points within 1e-5 of each other + // as one place: a strip that narrow, as a cut end where + // the hole meets its wall, winds as rounding has it. + auto apart = [&](U16 p, U16 q) { return (v3(face.mPositions[p]) - v3(face.mPositions[q])).lengthSquared() > 1e-10f; }; + if (tangentFlat(face, a, b, c) || !apart(a, b) || !apart(b, c) || !apart(a, c)) + { + continue; + } + const LLVector2 uv_ab = face.mTexCoords[b] - face.mTexCoords[a]; + const LLVector2 uv_ac = face.mTexCoords[c] - face.mTexCoords[a]; + const F32 uv_area = uv_ab.mV[0] * uv_ac.mV[1] - uv_ab.mV[1] * uv_ac.mV[0]; + const LLVector3 geometric = (v3(face.mPositions[b]) - v3(face.mPositions[a])) % (v3(face.mPositions[c]) - v3(face.mPositions[a])); + bool failed = false; + for (U16 v : { a, b, c }) + { + const F32 facing = geometric * v3(face.mNormals[v]); + if (fabsf(facing) <= 1e-3f * geometric.length()) + { + continue; + } + const F32 expected = (uv_area > 0.f) == (facing > 0.f) ? 1.f : -1.f; + if (face.mTangents[v][3] != expected) + { + report.fail(fmt::format("{}: triangle {}'s UVs wind for w = {} at vertex {}, which has {}", where, t / 3, expected, v, + face.mTangents[v][3])); + failed = true; + break; + } + } + if (failed) + { + break; + } + } + } + }); + INFO(fmt::format("{} faces", faces)); + INFO(report.text()); + CHECK(faces > 0); + CHECK(report.failures == 0); + } + + // The cheap tangents stay near the meshoptimizer-weighted ones every + // other prim takes, on the same geometry, away from the points where + // either has nothing to go by: a vertex in a triangle with no area or + // no UV area, and one whose weighted tangent rounding decides. A cap's + // and a straight side's are the weighted ones to within rounding. On a + // bent path a side's row turns from one station to the next, and the + // weighted tangent takes in the edges of the rows beside a vertex's + // own, where the cheap one follows its own row: they are as far apart + // as the path turns over a section, at most. + TEST_CASE("llvolume: a flexible prim's tangents stay near the weighted ones", "[llvolume]") + { + Report report; + F64 widest = 0.0; + std::string widest_at; + S32 compared = 0; + forEachFlexiFrame([&](LLVolume& volume, const std::string& name, S32 sections, F32 bend) + { + const F64 bound = llmax(0.01, fabs((F64)bend) / sections * 180.0 / F_PI); + for (S32 i = 0; i < volume.getNumVolumeFaces(); ++i) + { + const LLVolumeFace& face = volume.getVolumeFace(i); + const std::string where = fmt::format("{}: {}", name, faceName(face)); + std::vector weighted(face.mNumVertices); + const bool grid = !(face.mTypeMask & LLVolumeFace::CAP_MASK) && face.mNumS * face.mNumT == face.mNumVertices; + if (grid) + { + alsimd::grid_tangents(face.mPositions, face.mNormals, face.mTexCoords, face.mNumS, face.mNumT, weighted.data()); + } + else + { + alsimd::indexed_tangents(face.mPositions, face.mNormals, face.mTexCoords, face.mNumVertices, face.mIndices, face.mNumIndices, + weighted.data()); + } + std::vector skip(face.mNumVertices, false); + for (S32 t = 0; t + 2 < face.mNumIndices; t += 3) + { + if (tangentFlat(face, face.mIndices[t], face.mIndices[t + 1], face.mIndices[t + 2])) + { + skip[face.mIndices[t]] = skip[face.mIndices[t + 1]] = skip[face.mIndices[t + 2]] = true; + } + } + const std::vector rounding = tangentRoundingDegrees(face); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + if (skip[v] || rounding[v] >= 1.0) + { + continue; + } + ++compared; + const F64 apart = degreesApart(v3(face.mTangents[v]), v3(weighted[v])); + if (apart > widest) + { + widest = apart; + widest_at = fmt::format("{}: vertex {}", where, v); + } + if (!(apart <= bound) || face.mTangents[v][3] != weighted[v][3]) + { + report.fail(fmt::format("{}: vertex {} is {} degrees from the weighted tangent, past {}, w {} against {}", where, v, apart, + bound, face.mTangents[v][3], weighted[v][3])); + break; + } + } + } + }); + INFO(fmt::format("{} vertices compared; at most {} degrees apart, at {}", compared, widest, widest_at)); + INFO(report.text()); + CHECK(compared > 0); + CHECK(report.failures == 0); + } + // Lengyel's tangents, which a mesh LOD too large for meshoptimizer's // weld still takes, give a vertex no triangle gives a direction an axis // put in the plane of its normal: unit length and orthogonal to it, From 51ca0243b26ab1639e7a4b0b5540d377a12f8cbd Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 20 Oct 2026 12:24:45 -0400 Subject: [PATCH 15/16] A circle2 path's point count is the stations it builds Streaming cost and ARC count a circle2 path's points with getNumNGonPoints, which walks the stations as genNGon places them. Since genNGon now takes a station within a hundredth of a step of the end cut as the end's own, the count did not: a circle2 path whose end cut lands on a step counted one point more than it built, as a torus cut to 8 to 13 of its 24 sides at LOD 3 did. The count stops at the same place. Circle paths count their points by a formula of the side count, not by walking the stations, and are unchanged. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 5 +++-- indra/llmath/tests/llvolume_test.cpp | 31 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 2b6d709c3d..44cfcaec08 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -1264,8 +1264,9 @@ S32 LLPath::getNumNGonPoints(const LLPathParams& params, S32 sides, F32 startOff // affect most sample points. t = ((S32)(t * sides)) / (F32)sides; - // Run through the non-cut dependent points. - while (t < params.getEnd()) + // Run through the non-cut dependent points, as genNGon does. + const F32 end_before = params.getEnd() - step * 0.01f; + while (t < end_before) { ret++; t+=step; diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index a1c6ad6bab..b92f2a3098 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -3194,6 +3194,37 @@ namespace 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, + // is one station to both. + TEST_CASE("llvolume: a circle2 path's point count is its stations", "[llvolume]") + { + Report report; + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + const S32 sides = llfloor(MIN_DETAIL_FACES * LOD_DETAIL[lod]); + for (S32 k = 1; k <= sides; ++k) + { + Shape s = torus(); + s.name = "circle2 torus"; + s.path = LL_PCODE_PATH_CIRCLE2; + s.path_end = (F32)k / (F32)sides; + const LLVolumeParams params = s.params(); + LLPointer volume = new LLVolume(params, LOD_DETAIL[lod]); + const S32 counted = LLPath::getNumPoints(params.getPathParams(), LOD_DETAIL[lod]); + const S32 stations = (S32)volume->getPath().mPath.size(); + if (counted != stations) + { + report.fail(fmt::format("{} cut to {} of {} sides at LOD {}: {} points counted, {} stations", s.name, k, sides, lod, + counted, stations)); + } + } + } + INFO(report.text()); + CHECK(report.failures == 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. From a72e3a1d5ea03c49794831a5fc3bca391383792e Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:15:08 -0500 Subject: [PATCH 16/16] The tangent check holds no vertex to meshoptimizer's where it welds it to another meshopt_generateTangents welds vertices whose positions, normals and UVs compare equal and sums the corners of all of them, where createTangents sums each vertex's own. A flat inner side's copied columns share a position and a UV, and on a circle path the two copies of a straight column take normals that differ only by rounding. Where that rounding leaves them equal to the last bit, meshoptimizer gives each copy the sum of both sides, at its station's own angle, while each copy's tangent leans a quarter of a step toward its own side: 3.4 degrees at LOD 3 and 5.2 at LOD 2 on a square hole's inner side. Which pairs weld is the compiler's rounding, so the check failed on Clang, GCC and MSVC ARM64 and passed on MSVC x64. A vertex meshoptimizer joins to another with corners of its own now counts as split and is not compared. Forcing every such pair to weld on MSVC x64 failed the check 90 times before this and none after, and it still compares 1.66 million vertices. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/llvolume_test.cpp | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index b92f2a3098..08745f32a4 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -1165,7 +1165,7 @@ namespace struct TangentAgreement { S32 compared = 0; // vertices it gives one tangent with a length - S32 split = 0; // vertices whose corners it gives more than one + S32 split = 0; // vertices whose corners it gives more than one, or sums with another vertex's S32 lone = 0; // vertices it gives no tangent S32 unsure = 0; // vertices whose tangent rounding decides F64 widest = 0.0; // degrees between the two, at most @@ -1367,6 +1367,30 @@ namespace const std::vector rounding = tangentRoundingDegrees(face); std::vector first(face.mNumVertices, -1); std::vector split(face.mNumVertices, false); + // meshoptimizer welds vertices whose positions, normals and UVs + // compare equal and sums all their corners, where createTangents + // sums each vertex's own. A flat inner side's copied columns share a + // position and a UV, and where their normals come out equal to the + // last bit, as rounding decides on one platform and not another, it + // gives each the sum of both: such a vertex is not held to it. + std::map, U32> places; + for (U32 v : indices) + { + std::array key; + std::memcpy(key.data(), face.mPositions[v].getF32ptr(), 3 * sizeof(F32)); + std::memcpy(key.data() + 3, face.mNormals[v].getF32ptr(), 3 * sizeof(F32)); + std::memcpy(key.data() + 6, face.mTexCoords[v].mV, 2 * sizeof(F32)); + for (U32& bits : key) + { + bits = bits == 0x80000000u ? 0u : bits; // -0 is 0, as meshoptimizer compares them + } + const auto [place, fresh] = places.emplace(key, v); + if (!fresh && place->second != v) + { + split[v] = true; + split[place->second] = true; + } + } for (size_t c = 0; c < indices.size(); ++c) { const U32 v = indices[c];