diff --git a/indra/llmath/almeshsimplifier.cpp b/indra/llmath/almeshsimplifier.cpp index 4a698063e1..096c4f87e4 100644 --- a/indra/llmath/almeshsimplifier.cpp +++ b/indra/llmath/almeshsimplifier.cpp @@ -34,7 +34,9 @@ #include #include +#include #include +#include #include // base's faces as meshoptimizer takes them, their vertices and indices @@ -372,21 +374,33 @@ namespace // runs that no other of its triangles has, its vertices taken at // their positions. Where faces meet, one's border lies along the // other; faces that overlap, as a garment's inside is a copy of its - // outside, share positions across their insides too. + // outside, share positions across their insides too. A face made + // double-sided holds each triangle twice, once turned over, and each + // is counted once, so that its rim is its border as a face of one + // side's is. std::vector border(vertex_count, 0); std::vector edges; + std::vector> triangles; for (S32 f = 0; f < faces; ++f) { - edges.clear(); + triangles.clear(); for (U32 i = mesh.first_index[f]; i < mesh.first_index[f + 1]; i += 3) { - for (U32 k = 0; k < 3; ++k) + std::array corners = { remap[mesh.indices[i]], remap[mesh.indices[i + 1]], remap[mesh.indices[i + 2]] }; + std::sort(corners.begin(), corners.end()); + triangles.push_back(corners); + } + std::sort(triangles.begin(), triangles.end()); + triangles.erase(std::unique(triangles.begin(), triangles.end()), triangles.end()); + + edges.clear(); + for (const std::array& corners : triangles) + { + for (const auto& [a, b] : { std::pair{ corners[0], corners[1] }, std::pair{ corners[1], corners[2] }, std::pair{ corners[0], corners[2] } }) { - const U32 a = remap[mesh.indices[i + k]]; - const U32 b = remap[mesh.indices[i + (k + 1) % 3]]; if (a != b) { - edges.push_back(((U64)llmin(a, b) << 32) | llmax(a, b)); + edges.push_back(((U64)a << 32) | b); } } } diff --git a/indra/llmath/alsimdkernels.inl b/indra/llmath/alsimdkernels.inl index 511eeac452..455f5c9452 100644 --- a/indra/llmath/alsimdkernels.inl +++ b/indra/llmath/alsimdkernels.inl @@ -619,7 +619,13 @@ void unit_cube_codes_impl(const LLVector4a* src, size_t n, U32* dst) // The packs keep each 128-bit half apart: the codes come out as // points 0, 2, 4, 6, 1, 3, 5, 7. const __m256i order = _mm256_setr_epi32(0, 4, 1, 5, 2, 6, 3, 7); - const auto code = [&](const F32* p) { return _mm256_cvtps_epi32(_mm256_mul_ps(_mm256_add_ps(_mm256_loadu_ps(p), half), steps)); }; + // Clamped before the conversion, which gives a value past what an + // int32 holds the least one, and the packs then the low end. + const auto code = [&](const F32* p) + { + const __m256 scaled = _mm256_mul_ps(_mm256_add_ps(_mm256_loadu_ps(p), half), steps); + return _mm256_cvtps_epi32(_mm256_min_ps(_mm256_max_ps(scaled, _mm256_setzero_ps()), steps)); + }; for (; i + 8 <= n; i += 8) { const F32* p = src[i].getF32ptr(); @@ -631,7 +637,11 @@ void unit_cube_codes_impl(const LLVector4a* src, size_t n, U32* dst) const f32x4 half = alsimd::set1(0.5f); const f32x4 steps = alsimd::set1(255.f); const i32x4 xyz = alsimd::set1_i32(0x00FFFFFF); - const auto code = [&](size_t k) { return alsimd::cvt_round(alsimd::mul(alsimd::add(alsimd::load(src[k].getF32ptr()), half), steps)); }; + const auto code = [&](size_t k) + { + const f32x4 scaled = alsimd::mul(alsimd::add(alsimd::load(src[k].getF32ptr()), half), steps); + return alsimd::cvt_round(alsimd::min(alsimd::max(scaled, alsimd::zero()), steps)); + }; for (; i + 4 <= n; i += 4) { alsimd::storeu_i32(reinterpret_cast(dst + i), alsimd::and_i32(alsimd::pack_u8(code(i), code(i + 1), code(i + 2), code(i + 3)), xyz)); diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index fd38969364..f96dd75361 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -3641,33 +3641,79 @@ void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32 // A sculpt map averaged down by two along x, y or both: each pixel the rounded mean of the two or four it covers. static std::vector sculpt_map_averaged_down(const U8* data, U16 width, U16 height, S8 components, bool along_x, bool along_y) { - const S32 step_x = along_x ? 2 : 1; - const S32 step_y = along_y ? 2 : 1; - const S32 out_width = width / step_x; - const S32 out_height = height / step_y; - std::vector out((size_t)out_width * out_height * components); - for (S32 y = 0; y < out_height; ++y) - { - for (S32 x = 0; x < out_width; ++x) + const size_t pixel = (size_t)components; + const size_t out_width = along_x ? width / 2 : width; + const size_t out_height = along_y ? height / 2 : height; + const size_t in_row = (size_t)width * pixel; + const size_t out_row = out_width * pixel; + std::vector out(out_row * out_height); + for (size_t y = 0; y < out_height; ++y) + { + const U8* upper = data + (along_y ? 2 * y : y) * in_row; + const U8* lower = along_y ? upper + in_row : upper; + U8* to = out.data() + y * out_row; + if (along_x && along_y) + { + for (size_t x = 0; x < out_width; ++x, upper += 2 * pixel, lower += 2 * pixel) + { + for (size_t c = 0; c < pixel; ++c) + { + *to++ = (U8)((upper[c] + upper[pixel + c] + lower[c] + lower[pixel + c] + 2) >> 2); + } + } + } + else if (along_x) { - for (S32 c = 0; c < components; ++c) + for (size_t x = 0; x < out_width; ++x, upper += 2 * pixel) { - S32 sum = 0; - for (S32 dy = 0; dy < step_y; ++dy) + for (size_t c = 0; c < pixel; ++c) { - for (S32 dx = 0; dx < step_x; ++dx) - { - sum += data[((size_t)(y * step_y + dy) * width + (x * step_x + dx)) * components + c]; - } + *to++ = (U8)((upper[c] + upper[pixel + c] + 1) >> 1); } - const S32 count = step_x * step_y; - out[((size_t)y * out_width + x) * components + c] = (U8)((sum + count / 2) / count); + } + } + else + { + for (size_t i = 0; i < out_row; ++i) + { + to[i] = (U8)((upper[i] + lower[i] + 1) >> 1); } } } return out; } +// Whether every pixel of each row of a sculpt map is its row's first, or, where the stitching does not pinch its first +// and last rows to their middle pixels, every row is the first: each mesh row of such a map samples one point, or +// every mesh row the same points, and so does each of the map averaged down any number of times, which has each row one +// colour or its rows alike as it does. +static bool sculpt_map_has_no_area_at_any_size(const U8* data, U16 width, U16 height, S8 components, U8 sculpt_stitching) +{ + const size_t row = (size_t)width * components; + if (sculpt_stitching != LL_SCULPT_TYPE_SPHERE) + { + bool rows_alike = true; + for (U16 y = 1; y < height && rows_alike; ++y) + { + rows_alike = std::memcmp(data + y * row, data, row) == 0; + } + if (rows_alike) + { + return true; + } + } + for (U16 y = 0; y < height; ++y) + { + // Each pixel the one before it: the row from its second pixel on is the row up to its last. + const U8* start = data + y * row; + if (std::memcmp(start + components, start, row - components) != 0) + { + return false; + } + } + return true; +} + // sculpt replaces generate() for sculpted surfaces void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, const U8* sculpt_data, S32 sculpt_level, bool visible_placeholder) { @@ -3687,10 +3733,13 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, // detail nothing but the collapsed lines, and no area. Where the samples have no area, the map averaged down by // two along each such axis is sampled instead, as the level below it would be, until one has or no axis is left // to average; a map whose samples have their area builds as it always has. + // A map that no averaging down gives area is not averaged, and the sizes it would go through are taken as they + // would be, so that the sphere it ends as is the one it would. U16 width = sculpt_width; U16 height = sculpt_height; const U8* data = sculpt_data; std::vector averaged; + bool never_has_area = false; while (true) { S32 requested_sizeS = 0; @@ -3718,7 +3767,10 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, break; } - sculptGenerateMapVertices(width, height, sculpt_components, data, sculpt_type); + if (!never_has_area) + { + sculptGenerateMapVertices(width, height, sculpt_components, data, sculpt_type); + } // don't test lowest LOD to support legacy content DEV-33670 if (mDetail <= SCULPT_MIN_AREA_DETAIL) @@ -3726,7 +3778,7 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, break; } - F32 area = sculptGetSurfaceArea(); + F32 area = never_has_area ? 0.f : sculptGetSurfaceArea(); mSurfaceArea = area; @@ -3747,8 +3799,13 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, visible_placeholder = true; break; } - averaged = sculpt_map_averaged_down(data, width, height, sculpt_components, along_x, along_y); - data = averaged.data(); + never_has_area = + never_has_area || sculpt_map_has_no_area_at_any_size(data, width, height, sculpt_components, sculpt_type & LL_SCULPT_TYPE_MASK); + if (!never_has_area) + { + averaged = sculpt_map_averaged_down(data, width, height, sculpt_components, along_x, along_y); + data = averaged.data(); + } width = along_x ? width / 2 : width; height = along_y ? height / 2 : height; } @@ -5326,6 +5383,36 @@ namespace unit.normalize3(); v.setSelectWithMask(length2.greaterThan(LLVector4a(FLT_MIN)), unit, LLVector4a::getZero()); } + + // meshoptimizer's vertex cache order scores again every triangle a vertex + // in its cache has left whenever it takes a triangle, so a vertex of n + // triangles costs it about n squared: one vertex under the most + // triangles a decoded face may have would take it minutes. Past this + // sum, over the vertices, of each one's triangles squared, some tens of + // milliseconds of scoring, a face keeps the order it has. An ordinary face of 65,535 + // vertices, six triangles each, sums to under 2.4 million. + constexpr U64 CACHE_ORDER_BUDGET = 1ull << 26; + + bool cache_order_affordable(const U32* indices, size_t index_count, size_t vertex_count) + { + ALScratchArena::Scope scratch; + U32* triangles = scratch.allocate(vertex_count); + std::fill_n(triangles, vertex_count, 0u); + for (size_t i = 0; i < index_count; ++i) + { + ++triangles[indices[i]]; + } + U64 cost = 0; + for (size_t v = 0; v < vertex_count; ++v) + { + cost += (U64)triangles[v] * triangles[v]; + if (cost > CACHE_ORDER_BUDGET) + { + return false; + } + } + return true; + } } // namespace // A decoded mesh LOD face made ready to draw: MikkTSpace's tangents, as @@ -5531,7 +5618,14 @@ bool LLVolumeFace::cacheOptimize(bool gen_tangents) U32* ordered = scratch.allocate(index_count); { LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("cacheOptimize - cache order"); - meshopt_optimizeVertexCache(ordered, welded_indices, index_count, welded_count); + if (cache_order_affordable(welded_indices, index_count, welded_count)) + { + meshopt_optimizeVertexCache(ordered, welded_indices, index_count, welded_count); + } + else + { + std::copy_n(welded_indices, index_count, ordered); + } } // Each vertex's final place, in the order the reordered triangles @@ -5557,19 +5651,28 @@ bool LLVolumeFace::cacheOptimize(bool gen_tangents) } resizeVertices((S32)kept); - if (!mPositions) - { - LLError::LLUserWarningMsg::showOutOfMemory(); - LL_ERRS("LLVolume") << "Failed to allocate memory for resizeVertices(" << kept << ")" << LL_ENDL; - } - if (old_weights) + if (mPositions && old_weights) { allocateWeights((S32)kept); } - if (tangents || old_tangents) + if (mPositions && (tangents || old_tangents)) { allocateTangents((S32)kept); } + if (!mPositions || (old_weights && !mWeights) || ((tangents || old_tangents) && !mTangents)) + { + // The face has given up its buffers, and keeps none. This runs on + // the mesh threads, where a fatal error may not be raised. + ll_aligned_free<64>(old_positions); + ll_aligned_free_16(old_weights); + ll_aligned_free_16(old_tangents); + ll_aligned_free_16(mWeights); + mWeights = nullptr; + resizeVertices(0); + resizeIndices(0); + LL_WARNS("LLVolume") << "Failed to allocate memory for " << kept << " vertices in cacheOptimize" << LL_ENDL; + return false; + } for (size_t u = 0; u < welded_count; ++u) { @@ -6350,8 +6453,12 @@ namespace // The rows' frames, each in its station. static_assert(sizeof(LLPath::PathFrame) == 5 * sizeof(LLVector4a), "a frame is five vectors"); static_assert(sizeof(LLPath::PathPt) % sizeof(LLVector4a) == 0, "stations are a whole number of vectors apart"); + static_assert(offsetof(LLPath::PathPt, mFrame) % sizeof(LLVector4a) == 0, "a frame starts on a vector"); constexpr size_t STRIDE = sizeof(LLPath::PathPt) / sizeof(LLVector4a); - const LLVector4a* const frames = stations[begin_t].mFrame.mRow; + // A run of vectors over the stations, taken from the station rather + // than its frame's two rows, which the reads run past. + const LLVector4a* const frames = + reinterpret_cast(stations + begin_t) + offsetof(LLPath::PathPt, mFrame) / sizeof(LLVector4a); // A first or last row the path's scale narrows to a line or a point // takes its frame a step into the face. @@ -7935,6 +8042,16 @@ bool LLVolumeFace::createCap(LLVolume* volume, bool partial_build) } const LLVector4a normal = cap_normal(volume, mTypeMask & TOP_MASK, !partial_build, mReversed); + if (mReversed) + { + // Seen from the side it faces, a reversed cap is the other cap seen + // from its front, and takes that cap's mapping, mirrored in v, so + // that its texture reads the right way round as a bottom cap's does. + for (S32 i = 0; i < num_vertices; ++i) + { + mTexCoords[i].mV[1] = 1.f - mTexCoords[i].mV[1]; + } + } if (mFlexible) { // mCapTangent is plain floats, not aligned as a vector stores. @@ -8457,9 +8574,10 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) // quad's two triangles, bottom left, top right, top left has two corners // on its left column and on its top row, and bottom left, bottom right, // top right on its right column and its bottom row. A face that would be - // left with none keeps them all. + // left with none keeps them all. A sculpt of no stitching is built from + // its map, whatever its parameters would make. Collapse collapse; - if (!volume->isUnique() && sculpt_stitching == LL_SCULPT_TYPE_NONE) + if (!volume->isUnique() && sculpt_stitching == LL_SCULPT_TYPE_NONE && volume->getParams().getSculptID().isNull()) { collapse = collapse_of(volume, mTypeMask & INNER_MASK); if (mTypeMask & END_MASK) diff --git a/indra/llmath/tests/almeshsimplifier_test.cpp b/indra/llmath/tests/almeshsimplifier_test.cpp index 4d6ffd703c..78d730f69e 100644 --- a/indra/llmath/tests/almeshsimplifier_test.cpp +++ b/indra/llmath/tests/almeshsimplifier_test.cpp @@ -848,6 +848,83 @@ namespace } } + // A face made double-sided, as clothing is: its triangles again, turned + // over, on vertices of their own at the same positions facing the other + // way. + Geometry doubleSided(const Geometry& g) + { + Geometry out = g; + const U16 back = (U16)g.positions.size(); + for (size_t v = 0; v < g.positions.size(); ++v) + { + out.add(g.positions[v], -g.normals[v], g.uvs[v]); + } + for (size_t i = 0; i < g.indices.size(); i += 3) + { + out.triangle((U16)(back + g.indices[i]), (U16)(back + g.indices[i + 2]), (U16)(back + g.indices[i + 1])); + } + return out; + } + + // How many of a face's triangles have each edge, at its positions. + std::map edgeUses(const LLVolumeFace& face) + { + std::map uses; + for (S32 i = 0; i < face.mNumIndices; i += 3) + { + for (S32 k = 0; k < 3; ++k) + { + ++uses[edgeOf(keyOf(face.mPositions[face.mIndices[i + k]]), keyOf(face.mPositions[face.mIndices[i + (k + 1) % 3]]))]; + } + } + return uses; + } + + // Two double-sided faces that meet: each uses every edge of its rim + // twice, once on each side, and its rim is still its open border, held + // where the other meets it: each side of each keeps every edge of the + // seam. + TEST_CASE("almeshsimplifier: two double-sided faces simplified face by face still meet", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = volumeOf({ doubleSided(sphereRows(48, 24, 0, 12)), doubleSided(sphereRows(48, 24, 12, 24, 10.f)) }); + const std::set shared = sharedPositions(*base); + REQUIRE(shared.size() == 48); + std::set seam; + for (const auto& [edge, uses] : edgeUses(base->getVolumeFace(0))) + { + if (shared.count(edge.first) && shared.count(edge.second)) + { + seam.insert(edge); + } + } + REQUIRE(seam.size() == 48); + for (F32 decimator : DECIMATORS) + { + for (Seams seams : { Seams::ALL, Seams::UV, Seams::NONE }) + { + INFO(fmt::format("a {}th, {}", decimator, nameOf(seams))); + LLPointer target = targetFor(*base); + for (S32 f = 0; f < 2; ++f) + { + INFO(fmt::format("face {}", f)); + CHECK(ALMeshSimplifier(*base).simplifyFace(f, target->getVolumeFace(f), toCount(decimator), seams) > 0.f); + REQUIRE(wellFormed(target->getVolumeFace(f))); + const std::set kept = positionsOf(target->getVolumeFace(f)); + CHECK(std::includes(kept.begin(), kept.end(), shared.begin(), shared.end())); + const std::map uses = edgeUses(target->getVolumeFace(f)); + size_t sides_kept = 0; + for (const EdgeKey& edge : seam) + { + const auto found = uses.find(edge); + sides_kept += found != uses.end() && found->second == 2; + } + CHECK(sides_kept == seam.size()); + } + } + } + } + // ------------------------------------------------------------------ // Seams diff --git a/indra/llmath/tests/alsimdkernels_test.cpp b/indra/llmath/tests/alsimdkernels_test.cpp index d4d1bf9013..a84ca59c16 100644 --- a/indra/llmath/tests/alsimdkernels_test.cpp +++ b/indra/llmath/tests/alsimdkernels_test.cpp @@ -429,6 +429,31 @@ namespace } }); } + // Far outside the cube, past what a 32-bit integer holds and at + // infinity, a coordinate still clamps to its end. + { + const F32 inf = std::numeric_limits::infinity(); + const F32 outside[] = { 1e7f, -1e7f, 3e9f, -3e9f, inf, -inf, 0.6f, -0.6f }; + std::vector points; + std::vector expected; + for (size_t i = 0; i < 24; ++i) + { + const F32 x = outside[i % 8], y = outside[(i * 3 + 1) % 8], z = outside[(i * 5 + 2) % 8]; + points.emplace_back(x, y, z); + const auto end = [](F32 v) { return v > 0.f ? 255u : 0u; }; + expected.push_back(end(x) | (end(y) << 8) | (end(z) << 16)); + } + for_each_width([&](const char* width) + { + std::vector codes(points.size()); + unit_cube_codes_impl(points.data(), points.size(), codes.data()); + for (size_t i = 0; i < points.size(); ++i) + { + CAPTURE(width, i); + REQUIRE(codes[i] == expected[i]); + } + }); + } for (size_t n : COUNTS) { std::vector points(n + 1); diff --git a/indra/llmath/tests/llvolume_bench.cpp b/indra/llmath/tests/llvolume_bench.cpp index f5f53c6009..4c401c826b 100644 --- a/indra/llmath/tests/llvolume_bench.cpp +++ b/indra/llmath/tests/llvolume_bench.cpp @@ -183,6 +183,30 @@ namespace return map; } + // A map as large as a decode on screen hands a sculpt, each pixel what + // pixel gives: its samples have no area, and the map is averaged down + // until they have or none is left to average. + template + SculptMap largeSculptMap(U16 size, Pixel pixel) + { + SculptMap map; + map.width = size; + map.height = size; + map.rgb.resize((size_t)size * size * 3); + for (U16 y = 0; y < size; ++y) + { + for (U16 x = 0; x < size; ++x) + { + const LLVector3 p = pixel(x, y); + U8* out = &map.rgb[((size_t)y * size + x) * 3]; + out[0] = toByte(p.mV[0]); + out[1] = toByte(p.mV[1]); + out[2] = toByte(p.mV[2]); + } + } + return map; + } + LLPointer sculptVolume(U8 stitching, S32 lod) { LLVolumeParams p; @@ -672,6 +696,30 @@ namespace return volume->getNumVolumeFaces(); }; } + + // Large maps whose samples have no area: one colour and one of rows + // alike, which no averaging gives any, and one of two halves either + // side of a diagonal, which is averaged down to the end. + constexpr U16 LARGE = 2048; + const struct + { + const char* name; + SculptMap map; + } arealess[] = { + { "one colour", largeSculptMap(LARGE, [](U16, U16) { return LLVector3(0.1f, -0.2f, 0.3f); }) }, + { "rows alike", largeSculptMap(LARGE, [](U16 x, U16) { return LLVector3((F32)x / (LARGE - 1) - 0.5f, 0.f, 0.f); }) }, + { "diagonal halves", + largeSculptMap(LARGE, [](U16 x, U16 y) { return x + y < LARGE ? LLVector3(-0.25f, 0.f, 0.f) : LLVector3(0.25f, 0.f, 0.f); }) }, + }; + for (const auto& [name, map] : arealess) + { + LLPointer volume = sculptVolume(LL_SCULPT_TYPE_PLANE, 3); + BENCHMARK(fmt::format("plane sculpt, LOD 3, {} by {} map of {}", map.width, map.height, name)) + { + volume->sculpt(map.width, map.height, 3, map.rgb.data(), 0, true); + return volume->getNumVolumeFaces(); + }; + } } // createTangents sums meshoptimizer's default weighting a vertex at a diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 0b81b79957..50009d1f8e 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -67,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -860,6 +861,25 @@ namespace } } + // A cap's texture reads the right way round from the side it faces, a + // bottom cap's as a top cap's: its triangles wind the same way in UV as + // they do seen from in front, whichever way the cap faces. + void checkCapUV(const LLVolumeFace& face, Report& report, const std::string& where) + { + for (S32 t = 0; t + 2 < face.mNumIndices; t += 3) + { + const LLVector2& a = face.mTexCoords[face.mIndices[t]]; + const LLVector2& b = face.mTexCoords[face.mIndices[t + 1]]; + const LLVector2& c = face.mTexCoords[face.mIndices[t + 2]]; + const F32 area = (b.mV[0] - a.mV[0]) * (c.mV[1] - a.mV[1]) - (c.mV[0] - a.mV[0]) * (b.mV[1] - a.mV[1]); + if (area < -1e-6f) + { + report.fail(fmt::format("{}: triangle {} winds against its UVs: the texture reads mirrored from in front", where, t / 3)); + return; + } + } + } + // A face whose positions lie in a plane and, for a side, whose every // grid quad is a parallelogram, maps its UVs as an affine function of // position: least squares over the plane must fit every vertex. @@ -1627,6 +1647,7 @@ namespace if ((state.checks & Checks::CapFacing) && isCap(face)) { checkCapFacing(volume, face, state.raw, report, where); + checkCapUV(face, report, where); } // A sculpt's UVs are its grid, wherever the map puts the points. if ((state.checks & Checks::AffineUV) && !isSculpt(volume)) @@ -3330,6 +3351,59 @@ namespace } // The placeholders' normals have a case of their own, below. + // A sculpt block of no stitching with a map builds from the map, on + // whatever parameters its prim has. None of its rows or columns is the + // pole or apex those parameters would make of a prim's, and it keeps + // every triangle of its grid, as it always has. + TEST_CASE("llvolume: a sculpt of no stitching keeps every triangle of its map", "[llvolume]") + { + struct Shape + { + const char* name; + U8 profile; + U8 path; + F32 ratio; + }; + const Shape shapes[] = { + { "sphere", LL_PCODE_PROFILE_CIRCLE_HALF, LL_PCODE_PATH_CIRCLE, 1.f }, + { "line tapered to its end", LL_PCODE_PROFILE_CIRCLE, LL_PCODE_PATH_LINE, 0.f }, + { "line tapered from its beginning", LL_PCODE_PROFILE_CIRCLE, LL_PCODE_PATH_LINE, 2.f }, + }; + const SculptMap map = sphereMap(64, 64); + Report report; + for (const Shape& shape : shapes) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLVolumeParams p; + p.setType(shape.profile, shape.path); + p.setRatio(shape.ratio, shape.ratio); + p.setSculptID(LLUUID("c8b26d0b-6f5a-4f10-8d1e-5c3b3e4a9a01"), LL_SCULPT_TYPE_NONE); + LLPointer volume = new LLVolume(p, LOD_DETAIL[lod]); + volume->sculpt(map.width, map.height, 3, map.rgb.data(), 0, true); + S32 sides = 0; + for (S32 f = 0; f < volume->getNumVolumeFaces(); ++f) + { + const LLVolumeFace& face = volume->getVolumeFace(f); + if ((face.mTypeMask & LLVolumeFace::CAP_MASK) || (face.mTypeMask & LLVolumeFace::FLAT_MASK)) + { + continue; + } + ++sides; + const S32 grid = 6 * (face.mNumS - 1) * (face.mNumT - 1); + if (face.mNumIndices != grid) + { + report.fail(fmt::format("{} at LOD {}: {} has {} indices of its grid's {}", shape.name, lod, faceName(face), + face.mNumIndices, grid)); + } + } + CHECK(sides > 0); + } + } + INFO(report.text()); + CHECK(report.failures == 0); + } + TEST_CASE("llvolume: an empty sculpt map makes a placeholder", "[llvolume]") { Inspect how; @@ -3477,6 +3551,50 @@ namespace } } + // A fan of 60,000 triangles round one vertex, in shuffled order: a vertex + // cache order would score that vertex's triangles again for each it + // takes, some 1.8 billion times, so the face keeps the order it has. + TEST_CASE("llvolume: cacheOptimize keeps the order of a face with a vertex under too many triangles", "[llvolume]") + { + constexpr U32 RIM = 60000; + CorpusMesh mesh; + mesh.name = "fan"; + mesh.positions.emplace_back(0.f, 0.f, 0.f); + for (U32 i = 0; i < RIM; ++i) + { + const F32 angle = F_TWO_PI * (F32)i / (F32)RIM; + mesh.positions.emplace_back(0.5f * cosf(angle), 0.5f * sinf(angle), 0.f); + } + for (const LLVector3& p : mesh.positions) + { + mesh.normals.emplace_back(0.f, 0.f, 1.f); + mesh.uvs.emplace_back(p.mV[0] + 0.5f, p.mV[1] + 0.5f); + } + std::vector order(RIM); + std::iota(order.begin(), order.end(), 0u); + std::shuffle(order.begin(), order.end(), std::mt19937(7)); + for (U32 t : order) + { + mesh.indices.push_back(0); + mesh.indices.push_back((U16)(1 + t)); + mesh.indices.push_back((U16)(1 + (t + 1) % RIM)); + } + + const LLVolumeFace before = mesh.face(); + LLVolumeFace after = mesh.face(); + REQUIRE(after.cacheOptimize(true)); + REQUIRE(after.mNumIndices == before.mNumIndices); + S32 moved = 0; + for (S32 i = 0; i < before.mNumIndices; ++i) + { + if (bitsOf(after.mPositions[after.mIndices[i]]) != bitsOf(before.mPositions[before.mIndices[i]])) + { + ++moved; + } + } + CHECK(moved == 0); + } + TEST_CASE("llvolume: cacheOptimize splits the vertices a mirrored UV seam joins", "[llvolume]") { const CorpusMesh mesh = mirroredStrip(); diff --git a/indra/newview/gltf/primitive.cpp b/indra/newview/gltf/primitive.cpp index 9c4e52a0d4..5ee0ef6fcd 100644 --- a/indra/newview/gltf/primitive.cpp +++ b/indra/newview/gltf/primitive.cpp @@ -374,6 +374,23 @@ bool Primitive::prep(Asset& asset) } } + // Every attribute is read at each position's index, and skinning takes + // joints and weights together. + for (size_t count : { mNormals.size(), mTangents.size(), mColors.size(), mTexCoords0.size(), mTexCoords1.size(), mJoints.size(), + mWeights.size() }) + { + if (count != 0 && count != mPositions.size()) + { + LL_WARNS("GLTF") << "Attribute of " << count << " elements for " << mPositions.size() << " positions" << LL_ENDL; + return false; + } + } + if (mJoints.empty() != mWeights.empty()) + { + LL_WARNS("GLTF") << "Joints and weights not given together" << LL_ENDL; + return false; + } + // copy index buffer if (mIndices != INVALID_INDEX) { diff --git a/indra/newview/llface.cpp b/indra/newview/llface.cpp index b06a87b844..9ad17398ae 100644 --- a/indra/newview/llface.cpp +++ b/indra/newview/llface.cpp @@ -1118,6 +1118,21 @@ bool LLFace::getGeometryVolume(const LLVolume& volume, if (mVertexBuffer.notNull()) { + // A face fills the slots its batch laid out for it, as large as its + // volume face was then. A shared sculpt built again from a better map + // keeps its grid but may leave out other triangles, and its faces + // filled into slots of another size would write into the next face's + // or leave their own tails drawn: the batch is laid out again. + if (num_indices != mIndicesCount || num_vertices > mGeomCount) + { + if (gDebugGL) + { + LL_WARNS() << "Face slots no longer fit: " << num_vertices << " vertices and " << num_indices << " indices into " + << mGeomCount << " and " << mIndicesCount << LL_ENDL; + } + return false; + } + if (num_indices + mIndicesIndex > mVertexBuffer->getNumIndices()) { if (gDebugGL) diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index 47bd85e1ca..374821d3e4 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -717,6 +717,7 @@ void LLModelPreview::clearModel(S32 lod) mVertexBuffer[lod].clear(); mModel[lod].clear(); mScene[lod].clear(); + dropFacesCopiesOfLostModels(); } void LLModelPreview::getJointAliases(JointMap& joint_map) @@ -929,6 +930,7 @@ void LLModelPreview::clearIncompatible(S32 lod) } } } + dropFacesCopiesOfLostModels(); if (replaced_base_model && !mGenLOD) { @@ -1276,6 +1278,7 @@ void LLModelPreview::loadModelCallback(S32 loaded_lod) LLFloaterModelPreview::addStringToLog("ModelLoaded", args, false, loaded_lod); } } + dropFacesCopiesOfLostModels(); refresh(); mModelLoadedSignal(); @@ -1318,18 +1321,25 @@ void LLModelPreview::generateNormals() angle_cutoff *= DEG_TO_RAD; - if (which_lod == 3 && !mBaseModel.empty()) + // Every model's faces are copied before any is rewritten: the base's + // models are the high LOD's own. + const auto keep = [](faces_copy_t& copy, const LLModelLoader::model_list& models) { - if (mBaseModelFacesCopy.empty()) + for (const LLPointer& model : models) { - mBaseModelFacesCopy.reserve(mBaseModel.size()); - for (LLModelLoader::model_list::iterator it = mBaseModel.begin(), itE = mBaseModel.end(); it != itE; ++it) + if (std::none_of(copy.begin(), copy.end(), [&](const faces_copy_t::value_type& kept) { return kept.first == model; })) { v_LLVolumeFace_t faces; - (*it)->copyFacesTo(faces); - mBaseModelFacesCopy.push_back(faces); + model->copyFacesTo(faces); + copy.emplace_back(model, std::move(faces)); } } + }; + keep(mModelFacesCopy[which_lod], mModel[which_lod]); + + if (which_lod == 3 && !mBaseModel.empty()) + { + keep(mBaseModelFacesCopy, mBaseModel); for (LLModelLoader::model_list::iterator it = mBaseModel.begin(), itE = mBaseModel.end(); it != itE; ++it) { @@ -1339,20 +1349,8 @@ void LLModelPreview::generateNormals() mVertexBuffer[LLModel::NUM_LODS].clear(); } - bool perform_copy = mModelFacesCopy[which_lod].empty(); - if (perform_copy) { - mModelFacesCopy[which_lod].reserve(mModel[which_lod].size()); - } - for (LLModelLoader::model_list::iterator it = mModel[which_lod].begin(), itE = mModel[which_lod].end(); it != itE; ++it) { - if (perform_copy) - { - v_LLVolumeFace_t faces; - (*it)->copyFacesTo(faces); - mModelFacesCopy[which_lod].push_back(faces); - } - (*it)->generateNormals(angle_cutoff); } @@ -1375,29 +1373,17 @@ void LLModelPreview::restoreNormals() // generateNormals. mSimplifiers.clear(); - if (!mBaseModelFacesCopy.empty()) + // Each model takes back its own faces, whatever the LOD's models are now. + const auto restore = [](faces_copy_t& copy) { - llassert(mBaseModelFacesCopy.size() == mBaseModel.size()); - - vv_LLVolumeFace_t::const_iterator itF = mBaseModelFacesCopy.begin(); - for (LLModelLoader::model_list::iterator it = mBaseModel.begin(), itE = mBaseModel.end(); it != itE; ++it, ++itF) + for (const faces_copy_t::value_type& kept : copy) { - (*it)->copyFacesFrom((*itF)); + kept.first->copyFacesFrom(kept.second); } - - mBaseModelFacesCopy.clear(); - } - - if (!mModelFacesCopy[which_lod].empty()) - { - vv_LLVolumeFace_t::const_iterator itF = mModelFacesCopy[which_lod].begin(); - for (LLModelLoader::model_list::iterator it = mModel[which_lod].begin(), itE = mModel[which_lod].end(); it != itE; ++it, ++itF) - { - (*it)->copyFacesFrom((*itF)); - } - - mModelFacesCopy[which_lod].clear(); - } + copy.clear(); + }; + restore(mBaseModelFacesCopy); + restore(mModelFacesCopy[which_lod]); mVertexBuffer[which_lod].clear(); refresh(); @@ -1423,6 +1409,35 @@ const ALMeshSimplifier& LLModelPreview::simplifierFor(U32 mdl_idx) return *kept.mSimplifier; } +void LLModelPreview::dropFacesCopiesOfLostModels() +{ + const auto held = [this](const LLPointer& model) + { + if (std::find(mBaseModel.begin(), mBaseModel.end(), model) != mBaseModel.end()) + { + return true; + } + for (const LLModelLoader::model_list& models : mModel) + { + if (std::find(models.begin(), models.end(), model) != models.end()) + { + return true; + } + } + return false; + }; + const auto drop = [&](faces_copy_t& copy) + { + copy.erase(std::remove_if(copy.begin(), copy.end(), [&](const faces_copy_t::value_type& kept) { return !held(kept.first); }), + copy.end()); + }; + drop(mBaseModelFacesCopy); + for (faces_copy_t& copy : mModelFacesCopy) + { + drop(copy); + } +} + void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 decimation, bool enforce_tri_limit) { LL_DEBUGS("Upload") << "Generating lod " << which_lod << " using meshoptimizer" << LL_ENDL; @@ -1816,6 +1831,8 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d } } + dropFacesCopiesOfLostModels(); + LL_INFOS("Upload") << "LOD " << which_lod << ", Mesh optimizer processed meshes : " << meshes_processed <<" simplified: " << meshes_simplified << ", slopily simplified: " << meshes_sloppy_simplified @@ -2421,6 +2438,7 @@ void LLModelPreview::updateLodControls(S32 lod) mModel[lod] = mModel[lod + 1]; mScene[lod] = mScene[lod + 1]; mVertexBuffer[lod].clear(); + dropFacesCopiesOfLostModels(); // Also update lower LoD if (lod > LLModel::LOD_IMPOSTOR) diff --git a/indra/newview/llmodelpreview.h b/indra/newview/llmodelpreview.h index cce9a54473..b62030a349 100644 --- a/indra/newview/llmodelpreview.h +++ b/indra/newview/llmodelpreview.h @@ -208,6 +208,9 @@ class LLModelPreview : public LLViewerDynamicTexture, public LLMutex, public LLH void warnTextureScaling(); void lookupLODModelFiles(S32 lod); + // Drops the face copies of models no LOD and not the base holds any + // longer, which nothing restores into and the copies alone keep. + void dropFacesCopiesOfLostModels(); private: //Utility function for controller vertex compare @@ -287,10 +290,14 @@ class LLModelPreview : public LLViewerDynamicTexture, public LLMutex, public LLH LLModelLoader::model_list mBaseModel; typedef std::vector v_LLVolumeFace_t; - typedef std::vector vv_LLVolumeFace_t; - - vv_LLVolumeFace_t mModelFacesCopy[LLModel::NUM_LODS]; - vv_LLVolumeFace_t mBaseModelFacesCopy; + // Each model's faces as they were before generateNormals first rewrote + // them, for restoreNormals to put back into that model: a model the + // base and a LOD share is copied once, before either is rewritten, and + // a model loaded or made since takes nothing of another's. + typedef std::vector, v_LLVolumeFace_t>> faces_copy_t; + + faces_copy_t mModelFacesCopy[LLModel::NUM_LODS]; + faces_copy_t mBaseModelFacesCopy; // A simplifier of each of mBaseModel's models, made the first time a // level of detail is generated from the model and kept for the levels diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 552222598b..e1feeb4593 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -1504,13 +1504,14 @@ void LLVOVolume::sculpt() // Until then the volume builds from the image there is, and the read, with more pixels at the same level, // builds it again below. A texture with nothing on screen to read builds from the copy there is, which the // decode setForSculpt asks for brings up to size. - // The copy the texture on screen makes is within the sculpts' budget, as a decode's is kept. + // The copy the texture on screen makes is within the sculpts' budget, as a decode's is kept. A map of fewer than + // three components is no map a sculpt builds from, and reads nothing back. S32 copy_width = 0; S32 copy_height = 0; S32 copy_discard = -1; mSculptTexture->getSculptCopySize(copy_width, copy_height, copy_discard); const S32 copy_pixels = copy_width * copy_height; - if ((!raw_image || raw_image->getWidth() * raw_image->getHeight() < copy_pixels) && + if (mSculptTexture->getComponents() >= 3 && (!raw_image || raw_image->getWidth() * raw_image->getHeight() < copy_pixels) && getVolume()->getSculptPixels() < copy_pixels) { mSculptTexture->requestSculptReadback(); @@ -5105,7 +5106,20 @@ bool LLVOVolume::lineSegmentIntersect(const LLVector4a& start, const LLVector4a& } bool no_texture = !face->getTexture() || !face->getTexture()->hasGLTexture(); - bool mask = no_texture ? false : face->getTexture()->getMask(face->surfaceToTexture(tc, p, n)); + bool mask = false; + if (!no_texture) + { + // surfaceToTexture takes the hit in agent space, which a + // planar face projects from. + LLVector4a agent_p = p; + LLVector4a agent_n = n; + if (transform) + { + agent_p.load3(volumePositionToAgent(LLVector3(p.getF32ptr())).mV); + agent_n.load3(volumeDirectionToAgent(LLVector3(n.getF32ptr())).mV); + } + mask = face->getTexture()->getMask(face->surfaceToTexture(tc, agent_p, agent_n)); + } if (face && (ignore_alpha || pick_transparent || no_texture || mask)) { diff --git a/indra/vcpkg-configuration.json b/indra/vcpkg-configuration.json index 741a2ec9ea..db196d2fa9 100644 --- a/indra/vcpkg-configuration.json +++ b/indra/vcpkg-configuration.json @@ -2,7 +2,7 @@ "registries": [ { "kind": "git", - "baseline": "18989321bd0cb4b6163827833989621fce1e9209", + "baseline": "ee9cfc3d9ac97195a137d8fc1a1fd1902f1858d6", "reference": "main", "repository": "https://github.com/AlchemyViewer/alchemy-registry", "packages": [