diff --git a/indra/llmath/alsimdkernels.cpp b/indra/llmath/alsimdkernels.cpp index c3780042a6..3656f81d69 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,4 +100,37 @@ 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) +{ + 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, + const U16* indices, size_t index_count, LLVector4a* tangents) +{ + 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); +} + } // namespace alsimd diff --git a/indra/llmath/alsimdkernels.h b/indra/llmath/alsimdkernels.h index e87f2eca28..79ddfd4ddf 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 @@ -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. @@ -120,6 +126,50 @@ 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); + +// 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. +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 4307492c07..88d012212e 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 { @@ -242,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 @@ -577,6 +790,701 @@ 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); + } +} + +//////////////////////////////////// +// 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 +//////////////////////////////////// + +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 3b401b378a..44cfcaec08 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -601,6 +601,69 @@ 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. 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;i 0.f && toward - across > 1e-6f) || across == 0.f) + { + continue; + } + const F32 s = toward / across; + if (s > 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;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 @@ -2114,15 +2181,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(); @@ -2841,69 +2902,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 +2970,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(); @@ -3043,7 +3115,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(); @@ -3059,15 +3133,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()); @@ -4541,6 +4615,11 @@ LLVolumeFace& LLVolumeFace::operator=(const LLVolumeFace& src) mBeginT = src.mBeginT; mNumS = src.mNumS; mNumT = src.mNumT; + mJoinsS = src.mJoinsS; + mJoinsT = src.mJoinsT; + mReversed = src.mReversed; + mFlexible = src.mFlexible; + std::copy(std::begin(src.mCapTangent), std::end(src.mCapTangent), mCapTangent); mExtents[0] = src.mExtents[0]; mExtents[1] = src.mExtents[1]; @@ -4575,6 +4654,7 @@ LLVolumeFace& LLVolumeFace::operator=(const LLVolumeFace& src) { allocateTangents(src.mNumVertices); LLVector4a::memcpyNonAliased16((F32*) mTangents, (F32*) src.mTangents, vert_size); + mTangentsStale = src.mTangentsStale; } else { @@ -4673,6 +4753,11 @@ 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; + mFlexible = volume->getParams().getPathParams().getCurveType() == LL_PCODE_PATH_FLEXIBLE; + bool ret = false ; if (mTypeMask & CAP_MASK) { @@ -5420,7 +5505,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(); } } @@ -5534,12 +5624,442 @@ 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); 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; + } + + // 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); + + 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); + } + + // 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. 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; + if (!alsimd::normalize3_or_mark(norm, count)) + { + 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; + } + } + + // Twice the area, in profile units, within which a hollow cap's strip + // takes a triangle for one of no area where its hole touches its wall. + constexpr F32 CAP_ZERO_AREA = 1e-6f; + + // Whether a hollow cap's strip takes (1, a, 2) next, a step along its + // 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, 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); + if (area_2ab < -tolerance && !(tolerance > 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) + { + 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; + // 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 && curve != LL_PCODE_PATH_LINE && curve != LL_PCODE_PATH_FLEXIBLE) + { + 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; + } + + // +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) + { + 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, @@ -5597,20 +6117,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; @@ -5622,8 +6131,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; @@ -5694,6 +6205,11 @@ bool LLVolumeFace::createUnCutCubeCap(LLVolume* volume, bool partial_build) } } } + + if (mReversed) + { + reverse_winding(mIndices, mNumIndices); + } } return true; @@ -5721,20 +6237,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); } } @@ -5754,9 +6266,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 @@ -5835,27 +6344,73 @@ 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++; } - //if (partial_build) - //{ - // return true; - //} + 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])); + 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) { + // 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) { // HOLLOW TOP @@ -5900,54 +6455,15 @@ 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 + const auto outer_shorter = [&]() { LLVector4a d1; d1.setSub(p1, pa); - LLVector4a d2; d2.setSub(p2, pb); - - if (d1.dot3(d1) < d2.dot3(d2)) - { - use_tri1a2 = true; - } - else - { - use_tri1a2 = false; - } - } + 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) { @@ -6006,53 +6522,15 @@ 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 + const auto outer_shorter = [&]() { LLVector4a d1; - d1.setSub(p1,pa); + d1.setSub(p1, pa); LLVector4a d2; - d2.setSub(p2,pb); - - if (d1.dot3(d1) < d2.dot3(d2)) - { - use_tri1a2 = true; - } - else - { - use_tri1a2 = false; - } - } + 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 if (use_tri1a2) @@ -6094,36 +6572,9 @@ 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) + 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); - } - - 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()); + reverse_winding(mIndices, mNumIndices); } return true; @@ -6133,29 +6584,70 @@ void LLVolumeFace::createTangents() { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; - if (!mTangents) + if (!mTangents || mTangentsStale) { - allocateTangents(mNumVertices); - - //generate tangents - LLVector4a* ptr = (LLVector4a*)mTangents; - - LLVector4a* end = mTangents + mNumVertices; - while (ptr < end) + if (!mTangents) { - (*ptr++).clear(); + allocateTangents(mNumVertices); } + mTangentsStale = false; - LLCalculateTangentArray(mNumVertices, mPositions, mNormals, mTexCoords, mNumIndices / 3, mIndices, 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) + { + (*ptr++).clear(); + } - //normalize normals - for (S32 i = 0; i < mNumVertices; i++) + 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. + 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 { - //bump map/planar projection code requires normals to be normalized - mNormals[i].normalize3fast(); + alsimd::indexed_tangents(mPositions, mNormals, mTexCoords, mNumVertices, mIndices, mNumIndices, mTangents); } } - } void LLVolumeFace::resizeVertices(S32 num_verts) @@ -6391,13 +6883,19 @@ 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; 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) { @@ -6414,89 +6912,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++; } } @@ -6511,12 +7007,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; @@ -6528,26 +7020,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(); @@ -6581,231 +7058,139 @@ 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&) + + for (t = 0; t < mNumT - 1; ++t) { - LL_WARNS("LLVOLUME") << "Resize of triangle_normals to " << count << " failed" << LL_ENDL; - return false; + 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; + 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; + } } - LLVector4a* output = triangle_normals.mArray; - LLVector4a* end_output = output+count; - - U16* idx = mIndices; - while (output < end_output) + // 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) { - 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; + if (sculpt_stitching == LL_SCULPT_TYPE_NONE) + { + mJoinsS = columns_meet(pos, mNumS, mNumT, 0, mNumS - 1); + mJoinsT = rows_meet(pos, mNumS, 0, mNumT - 1); + } + else + { + 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; + } } - idx = mIndices; - - LLVector4a* src = triangle_normals.mArray; - - for (U32 i = 0; i < count; i++) //for each triangle + if (sculpt_stitching == LL_SCULPT_TYPE_SPHERE) { - LLVector4a c; - c.load4a((F32*) (src++)); - - LLVector4a* n0p = norm+idx[0]; - LLVector4a* n1p = norm+idx[1]; - LLVector4a* n2p = norm+idx[2]; - - idx += 3; + // average normals for north pole - LLVector4a n0,n1,n2; - n0.load4a((F32*) n0p); - n1.load4a((F32*) n1p); - n2.load4a((F32*) n2p); + LLVector4a average; + average.clear(); - n0.add(c); - n1.add(c); - n2.add(c); - - llassert(c.isFinite3()); - - //even out quad contributions - switch (i%2+1) + for (S32 i = 0; i < mNumS; i++) { - 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); - } + average.add(norm[i]); + } - // adjust normals based on wrapping and stitching + // set average + for (S32 i = 0; i < mNumS; i++) + { + norm[i] = average; + } - LLVector4a top; - top.setSub(pos[0], pos[mNumS*(mNumT-2)]); - bool s_bottom_converges = (top.dot3(top) < 0.000001f); + // average normals for south pole - top.setSub(pos[mNumS-1], pos[mNumS*(mNumT-2)+mNumS-1]); - bool s_top_converges = (top.dot3(top) < 0.000001f); + average.clear(); - if (sculpt_stitching == LL_SCULPT_TYPE_NONE) // logic for non-sculpt volumes - { - 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; - } + for (S32 i = 0; i < mNumS; i++) + { + average.add(norm[i + mNumS * (mNumT - 1)]); } - 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; - } + // set average + for (S32 i = 0; i < mNumS; i++) + { + norm[i + mNumS * (mNumT - 1)] = average; } + } - if (volume->getPathType() == LL_PCODE_PATH_CIRCLE && - ((volume->getProfileType() & LL_PCODE_PROFILE_MASK) == LL_PCODE_PROFILE_CIRCLE_HALF)) + if (mJoinsS) + { + for (S32 i = 0; i < mNumT; i++) { - 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); - } - } + LLVector4a n; + n.setAdd(norm[mNumS*i], norm[mNumS*i+mNumS-1]); + norm[mNumS * i] = n; + norm[mNumS * i+mNumS-1] = n; } } - 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 (average_poles) + if (mJoinsT) + { + 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 normals for south pole - - average.clear(); - - 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; - } - + LLVector4a n; + n.setAdd(norm[i], norm[mNumS*(mNumT-1)+i]); + norm[i] = n; + norm[mNumS*(mNumT-1)+i] = n; } + } - - if (wrap_s) - { + // 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)) + { //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); } } - } + normalize_grid_normals(norm, mNumS, mNumT); + return true; } @@ -6906,8 +7291,10 @@ 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: 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 396ee64e1b..d9fa739d9c 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; @@ -941,6 +945,26 @@ 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; + + // 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; + + // 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. @@ -989,6 +1013,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/alsimdkernels_test.cpp b/indra/llmath/tests/alsimdkernels_test.cpp index d20a597d4b..60a71af603 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. @@ -751,4 +796,300 @@ 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]); + } + }); + } + } + } + + // ------------------------------------------------------------------ + // 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 + + // 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()); + } + } + }); + } + } } 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 63467e255a..08745f32a4 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" @@ -51,6 +49,7 @@ #include "v3math.h" #include "v4math.h" #include "llvector4a.h" +#include "alsimdkernels.h" #include #include @@ -374,11 +373,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; @@ -573,83 +567,20 @@ 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 + constexpr U32 Meshopt = 1 << 12; // after genTangents: meshoptimizer's tangents, where it gives a vertex one - // 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 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. + // A cap with no area. 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 - // 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) - { - 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; - } - if (regions & Regions::SquareHole) - { - checks |= Checks::Inversion | Checks::CapFacing | Checks::Oracle; - } - if (regions & Regions::TwoRowSeam) - { - checks |= Checks::SeamsAgree; - } - if (regions & Regions::TwistedCapHub) - { - checks |= Checks::AffineUV; - } - if (regions & Regions::InsideOutCap) - { - checks |= Checks::CapFacing; - } - return checks; } // Vertices that no triangle with area uses. @@ -674,52 +605,12 @@ 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 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; - 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 (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; - } - } - else if (!isSculpt(volume) && isGrid(face)) + if (isCap(face) && std::all_of(triangles.begin(), triangles.end(), [](const Triangle& t) { return t.degenerate; })) { - 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; + regions |= Regions::CapNoArea; } return regions; } @@ -727,15 +618,12 @@ 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; + // Where Checks::Meshopt keeps what it found. + struct TangentAgreement* agreement = nullptr; }; // ------------------------------------------------------------------ @@ -804,16 +692,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)); @@ -882,10 +765,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(); @@ -909,7 +792,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; @@ -1076,8 +959,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)) { @@ -1090,7 +973,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)) { @@ -1123,19 +1006,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) @@ -1157,7 +1043,7 @@ namespace for (S32 v = 0; v < face.mNumVertices; ++v) { const S32 column = v % face.mNumS; - if (skip[v]) + if (lone[v]) { continue; } @@ -1186,7 +1072,57 @@ 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 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) + { + 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) { @@ -1197,10 +1133,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]; @@ -1229,8 +1161,298 @@ 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, 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 + 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); + // 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]; + 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: 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(); @@ -1240,7 +1462,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) @@ -1252,25 +1473,19 @@ 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(face, triangles); if (how.only_regions && !(state.regions & how.only_regions)) { 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) { 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) { @@ -1278,7 +1493,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) { @@ -1286,7 +1501,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)) @@ -1299,24 +1514,32 @@ 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); } } 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.skip, state.checks, report, fmt::format("{}: {}", name, faceName(face))); + 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))); + } } } @@ -2027,9 +2250,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); @@ -2547,12 +2770,14 @@ namespace } // ------------------------------------------------------------------ - // Today's defects + // Defects, a case each: tagged [!shouldfail] while the generator has one - // 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 +2806,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; @@ -2628,16 +2860,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); @@ -2655,15 +2886,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); @@ -2681,15 +2911,17 @@ 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. - 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); @@ -2718,10 +2950,13 @@ 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. - 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; @@ -2733,10 +2968,291 @@ 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); + } + + // 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, + // 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); + } + + // 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. + 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 @@ -2773,9 +3289,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; @@ -2788,9 +3305,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() }) @@ -2834,18 +3351,28 @@ 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); } // 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 }) @@ -2859,7 +3386,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 }) @@ -2874,7 +3401,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 }) @@ -2891,7 +3418,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) @@ -2918,15 +3445,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); @@ -2935,14 +3462,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); @@ -2952,13 +3478,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); @@ -2966,11 +3491,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; @@ -2980,16 +3506,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); 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(); }