diff options
| author | Erik Kundiman <erik@megapahit.org> | 2026-09-01 18:35:23 +0800 |
|---|---|---|
| committer | Erik Kundiman <erik@megapahit.org> | 2026-09-04 21:58:53 +0800 |
| commit | 00bb3bb6f07660bd914d29a1389342bb3060d254 (patch) | |
| tree | 431f574922494d2201718f13085f17bc6bd7fb42 /indra/llmath/llvolume.cpp | |
| parent | a8636720129952c10cf49ab80da21bf8b340b96c (diff) | |
| parent | 4ef9f8f14b35ed388c294d53767a0dca0e890924 (diff) | |
Merge remote-tracking branch 'secondlife/release/26.4' into 26.4
Diffstat (limited to 'indra/llmath/llvolume.cpp')
| -rw-r--r-- | indra/llmath/llvolume.cpp | 356 |
1 files changed, 341 insertions, 15 deletions
diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 4660026d40..c49d5ff7d3 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -52,6 +52,7 @@ #include "llmeshoptimizer.h" #include "lltimer.h" #include "llvolumeoctree.h" +#include "workqueue.h" #include "mikktspace/mikktspace.hh" @@ -1971,6 +1972,8 @@ LLVolume::LLVolume(const LLVolumeParams ¶ms, const F32 detail, const bool ge mNumHullIndices = 0; // set defaults + mSculptValidationCache.fill(LLSculptValidationState::Unvalidated); + if (mParams.getPathParams().getCurveType() == LL_PCODE_PATH_FLEXIBLE) { mPathp = new LLDynamicPath(); @@ -1998,6 +2001,13 @@ void LLVolume::resizePath(S32 length) setDirty(); } +void LLVolume::setDirty() +{ + mPathp->setDirty(); + mProfilep->setDirty(); + mSculptValidationCache.fill(LLSculptValidationState::Unvalidated); +} + void LLVolume::regen() { generate(); @@ -2292,30 +2302,149 @@ bool LLVolume::unpackVolumeFaces(std::istream& is, S32 size) { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; + // Sanity-check before even trying to decompress + constexpr S32 MAX_MESH_COMPRESSED_SIZE = 128 * 1024 * 1024; // 128 MB + const LLUUID& mesh_id = getParams().getSculptID(); + + if (size <= 0 || size > MAX_MESH_COMPRESSED_SIZE) + { + LL_WARNS("MeshStreaming") << "Rejecting implausible compressed mesh size " << size + << " for mesh id " << mesh_id << LL_ENDL; + return false; + } + //input stream is now pointing at a zlib compressed block of LLSD //decompress block - LLSD mdl; - U32 uzip_result = LLUZipHelper::unzip_llsd(mdl, is, size); - if (uzip_result != LLUZipHelper::ZR_OK) + try { - LL_DEBUGS("MeshStreaming") << "Failed to unzip LLSD blob for LoD with code " << uzip_result << " , will probably fetch from sim again." << LL_ENDL; + LLSD mdl; + U32 uzip_result = LLUZipHelper::unzip_llsd(mdl, is, size); + if (uzip_result != LLUZipHelper::ZR_OK) + { + LL_DEBUGS("MeshStreaming") << "Failed to unzip LLSD blob for LoD with code " << uzip_result << " , will probably fetch from sim again." << LL_ENDL; + return false; + } + return unpackVolumeFacesInternal(mdl); + } + catch (const std::bad_alloc&) + { + constexpr S32 SMALL_MESH_THRESHOLD = 4000; + if (size < SMALL_MESH_THRESHOLD) + { + // showOutOfMemory and LL_ERRS must run on the main thread. + // Post to mainloop WorkQueue, mirroring LLThread::tryRun(). + LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); + bool done = false; + if (main_queue) + { + const LLUUID mesh_id_copy = mesh_id; // capture by value for the lambda + done = main_queue->post([mesh_id_copy, size]() + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id_copy + << " of compressed size " << size << LL_ENDL; + }); + } + if (!done) + { + // No main queue available (e.g. during shutdown) + LL_WARNS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id + << " of compressed size " << size << " (main queue unavailable)" << LL_ENDL; + } + } + else + { + LL_WARNS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id + << " of compressed size " << size << LL_ENDL; + } + return false; + } + catch (const std::exception& e) + { + LL_WARNS("MeshStreaming") << "Exception unpacking mesh id " << mesh_id + << " of compressed size " << size << ": " << e.what() << LL_ENDL; + return false; + } + catch (...) + { + LL_WARNS("MeshStreaming") << "Unknown exception unpacking mesh id " << mesh_id + << " of compressed size " << size << LL_ENDL; return false; } - return unpackVolumeFacesInternal(mdl); } bool LLVolume::unpackVolumeFaces(U8* in_data, S32 size) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; + constexpr S32 MAX_MESH_COMPRESSED_SIZE = 128 * 1024 * 1024; // 128 MB + const LLUUID& mesh_id = getParams().getSculptID(); + + if (!in_data || size <= 0 || size > MAX_MESH_COMPRESSED_SIZE) + { + LL_WARNS("MeshStreaming") << "Rejecting implausible compressed mesh size " << size + << " for mesh id " << mesh_id << LL_ENDL; + return false; + } + //input data is now pointing at a zlib compressed block of LLSD //decompress block - LLSD mdl; - U32 uzip_result = LLUZipHelper::unzip_llsd(mdl, in_data, size); - if (uzip_result != LLUZipHelper::ZR_OK) + try { - LL_DEBUGS("MeshStreaming") << "Failed to unzip LLSD blob for LoD with code " << uzip_result << " , will probably fetch from sim again." << LL_ENDL; + LLSD mdl; + U32 uzip_result = LLUZipHelper::unzip_llsd(mdl, in_data, size); + if (uzip_result != LLUZipHelper::ZR_OK) + { + LL_DEBUGS("MeshStreaming") << "Failed to unzip LLSD blob for LoD, mesh id " << mesh_id + << ", code " << uzip_result << " , will probably fetch from sim again." << LL_ENDL; + return false; + } + return unpackVolumeFacesInternal(mdl); + } + catch (const std::bad_alloc&) + { + constexpr S32 SMALL_MESH_THRESHOLD = 4000; + if (size < SMALL_MESH_THRESHOLD) + { + // showOutOfMemory and LL_ERRS must run on the main thread. + // Post to mainloop WorkQueue, mirroring LLThread::tryRun(). + LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); + bool done = false; + if (main_queue) + { + const LLUUID mesh_id_copy = mesh_id; // capture by value for the lambda + done = main_queue->post([mesh_id_copy, size]() + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id_copy + << " of compressed size " << size << LL_ENDL; + }); + } + if (!done) + { + // No main queue available (e.g. during shutdown) + LL_WARNS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id + << " of compressed size " << size << " (main queue unavailable)" << LL_ENDL; + } + } + else + { + LL_WARNS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id + << " of compressed size " << size << LL_ENDL; + } + return false; + } + catch (const std::exception& e) + { + LL_WARNS("MeshStreaming") << "Exception unpacking mesh id " << mesh_id + << " of compressed size " << size << ": " << e.what() << LL_ENDL; + return false; + } + catch (...) + { + LL_WARNS("MeshStreaming") << "Unknown exception unpacking mesh id " << mesh_id + << " of compressed size " << size << LL_ENDL; return false; } - return unpackVolumeFacesInternal(mdl); } bool LLVolume::unpackVolumeFacesInternal(const LLSD& mdl) @@ -2348,7 +2477,9 @@ bool LLVolume::unpackVolumeFacesInternal(const LLSD& mdl) const LLSD::Binary& pos = mdl[i]["Position"].asBinary(); const LLSD::Binary& norm = mdl[i]["Normal"].asBinary(); +#if 0 // keep this code for now in case we decide to add support for on-the-wire tangents const LLSD::Binary& tangent = mdl[i]["Tangent"].asBinary(); +#endif const LLSD::Binary& tc = mdl[i]["TexCoord0"].asBinary(); const LLSD::Binary& idx = mdl[i]["TriangleList"].asBinary(); @@ -2383,11 +2514,31 @@ bool LLVolume::unpackVolumeFacesInternal(const LLSD& mdl) //copy out vertices U32 num_verts = static_cast<U32>(pos.size())/(3*2); + if (num_verts == 0) + { + LL_WARNS() << "Zero vertices for face index: " << i << LL_ENDL; + face.resizeIndices(3); + face.resizeVertices(1); + face.mPositions->clear(); + face.mNormals->clear(); + face.mTexCoords->setZero(); + memset(face.mIndices, 0, sizeof(U16) * 3); + continue; + } + + if (num_verts > 65535) // U16 indices + { + LL_WARNS() << "Invalid vertex count " << num_verts << " exceeds maximum for face index: " << i << LL_ENDL; + mVolumeFaces.clear(); + return false; + } + face.resizeVertices(num_verts); if (num_verts > 0 && !face.mPositions) { LL_WARNS() << "Failed to allocate " << num_verts << " vertices for face index: " << i << " Total: " << face_count << LL_ENDL; + face.resizeVertices(0); face.resizeIndices(0); continue; } @@ -3138,10 +3289,100 @@ S32 sculpt_sides(F32 detail) } } +static bool validate_sculpt_geometry(const LLVolume* volume) +{ + if (!volume || volume->getNumVolumeFaces() == 0) + { + return true; + } + + // Validate each face in the sculpt + for (S32 face_idx = 0; face_idx < volume->getNumVolumeFaces(); ++face_idx) + { + const LLVolumeFace& face = volume->getVolumeFace(face_idx); + + // Skip validation for degenerate faces + if (face.mNumVertices < 3 || face.mNumIndices < 3) + { + continue; + } + + // Calculate 'bounding' surface + LLVector4a extent_size; + extent_size.setSub(face.mExtents[1], face.mExtents[0]); + + F32 width = extent_size[0]; + F32 height = extent_size[1]; + F32 depth = extent_size[2]; + + F32 bounding_surface = width * height + width * depth + height * depth; + bounding_surface *= 2; + + if (!llfinite(bounding_surface) || bounding_surface <= FLT_EPSILON) + { + return false; + } + + // Calculate total surface area of all triangles + F32 total_triangle_area = 0.0f; + S32 num_triangles = face.mNumIndices / 3; + + for (S32 i = 0; i < num_triangles; ++i) + { + S32 idx_base = i * 3; + U16 idx0 = face.mIndices[idx_base]; + U16 idx1 = face.mIndices[idx_base + 1]; + U16 idx2 = face.mIndices[idx_base + 2]; + + // Validate indices + if (idx0 >= face.mNumVertices || idx1 >= face.mNumVertices || idx2 >= face.mNumVertices) + { + continue; + } + const LLVector4a& v0 = face.mPositions[idx0]; + const LLVector4a& v1 = face.mPositions[idx1]; + const LLVector4a& v2 = face.mPositions[idx2]; + + // Calculate triangle area + LLVector4a edge1, edge2, cross; + edge1.setSub(v1, v0); + edge2.setSub(v2, v0); + cross.setCross3(edge1, edge2); + + F32 triangle_area = cross.getLength3().getF32() * 0.5f; + total_triangle_area += triangle_area; + } + + if (!llfinite(total_triangle_area)) + { + return false; + } + + // The idea here is that the total area of a sculpt is normally comparable + // to the area of a bounding box. But a random overlapping collection of + // triangles has a significantly larger area than a sculpt normally has. + F32 area_ratio = total_triangle_area / bounding_surface; + + constexpr F32 MAX_AREA_TO_AREA_RATIO = 16.0f; + if (area_ratio > MAX_AREA_TO_AREA_RATIO) + { + LL_DEBUGS("LLVOLUME") << "Sculpt rejected: excessive triangle area." + << " Face index: " << face_idx + << " Total triangle area: " << total_triangle_area << " sq m" + << " Bounding surface area: " << bounding_surface << " sq m" + << " Ratio: " << area_ratio + << " Triangle count: " << num_triangles + << " Bounds: [" << width << " x " << height << " x " << depth << "]" << LL_ENDL; + return false; + } + } + + return true; +} // determine the number of vertices in both s and t direction for this sculpt -void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32& s, S32& t) +static void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32& s, S32& t) { // this code has the following properties: // 1) the aspect ratio of the mesh is as close as possible to the ratio of the map @@ -3251,10 +3492,62 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, mSculptLevel = sculpt_level; - // Delete any existing faces so that they get regenerated - mVolumeFaces.clear(); + LLSculptValidationState cached_state = LLSculptValidationState::Valid; + if (!data_is_empty && (sculpt_level >= 0) && sculpt_level < SCULPT_CACHE_SIZE) + { + // validation might be expensive, so we do it only once per lod. + cached_state = mSculptValidationCache[sculpt_level]; + } - createVolumeFaces(); + switch (cached_state) + { + case LLSculptValidationState::Valid: + { + // Delete any existing faces, then regenerate + mVolumeFaces.clear(); + createVolumeFaces(); + break; + } + case LLSculptValidationState::Invalid: + { + // Invalid geometry + // Regenerate mesh with an empty placeholder + mVolumeFaces.clear(); + sculptGenerateEmptyPlaceholder(); + createVolumeFaces(); + break; + } + case LLSculptValidationState::Unvalidated: + { + // First time at this LOD + mVolumeFaces.clear(); + createVolumeFaces(); + + bool valid_geometry = true; + + // Todo: can this be backed into createVolumeFaces? + // Or calculated without having to call createVolumeFaces first? + // Todo 2: should the same be done for meshes? + valid_geometry = validate_sculpt_geometry(this); + mSculptValidationCache[sculpt_level] = valid_geometry ? LLSculptValidationState::Valid : LLSculptValidationState::Invalid; + + if (!valid_geometry) + { + LL_WARNS("LLVOLUME") << "Sculpt failed geometry validation - either invalid image or malicious. " + << "Replacing with placeholder. Image: " << mParams.getSculptID() << LL_ENDL; + + // Clear the malicious faces + mVolumeFaces.clear(); + + // Regenerate mesh with empty placeholder + sculptGenerateEmptyPlaceholder(); + + // Recreate faces with placeholder geometry + createVolumeFaces(); + } + break; + } + } //switch (cached_state) } @@ -5677,7 +5970,40 @@ bool LLVolumeFace::cacheOptimize(bool gen_tangents) mOptimized = true; if (gen_tangents && mNormals && mTexCoords) - { // generate mikkt space tangents before cache optimizing since the index buffer may change + { + if (!mPositions || !mIndices || mNumVertices <= 0 || mNumIndices <= 0) + { + LL_WARNS_ONCE("LLVolume") << "Invalid volume face data for tangent generation: " + << "mPositions=" << (void*)mPositions + << ", mIndices=" << (void*)mIndices + << ", mNumVertices=" << mNumVertices + << ", mNumIndices=" << mNumIndices << LL_ENDL; + return false; + } + + if (mNumIndices % 3 != 0) + { + LL_WARNS_ONCE("LLVolume") << "Non-triangulated mesh, mNumIndices=" << mNumIndices << LL_ENDL; + return false; + } + + for (S32 i = 0; i < mNumIndices; ++i) + { + if (mIndices[i] >= mNumVertices) + { + LL_WARNS_ONCE("LLVolume") << "Out of bounds index detected: mIndices[" << i << "]=" + << mIndices[i] << " >= mNumVertices=" << mNumVertices << LL_ENDL; + return false; + } + } + + if (mNormalizedScale.mV[0] == 0.0f || mNormalizedScale.mV[1] == 0.0f || mNormalizedScale.mV[2] == 0.0f) + { + LL_WARNS_ONCE("LLVolume") << "Invalid normalized scale: " << mNormalizedScale << LL_ENDL; + return false; + } + + // generate mikkt space tangents before cache optimizing since the index buffer may change // a bit of a hack to do this here, but this function gets called exactly once for the lifetime of a mesh // and is executed on a background thread MikktData data(this); |
