From 6a829d78fa6d1fd1e4a76474e16d00632e74a09a Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Thu, 16 Oct 2025 19:55:24 +0200 Subject: [PATCH] Mesh: Rewrite validation code Rewrite the "mesh is valid" and "validate mesh" functions to be more agnostic of the custom data storage system, align with the changes to topology storage in the last 5 years, be much clearer overall, more reusable. Each check is implemented as a separate pass over the remaining valid geometry in the mesh, producing an IndexMask of the invalid elements it finds. At the cost of some extra iteration over mesh elements, this should make each requirement clearer and make it easier to optimize and reuse each check if needed. The code is roughly twice as fast as it was before. I measured 92ms instead of 200ms for a 1 million vertex cube on a Ryzen 7950X. There's a bit of low hanging fruit for further optimization too. There are now automated tests just for the validation code as well. For now they are very basic but they could be extended in the future. Some non-obvious points: - The new face offsets storage (replacing `MPoly`) upholds more invariants by itself. Previously faces could easily overlap or leave corners unreferenced. That doesn't really happen anymore, but bad offset values are a more "global" problem. - The validation code for the old "MFace" storage was removed. It is just rebuilt when it's needed at runtime anyway, so there isn't much point in validating it. - The versioning code for 2.90.1 was calling the mesh validation code to fix an issue where the extrude manifold tool could generate bad faces. Unfortunately keeping that would mean being unable to remove the old code, so now there's a warning to open and save the file in a previous version instead. - One of the main goals of the new code is better const correctness, and working better with implicit sharing. The code now only requests mutable copies of the mesh data if it has to change. Part of #122398 Pull Request: https://projects.blender.org/blender/blender/pulls/148063 --- source/blender/blenkernel/BKE_mesh.h | 38 - source/blender/blenkernel/BKE_mesh_runtime.hh | 4 - .../blender/blenkernel/intern/mesh_runtime.cc | 68 - .../blenkernel/intern/mesh_validate.cc | 1997 +++++++---------- .../blenloader/intern/versioning_290.cc | 33 +- tests/python/CMakeLists.txt | 5 + tests/python/mesh_validate.py | 113 + 7 files changed, 923 insertions(+), 1335 deletions(-) create mode 100644 tests/python/mesh_validate.py diff --git a/source/blender/blenkernel/BKE_mesh.h b/source/blender/blenkernel/BKE_mesh.h index ce96d301b2f..334a55aad5e 100644 --- a/source/blender/blenkernel/BKE_mesh.h +++ b/source/blender/blenkernel/BKE_mesh.h @@ -449,44 +449,6 @@ bool BKE_mesh_is_valid(Mesh *mesh); */ bool BKE_mesh_validate_material_indices(Mesh *mesh); -/** - * Validate the mesh, \a do_fixes requires \a mesh to be non-null. - * - * \return false if no changes needed to be made. - */ -bool BKE_mesh_validate_arrays(Mesh *mesh, - float (*vert_positions)[3], - unsigned int verts_num, - blender::int2 *edges, - unsigned int edges_num, - MFace *legacy_faces, - unsigned int legacy_faces_num, - const int *corner_verts, - int *corner_edges, - unsigned int corners_num, - const int *face_offsets, - unsigned int faces_num, - MDeformVert *dverts, /* assume totvert length */ - bool do_verbose, - bool do_fixes, - bool *r_change); - -/** - * \returns is_valid. - */ -bool BKE_mesh_validate_all_customdata(CustomData *vert_data, - uint verts_num, - CustomData *edge_data, - uint edges_num, - CustomData *corner_data, - uint corners_num, - CustomData *face_data, - uint faces_num, - bool check_meshmask, - bool do_verbose, - bool do_fixes, - bool *r_change); - void BKE_mesh_strip_loose_faces(Mesh *mesh); /* **** Depsgraph evaluation **** */ diff --git a/source/blender/blenkernel/BKE_mesh_runtime.hh b/source/blender/blenkernel/BKE_mesh_runtime.hh index 58996f736b9..0bdd6c6266b 100644 --- a/source/blender/blenkernel/BKE_mesh_runtime.hh +++ b/source/blender/blenkernel/BKE_mesh_runtime.hh @@ -83,7 +83,3 @@ Mesh *mesh_create_eval_no_deform_render(Depsgraph *depsgraph, const CustomData_MeshMasks *dataMask); } // namespace blender::bke - -#ifndef NDEBUG -bool BKE_mesh_runtime_is_valid(Mesh *mesh_eval); -#endif /* !NDEBUG */ diff --git a/source/blender/blenkernel/intern/mesh_runtime.cc b/source/blender/blenkernel/intern/mesh_runtime.cc index f9ab55f9d0d..2d0f0ff2924 100644 --- a/source/blender/blenkernel/intern/mesh_runtime.cc +++ b/source/blender/blenkernel/intern/mesh_runtime.cc @@ -466,71 +466,3 @@ void BKE_mesh_batch_cache_free(void *batch_cache) } /** \} */ - -/* -------------------------------------------------------------------- */ -/** \name Mesh Runtime Validation - * \{ */ - -#ifndef NDEBUG - -bool BKE_mesh_runtime_is_valid(Mesh *mesh_eval) -{ - const bool do_verbose = true; - const bool do_fixes = false; - - bool is_valid = true; - bool changed = true; - - if (do_verbose) { - printf("MESH: %s\n", mesh_eval->id.name + 2); - } - - MutableSpan positions = mesh_eval->vert_positions_for_write(); - MutableSpan edges = mesh_eval->edges_for_write(); - Span face_offsets = mesh_eval->face_offsets(); - Span corner_verts = mesh_eval->corner_verts(); - MutableSpan corner_edges = mesh_eval->corner_edges_for_write(); - - is_valid &= BKE_mesh_validate_all_customdata( - &mesh_eval->vert_data, - mesh_eval->verts_num, - &mesh_eval->edge_data, - mesh_eval->edges_num, - &mesh_eval->corner_data, - mesh_eval->corners_num, - &mesh_eval->face_data, - mesh_eval->faces_num, - false, /* setting mask here isn't useful, gives false positives */ - do_verbose, - do_fixes, - &changed); - - MDeformVert *dverts = static_cast( - CustomData_get_layer_for_write(&mesh_eval->vert_data, CD_MDEFORMVERT, mesh_eval->verts_num)); - is_valid &= BKE_mesh_validate_arrays( - mesh_eval, - reinterpret_cast(positions.data()), - positions.size(), - edges.data(), - edges.size(), - static_cast(CustomData_get_layer_for_write( - &mesh_eval->fdata_legacy, CD_MFACE, mesh_eval->totface_legacy)), - mesh_eval->totface_legacy, - corner_verts.data(), - corner_edges.data(), - corner_verts.size(), - face_offsets.data(), - mesh_eval->faces_num, - dverts, - do_verbose, - do_fixes, - &changed); - - BLI_assert(changed == false); - - return is_valid; -} - -#endif /* !NDEBUG */ - -/** \} */ diff --git a/source/blender/blenkernel/intern/mesh_validate.cc b/source/blender/blenkernel/intern/mesh_validate.cc index 58a2d7dac08..c5f7264f22e 100644 --- a/source/blender/blenkernel/intern/mesh_validate.cc +++ b/source/blender/blenkernel/intern/mesh_validate.cc @@ -8,1272 +8,861 @@ #include #include -#include +#include +#include "BKE_attribute_legacy_convert.hh" +#include "BKE_attribute_math.hh" #include "CLG_log.h" #include "DNA_mesh_types.h" #include "DNA_meshdata_types.h" -#include "BLI_map.hh" -#include "BLI_math_base.h" -#include "BLI_math_vector.h" +#include "BLI_array_utils.hh" +#include "BLI_enumerable_thread_specific.hh" +#include "BLI_index_ranges_builder.hh" #include "BLI_ordered_edge.hh" -#include "BLI_sort.hh" -#include "BLI_sys_types.h" -#include "BLI_utildefines.h" #include "BKE_attribute.hh" #include "BKE_customdata.hh" -#include "BKE_deform.hh" #include "BKE_mesh.hh" -#include "BKE_mesh_runtime.hh" #include "DEG_depsgraph.hh" -#include "MEM_guardedalloc.h" - -using blender::float3; -using blender::MutableSpan; -using blender::Span; - -/* corner v/e are unsigned, so using max uint_32 value as invalid marker... */ -#define INVALID_CORNER_EDGE_MARKER 4294967295u - static CLG_LogRef LOG = {"geom.mesh"}; -void strip_loose_faces_corners(Mesh *mesh, blender::BitSpan faces_to_remove); -void mesh_strip_edges(Mesh *mesh); +namespace blender::bke { -/* -------------------------------------------------------------------- */ -/** \name Internal functions - * \{ */ +/* Helper class to collect error messages in parallel. */ +class ErrorMessages { + Mutex mutex_; + bool verbose_; -union EdgeUUID { - uint32_t verts[2]; - int64_t edval; -}; + public: + Vector messages; -struct SortFaceLegacy { - EdgeUUID es[4]; - uint index; -}; - -/* Used to detect faces using exactly the same vertices. */ -/* Used to detect corners used by no (disjoint) or more than one (intersect) faces. */ -struct SortFace { - int *verts = nullptr; - int numverts = 0; - int corner_start = 0; - uint index = 0; - bool invalid = false; -}; - -static void edge_store_assign(uint32_t verts[2], const uint32_t v1, const uint32_t v2) -{ - if (v1 < v2) { - verts[0] = v1; - verts[1] = v2; - } - else { - verts[0] = v2; - verts[1] = v1; - } -} - -static void edge_store_from_mface_quad(EdgeUUID es[4], const MFace *mf) -{ - edge_store_assign(es[0].verts, mf->v1, mf->v2); - edge_store_assign(es[1].verts, mf->v2, mf->v3); - edge_store_assign(es[2].verts, mf->v3, mf->v4); - edge_store_assign(es[3].verts, mf->v4, mf->v1); -} - -static void edge_store_from_mface_tri(EdgeUUID es[4], const MFace *mf) -{ - edge_store_assign(es[0].verts, mf->v1, mf->v2); - edge_store_assign(es[1].verts, mf->v2, mf->v3); - edge_store_assign(es[2].verts, mf->v3, mf->v1); - es[3].verts[0] = es[3].verts[1] = UINT_MAX; -} - -static bool search_legacy_face_cmp(const SortFaceLegacy &sfa, const SortFaceLegacy &sfb) -{ - if (sfa.es[0].edval != sfb.es[0].edval) { - return sfa.es[0].edval < sfb.es[0].edval; - } - if (sfa.es[1].edval != sfb.es[1].edval) { - return sfa.es[1].edval < sfb.es[1].edval; - } - if (sfa.es[2].edval != sfb.es[2].edval) { - return sfa.es[2].edval < sfb.es[2].edval; - } - return sfa.es[3].edval < sfb.es[3].edval; -} - -static bool search_face_cmp(const SortFace &sp1, const SortFace &sp2) -{ - /* Reject all invalid faces at end of list! */ - if (sp1.invalid || sp2.invalid) { - return sp1.invalid < sp2.invalid; - } - /* Else, sort on first non-equal verts (remember verts of valid faces are sorted). */ - const int max_idx = std::min(sp1.numverts, sp2.numverts); - for (int idx = 0; idx < max_idx; idx++) { - const int v1_i = sp1.verts[idx]; - const int v2_i = sp2.verts[idx]; - if (v1_i != v2_i) { - return v1_i < v2_i; - } - } - return sp1.numverts < sp2.numverts; -} - -static bool search_face_corner_cmp(const SortFace &sp1, const SortFace &sp2) -{ - /* Reject all invalid faces at end of list! */ - if (sp1.invalid || sp2.invalid) { - return sp1.invalid < sp2.invalid; - } - /* Else, sort on corner start. */ - return sp1.corner_start < sp2.corner_start; -} - -/** \} */ - -/* -------------------------------------------------------------------- */ -/** \name Mesh Validation - * \{ */ - -#define PRINT_MSG(...) \ - if (do_verbose) { \ - CLOG_INFO(&LOG, __VA_ARGS__); \ - } \ - ((void)0) - -#define PRINT_ERR(...) \ - do { \ - is_valid = false; \ - if (do_verbose) { \ - CLOG_ERROR(&LOG, __VA_ARGS__); \ - } \ - } while (0) - -/* NOLINTNEXTLINE: readability-function-size */ -bool BKE_mesh_validate_arrays(Mesh *mesh, - float (*vert_positions)[3], - uint verts_num, - blender::int2 *edges, - uint edges_num, - MFace *legacy_faces, - uint legacy_faces_num, - const int *corner_verts, - int *corner_edges, - uint corners_num, - const int *face_offsets, - uint faces_num, - MDeformVert *dverts, /* assume verts_num length */ - const bool do_verbose, - const bool do_fixes, - bool *r_changed) -{ - using namespace blender; - using namespace blender::bke; -#define REMOVE_EDGE_TAG(_me) \ - { \ - _me[0] = _me[1]; \ - free_flag.edges = do_fixes; \ - } \ - (void)0 -#define IS_REMOVED_EDGE(_me) (_me[0] == _me[1]) - -#define REMOVE_CORNER_TAG(corner) \ - { \ - corner_edges[corner] = INVALID_CORNER_EDGE_MARKER; \ - free_flag.face_corners = do_fixes; \ - } \ - (void)0 - blender::BitVector<> faces_to_remove(faces_num); - - blender::bke::AttributeWriter material_indices = - mesh->attributes_for_write().lookup_for_write("material_index"); - blender::MutableVArraySpan material_indices_span(material_indices.varray); - - uint i, j; - int *v; - - bool is_valid = true; - - union { - struct { - int verts : 1; - int verts_weight : 1; - int corners_edge : 1; - }; - int as_flag; - } fix_flag; - - union { - struct { - int edges : 1; - int faces : 1; - /* This regroups corners and faces! */ - int face_corners : 1; - int mselect : 1; - }; - int as_flag; - } free_flag; - - union { - struct { - int edges : 1; - }; - int as_flag; - } recalc_flag; - - Map edge_hash; - edge_hash.reserve(edges_num); - - BLI_assert(!(do_fixes && mesh == nullptr)); - - fix_flag.as_flag = 0; - free_flag.as_flag = 0; - recalc_flag.as_flag = 0; - - PRINT_MSG("verts(%u), edges(%u), corners(%u), faces(%u)", - verts_num, - edges_num, - corners_num, - faces_num); - - if (edges_num == 0 && faces_num != 0) { - PRINT_ERR("\tLogical error, %u faces and 0 edges", faces_num); - recalc_flag.edges = do_fixes; - } - - for (i = 0; i < verts_num; i++) { - for (j = 0; j < 3; j++) { - if (!isfinite(vert_positions[i][j])) { - PRINT_ERR("\tVertex %u: has invalid coordinate", i); - - if (do_fixes) { - zero_v3(vert_positions[i]); - - fix_flag.verts = true; - } - } - } - } - - for (i = 0; i < edges_num; i++) { - blender::int2 &edge = edges[i]; - bool remove = false; - - if (edge[0] == edge[1]) { - PRINT_ERR("\tEdge %u: has matching verts, both %d", i, edge[0]); - remove = do_fixes; - } - if (edge[0] >= verts_num) { - PRINT_ERR("\tEdge %u: v1 index out of range, %d", i, edge[0]); - remove = do_fixes; - } - if (edge[1] >= verts_num) { - PRINT_ERR("\tEdge %u: v2 index out of range, %d", i, edge[1]); - remove = do_fixes; - } - - if ((edge[0] != edge[1]) && edge_hash.contains(edge)) { - PRINT_ERR("\tEdge %u: is a duplicate of %d", i, edge_hash.lookup(edge)); - remove = do_fixes; - } - - if (remove == false) { - if (edge[0] != edge[1]) { - edge_hash.add(edge, i); - } - } - else { - REMOVE_EDGE_TAG(edge); - } - } - - if (legacy_faces && !face_offsets) { -#define REMOVE_FACE_TAG(_mf) \ - { \ - _mf->v3 = 0; \ - free_flag.faces = do_fixes; \ - } \ - (void)0 -#define CHECK_FACE_VERT_INDEX(a, b) \ - if (mf->a == mf->b) { \ - PRINT_ERR(" face %u: verts invalid, " STRINGIFY(a) "/" STRINGIFY(b) " both %u", i, mf->a); \ - remove = do_fixes; \ - } \ - (void)0 -#define CHECK_FACE_EDGE(a, b) \ - if (!edge_hash.contains({mf->a, mf->b})) { \ - PRINT_ERR(" face %u: edge " STRINGIFY(a) "/" STRINGIFY(b) " (%u,%u) is missing edge data", \ - i, \ - mf->a, \ - mf->b); \ - recalc_flag.edges = do_fixes; \ - } \ - (void)0 - - MFace *mf; - const MFace *mf_prev; - - Array sort_faces(legacy_faces_num); - SortFaceLegacy *sf; - SortFaceLegacy *sf_prev; - uint totsortface = 0; - - PRINT_ERR("No faces, only tessellated Faces"); - - for (i = 0, mf = legacy_faces, sf = sort_faces.data(); i < legacy_faces_num; i++, mf++) { - bool remove = false; - int fidx; - uint fv[4]; - - fidx = mf->v4 ? 3 : 2; - do { - fv[fidx] = *(&(mf->v1) + fidx); - if (fv[fidx] >= verts_num) { - PRINT_ERR("\tFace %u: 'v%d' index out of range, %u", i, fidx + 1, fv[fidx]); - remove = do_fixes; - } - } while (fidx--); - - if (remove == false) { - if (mf->v4) { - CHECK_FACE_VERT_INDEX(v1, v2); - CHECK_FACE_VERT_INDEX(v1, v3); - CHECK_FACE_VERT_INDEX(v1, v4); - - CHECK_FACE_VERT_INDEX(v2, v3); - CHECK_FACE_VERT_INDEX(v2, v4); - - CHECK_FACE_VERT_INDEX(v3, v4); - } - else { - CHECK_FACE_VERT_INDEX(v1, v2); - CHECK_FACE_VERT_INDEX(v1, v3); - - CHECK_FACE_VERT_INDEX(v2, v3); - } - - if (remove == false) { - if (edges_num) { - if (mf->v4) { - CHECK_FACE_EDGE(v1, v2); - CHECK_FACE_EDGE(v2, v3); - CHECK_FACE_EDGE(v3, v4); - CHECK_FACE_EDGE(v4, v1); - } - else { - CHECK_FACE_EDGE(v1, v2); - CHECK_FACE_EDGE(v2, v3); - CHECK_FACE_EDGE(v3, v1); - } - } - - sf->index = i; - - if (mf->v4) { - edge_store_from_mface_quad(sf->es, mf); - std::sort(sf->es, sf->es + 4, [](const EdgeUUID &a, const EdgeUUID &b) { - return a.edval < b.edval; - }); - } - else { - edge_store_from_mface_tri(sf->es, mf); - std::sort(sf->es, sf->es + 3, [](const EdgeUUID &a, const EdgeUUID &b) { - return a.edval < b.edval; - }); - } - - totsortface++; - sf++; - } - } - - if (remove) { - REMOVE_FACE_TAG(mf); - } - } - - blender::parallel_sort(sort_faces.begin(), sort_faces.end(), search_legacy_face_cmp); - - sf = sort_faces.data(); - sf_prev = sf; - sf++; - - for (i = 1; i < totsortface; i++, sf++) { - bool remove = false; - - /* on a valid mesh, code below will never run */ - if (memcmp(sf->es, sf_prev->es, sizeof(sf_prev->es)) == 0) { - mf = legacy_faces + sf->index; - - if (do_verbose) { - mf_prev = legacy_faces + sf_prev->index; - - if (mf->v4) { - PRINT_ERR("\tFace %u & %u: are duplicates (%u,%u,%u,%u) (%u,%u,%u,%u)", - sf->index, - sf_prev->index, - mf->v1, - mf->v2, - mf->v3, - mf->v4, - mf_prev->v1, - mf_prev->v2, - mf_prev->v3, - mf_prev->v4); - } - else { - PRINT_ERR("\tFace %u & %u: are duplicates (%u,%u,%u) (%u,%u,%u)", - sf->index, - sf_prev->index, - mf->v1, - mf->v2, - mf->v3, - mf_prev->v1, - mf_prev->v2, - mf_prev->v3); - } - } - - remove = do_fixes; - } - else { - sf_prev = sf; - } - - if (remove) { - REMOVE_FACE_TAG(mf); - } - } - -#undef REMOVE_FACE_TAG -#undef CHECK_FACE_VERT_INDEX -#undef CHECK_FACE_EDGE - } - - /* Checking corners and faces is a bit tricky, as they are quite intricate... - * - * Faces must have: - * - a valid corner_start value. - * - a valid corners_num value (>= 3 and corner_start+corners_num < mesh.corners_num). - * - * corners must have: - * - a valid v value. - * - a valid e value (corresponding to the edge it defines with the next corner in face). - * - * Also, corners not used by faces can be discarded. - * And "intersecting" corners (i.e. corners used by more than one face) are invalid, - * so be sure to leave at most one face per corner! - */ + ErrorMessages(const bool verbose) : verbose_(verbose) {} + ~ErrorMessages() { - BitVector<> vert_tag(mesh->verts_num); - Array sort_faces(faces_num); - Array sort_face_verts(faces_num == 0 ? 0 : face_offsets[faces_num]); - int64_t sort_face_verts_offset = 0; + std::sort(messages.begin(), messages.end()); + for (const std::string &message : messages) { + CLOG_ERROR(&LOG, "%s", message.c_str()); + } + } - for (const int64_t i : blender::IndexRange(faces_num)) { - SortFace *sp = &sort_faces[i]; - const int face_start = face_offsets[i]; - const int face_size = face_offsets[i + 1] - face_start; - sp->index = i; + void add(std::string message) + { + if (!verbose_) { + return; + } + std::lock_guard lock(mutex_); + this->messages.append(std::move(message)); + } - /* Material index, isolated from other tests here. While large indices are clamped, - * negative indices aren't supported by drawing, exporters etc. - * To check the indices are in range, use #BKE_mesh_validate_material_indices */ - if (material_indices && material_indices_span[i] < 0) { - PRINT_ERR("\tFace %u has invalid material (%d)", sp->index, material_indices_span[i]); - if (do_fixes) { - material_indices_span[i] = 0; + template void add(fmt::format_string fmt, T &&...args) + { + if (!verbose_) { + return; + } + this->add(fmt::format(fmt, args...)); + } +}; + +template +static void print_error_with_indices(const IndexMask &mask, + fmt::format_string fmt, + T &&...args) +{ + fmt::memory_buffer buffer; + fmt::appender dst(buffer); + fmt::format_to(dst, fmt, std::forward(args)...); + fmt::format_to(dst, " at indices: "); + mask.foreach_index([&](const int index, const int pos) { + if (pos != 0) { + fmt::format_to(dst, ", "); + } + fmt::format_to(dst, "{}", index); + }); + CLOG_ERROR(&LOG, "%s", fmt::to_string(buffer).c_str()); +} + +static IndexMask find_edges_bad_verts(const Mesh &mesh, + IndexMaskMemory &memory, + const bool verbose) +{ + const IndexRange verts_range(mesh.verts_num); + const Span edges = mesh.edges(); + + ErrorMessages errors(verbose); + return IndexMask::from_predicate( + edges.index_range(), GrainSize(4096), memory, [&](const int edge_i) { + const int2 edge = edges[edge_i]; + if (edge[0] == edge[1]) { + errors.add("Edge {} has equal vertex indices {}", edge_i, edge[0]); + return true; } - } + if (!verts_range.contains(edge[0]) || !verts_range.contains(edge[1])) { + errors.add("Edge {} has out of range vertex ({}, {})", edge_i, edge[0], edge[1]); + return true; + } + return false; + }); +} - if (face_start < 0 || face_size < 3) { - /* Invalid corner data. */ - PRINT_ERR("\tFace %u is invalid (corner_start: %d, corners_num: %d)", - sp->index, - face_start, - face_size); - sp->invalid = true; - } - else if (face_start + face_size > corners_num) { - /* Invalid corner data. */ - PRINT_ERR( - "\tFace %u uses corners out of range " - "(corner_start: %d, corner_end: %d, max number of corners: %u)", - sp->index, - face_start, - face_start + face_size - 1, - corners_num - 1); - sp->invalid = true; - } - else { - /* Face itself is valid, for now. */ - int v1, v2; /* v1 is prev corner vert idx, v2 is current corner one. */ - sp->invalid = false; - sp->verts = v = sort_face_verts.data() + sort_face_verts_offset; - sort_face_verts_offset += face_size; - sp->numverts = face_size; - sp->corner_start = face_start; +using EdgeMap = VectorSet, + DefaultEquality, + SimpleVectorSetSlot, + GuardedAllocator>; - /* Ideally we would only have to do that once on all vertices - * before we start checking each face, but several faces can use same vert, - * so we have to ensure here all verts of current face are cleared. */ - for (j = 0; j < face_size; j++) { - const int vert = corner_verts[sp->corner_start + j]; - if (vert < verts_num) { - vert_tag[vert].reset(); +static IndexMask find_edges_duplicates(const Mesh &mesh, + const IndexMask &mask, + IndexMaskMemory &memory, + const bool verbose, + EdgeMap &unique_edges) +{ + const Span edges = mesh.edges(); + BitVector<> duplicate_edges(edges.size()); + ErrorMessages errors(verbose); + mask.foreach_index([&](const int edge_i) { + const int2 edge = edges[edge_i]; + if (!unique_edges.add(edge)) { + errors.add("Edge {} is a duplicate of {}", edge_i, unique_edges.index_of(edge)); + duplicate_edges[edge_i].set(); + } + }); + return IndexMask::from_bits(mask, duplicate_edges, memory); +} + +static IndexMask find_faces_bad_offsets(const Mesh &mesh, + IndexMaskMemory &memory, + const bool verbose) +{ + if (mesh.faces_num == 0) { + return {}; + } + const Span face_offsets = mesh.face_offsets(); + ErrorMessages errors(verbose); + if (face_offsets.last() != mesh.corners_num) { + errors.add("Face offsets last value is {}, expected {}. Considering all faces invalid", + face_offsets.last(), + mesh.corners_num); + return IndexMask(mesh.faces_num); + } + if (face_offsets.first() != 0) { + errors.add("Faces offsets do not start at 0. Considering all faces invalid"); + return IndexMask(mesh.faces_num); + } + return IndexMask::from_predicate( + IndexRange(mesh.faces_num), GrainSize(4096), memory, [&](const int face_i) { + const int face_start = face_offsets[face_i]; + const int face_size = face_offsets[face_i + 1] - face_start; + if (face_size < 3) { + errors.add("Face {} has invalid size {}", face_i, face_size); + return true; + } + return false; + }); +} + +static IndexMask find_faces_bad_verts(const Mesh &mesh, + const IndexMask &mask, + IndexMaskMemory &memory, + const bool verbose) +{ + const IndexRange verts_range(mesh.verts_num); + const OffsetIndices faces(mesh.face_offsets(), offset_indices::NoSortCheck()); + const Span corner_verts = mesh.corner_verts(); + ErrorMessages errors(verbose); + return IndexMask::from_predicate(mask, GrainSize(512), memory, [&](const int face_i) { + const IndexRange face = faces[face_i]; + for (const int vert : corner_verts.slice(face)) { + if (!verts_range.contains(vert)) { + errors.add("Face {} has invalid vertex {}", face_i, vert); + return true; + } + } + return false; + }); +} + +static IndexMask find_faces_duplicate_verts(const Mesh &mesh, + const IndexMask &mask, + IndexMaskMemory &memory, + const bool verbose) +{ + const IndexRange verts_range(mesh.verts_num); + const OffsetIndices faces(mesh.face_offsets(), offset_indices::NoSortCheck()); + const Span corner_verts = mesh.corner_verts(); + ErrorMessages errors(verbose); + return IndexMask::from_predicate(mask, GrainSize(512), memory, [&](const int face_i) { + Set set; + const IndexRange face = faces[face_i]; + for (const int vert : corner_verts.slice(face)) { + if (!set.add(vert)) { + errors.add("Face {} has duplicate vertex {}", face_i, vert); + return true; + } + } + return false; + }); +} + +static IndexMask find_faces_missing_edges(const Mesh &mesh, + const IndexMask &mask, + const EdgeMap &unique_edges, + IndexMaskMemory &memory, + const bool verbose) +{ + const IndexRange edges_range(mesh.edges_num); + const OffsetIndices faces(mesh.face_offsets(), offset_indices::NoSortCheck()); + const Span corner_verts = mesh.corner_verts(); + + ErrorMessages errors(verbose); + return IndexMask::from_predicate(mask, GrainSize(1024), memory, [&](const int face_i) { + const IndexRange face = faces[face_i]; + for (const int corner : face) { + const int corner_next = mesh::face_corner_next(face, corner); + const OrderedEdge actual_edge(corner_verts[corner], corner_verts[corner_next]); + if (!unique_edges.contains(actual_edge)) { + errors.add( + "Face {} has missing edge ({}, {})", face_i, actual_edge.v_low, actual_edge.v_high); + return true; + } + } + return false; + }); +} + +static IndexMask find_faces_bad_edges(const Mesh &mesh, + const IndexMask &mask, + const EdgeMap &unique_edges, + IndexMaskMemory &memory, + const bool verbose, + Vector>> &r_corner_edge_fixes) +{ + const IndexRange edges_range(mesh.edges_num); + const Span edges = mesh.edges(); + const OffsetIndices faces(mesh.face_offsets(), offset_indices::NoSortCheck()); + const Span corner_verts = mesh.corner_verts(); + const Span corner_edges = mesh.corner_edges(); + + ErrorMessages errors(verbose); + threading::EnumerableThreadSpecific>> all_replacements; + IndexMask faces_bad_edges = IndexMask::from_batch_predicate( + mask, + GrainSize(4096), + memory, + [&](const IndexMaskSegment universe_segment, IndexRangesBuilder &builder) { + Vector> &replacements = all_replacements.local(); + for (const int face_i : universe_segment) { + const IndexRange face = faces[face_i]; + + bool has_invalid_edge = false; + for (const int corner : face) { + const int corner_next = mesh::face_corner_next(face, corner); + const OrderedEdge actual_edge(corner_verts[corner], corner_verts[corner_next]); + const int actual_edge_index = unique_edges.index_of(actual_edge); + const int edge_index = corner_edges[corner]; + if (!edges_range.contains(edge_index)) { + errors.add("Corner {} has out of range edge index {}. Expected {}", + corner, + edge_index, + actual_edge_index); + replacements.append({corner, actual_edge_index}); + has_invalid_edge = true; + continue; + } + if (OrderedEdge(edges[edge_index]) != actual_edge) { + errors.add("Corner {} has incorrect edge index {}. Expected {}", + corner, + edge_index, + actual_edge_index); + replacements.append({corner, actual_edge_index}); + has_invalid_edge = true; + continue; + } + } + if (has_invalid_edge) { + builder.add(face_i); } } + return universe_segment.offset(); + }); - /* Test all face's corners' vert idx. */ - for (j = 0; j < face_size; j++, v++) { - const int vert = corner_verts[sp->corner_start + j]; - if (vert >= verts_num) { - /* Invalid vert idx. */ - PRINT_ERR("\tCorner %u has invalid vert reference (%d)", sp->corner_start + j, vert); - sp->invalid = true; - } - else if (vert_tag[vert].test()) { - PRINT_ERR("\tFace %u has duplicated vert reference at corner (%u)", uint(i), j); - sp->invalid = true; - } - else { - vert_tag[vert].set(); - } - *v = vert; - } + for (Vector> &replacements : all_replacements) { + if (!replacements.is_empty()) { + r_corner_edge_fixes.append(std::move(replacements)); + } + } - if (sp->invalid) { - sp++; + return faces_bad_edges; +} + +static IndexMask find_duplicate_faces(const Mesh &mesh, + const IndexMask &mask, + IndexMaskMemory &memory, + const bool verbose) +{ + const OffsetIndices faces(mesh.face_offsets(), offset_indices::NoSortCheck()); + const Span corner_verts = mesh.corner_verts(); + + Array sorted_corner_verts(mesh.corners_num); + mask.foreach_index(GrainSize(1024), [&](const int face_i) { + const IndexRange face = faces[face_i]; + MutableSpan sorted_face_verts = sorted_corner_verts.as_mutable_span().slice(face); + sorted_face_verts.copy_from(corner_verts.slice(face)); + std::sort(sorted_face_verts.begin(), sorted_face_verts.end()); + }); + + using FaceMap = VectorSet, + 32, + DefaultProbingStrategy, + DefaultHash>, + DefaultEquality>, + SimpleVectorSetSlot, int>>; + + FaceMap face_hash; + face_hash.reserve(mesh.faces_num); + + BitVector<> duplicate_faces(mesh.faces_num, false); + ErrorMessages errors(verbose); + mask.foreach_index([&](int face_i) { + const IndexRange face = faces[face_i]; + const Span face_verts = sorted_corner_verts.as_span().slice(face); + if (!face_hash.add(face_verts)) { + errors.add("Face {} is a duplicate of {}", face_i, face_hash.index_of(face_verts)); + duplicate_faces[face_i].set(); + } + }); + + return IndexMask::from_bits(duplicate_faces, memory); +} + +static void remove_invalid_faces(Mesh &mesh, const IndexMask &valid_faces) +{ + const int valid_faces_num = valid_faces.size(); + const OffsetIndices old_faces(mesh.face_offsets(), offset_indices::NoSortCheck()); + + Vector new_face_offsets(valid_faces_num + 1); + const OffsetIndices new_faces = offset_indices::gather_selected_offsets( + old_faces, valid_faces, new_face_offsets); + + for (CustomDataLayer &layer : MutableSpan(mesh.face_data.layers, mesh.face_data.totlayer)) { + const eCustomDataType cd_type = eCustomDataType(layer.type); + if (CD_TYPE_AS_MASK(cd_type) & CD_MASK_PROP_ALL) { + const CPPType &type = *bke::custom_data_type_to_cpp_type(cd_type); + const GSpan src(type, layer.data, mesh.faces_num); + + void *dst_data = MEM_malloc_arrayN(valid_faces_num, type.size, __func__); + GMutableSpan dst(type, dst_data, valid_faces_num); + + array_utils::gather(src, valid_faces, dst); + + layer.sharing_info->remove_user_and_delete_if_last(); + layer.data = dst_data; + layer.sharing_info = implicit_sharing::info_for_mem_free(dst_data); + } + else if (cd_type == CD_ORIGINDEX) { + const Span src(static_cast(layer.data), mesh.edges_num); + + int *dst_data = MEM_malloc_arrayN(valid_faces_num, __func__); + MutableSpan dst(dst_data, valid_faces_num); + + array_utils::gather(src, valid_faces, dst); + + layer.sharing_info->remove_user_and_delete_if_last(); + layer.data = dst_data; + layer.sharing_info = implicit_sharing::info_for_mem_free(dst_data); + } + } + + for (CustomDataLayer &layer : MutableSpan(mesh.corner_data.layers, mesh.corner_data.totlayer)) { + const eCustomDataType cd_type = eCustomDataType(layer.type); + if (CD_TYPE_AS_MASK(cd_type) & CD_MASK_PROP_ALL) { + const CPPType &type = *bke::custom_data_type_to_cpp_type(cd_type); + const GSpan src(type, layer.data, mesh.corners_num); + + void *dst_data = MEM_malloc_arrayN(new_faces.total_size(), type.size, __func__); + GMutableSpan dst(type, dst_data, new_faces.total_size()); + + bke::attribute_math::gather_group_to_group(old_faces, new_faces, valid_faces, src, dst); + + layer.sharing_info->remove_user_and_delete_if_last(); + layer.data = dst_data; + layer.sharing_info = implicit_sharing::info_for_mem_free(dst_data); + } + else if (ELEM(cd_type, + CD_NORMAL, + CD_ORIGINDEX, + CD_MDISPS, + CD_GRID_PAINT_MASK, + CD_ORIGSPACE_MLOOP)) + { + const size_t elem_size = CustomData_sizeof(cd_type); + const void *src = layer.data; + + void *dst = MEM_malloc_arrayN(new_faces.total_size(), elem_size, __func__); + + valid_faces.foreach_index(GrainSize(512), [&](const int64_t src_i, const int64_t dst_i) { + CustomData_copy_elements(cd_type, + POINTER_OFFSET(src, elem_size * old_faces[src_i].start()), + POINTER_OFFSET(dst, elem_size * new_faces[dst_i].start()), + new_faces[dst_i].size()); + }); + + layer.sharing_info->remove_user_and_delete_if_last(); + layer.data = dst; + layer.sharing_info = implicit_sharing::info_for_mem_free(dst); + } + } + + mesh.faces_num = new_faces.size(); + mesh.corners_num = new_faces.total_size(); + mesh.face_offset_indices = new_face_offsets.release().data; + mesh.runtime->face_offsets_sharing_info->remove_user_and_delete_if_last(); + mesh.runtime->face_offsets_sharing_info = implicit_sharing::info_for_mem_free( + mesh.face_offset_indices); + + mesh.tag_topology_changed(); +} + +static void remove_invalid_edges(Mesh &mesh, const IndexMask &valid_edges) +{ + const int valid_edges_num = valid_edges.size(); + for (CustomDataLayer &layer : MutableSpan(mesh.edge_data.layers, mesh.edge_data.totlayer)) { + const eCustomDataType cd_type = eCustomDataType(layer.type); + if (CD_TYPE_AS_MASK(cd_type) & CD_MASK_PROP_ALL) { + const CPPType &type = *bke::custom_data_type_to_cpp_type(cd_type); + const GSpan src(type, layer.data, mesh.edges_num); + + void *dst_data = MEM_malloc_arrayN(valid_edges_num, type.size, __func__); + GMutableSpan dst(type, dst_data, valid_edges_num); + + array_utils::gather(src, valid_edges, dst); + + layer.sharing_info->remove_user_and_delete_if_last(); + layer.data = dst_data; + layer.sharing_info = implicit_sharing::info_for_mem_free(dst_data); + } + else if (cd_type == CD_ORIGINDEX) { + const Span src(static_cast(layer.data), mesh.edges_num); + + int *dst_data = MEM_malloc_arrayN(valid_edges_num, __func__); + MutableSpan dst(dst_data, valid_edges_num); + + array_utils::gather(src, valid_edges, dst); + + layer.sharing_info->remove_user_and_delete_if_last(); + layer.data = dst_data; + layer.sharing_info = implicit_sharing::info_for_mem_free(dst_data); + } + } + + mesh.edges_num = valid_edges.size(); + + Array all_edges_to_valid_edges(mesh.edges_num); + index_mask::build_reverse_map(valid_edges, all_edges_to_valid_edges.as_mutable_span()); + MutableSpan corner_edges = mesh.corner_edges_for_write(); + threading::parallel_for(corner_edges.index_range(), 4096, [&](const IndexRange range) { + for (const int i : range) { + corner_edges[i] = all_edges_to_valid_edges[corner_edges[i]]; + } + }); + + mesh.tag_topology_changed(); +} + +static bool validate_vertex_groups(const Mesh &mesh, const bool verbose, Mesh *mesh_mut) +{ + const Span dverts = mesh.deform_verts(); + if (dverts.is_empty()) { + return true; + } + Mutex mutex; + Vector>> errors; + Vector>> replacements; + threading::parallel_for(dverts.index_range(), 2048, [&](const IndexRange range) { + for (const int vert : range) { + const MDeformVert &dvert = dverts[vert]; + + Vector errors; + bool invalid = false; + Vector fixed_weights; + for (const MDeformWeight &dw : Span(dvert.dw, dvert.totweight)) { + const uint def_nr = dw.def_nr; + if (dw.def_nr > INT_MAX) { + invalid = true; + if (verbose) { + std::lock_guard lock(mutex); + errors.append(fmt::format("Vertex {} has invalid deform group {}", vert, def_nr)); + } continue; } - - /* Test all face's corners. */ - for (j = 0; j < face_size; j++) { - const int corner = sp->corner_start + j; - const int vert = corner_verts[corner]; - const int edge_i = corner_edges[corner]; - v1 = vert; - v2 = corner_verts[sp->corner_start + (j + 1) % face_size]; - if (!edge_hash.contains({v1, v2})) { - /* Edge not existing. */ - PRINT_ERR("\tFace %u needs missing edge (%d, %d)", sp->index, v1, v2); - if (do_fixes) { - recalc_flag.edges = true; - } - else { - sp->invalid = true; - } + if (!std::isfinite(dw.weight)) { + invalid = true; + if (verbose) { + std::lock_guard lock(mutex); + errors.append(fmt::format( + "Vertex {} deform group {} has invalid weight {}", vert, def_nr, dw.weight)); } - else if (edge_i >= edges_num) { - /* Invalid edge idx. - * We already know from previous text that a valid edge exists, use it (if allowed)! */ - if (do_fixes) { - int prev_e = edge_i; - corner_edges[corner] = edge_hash.lookup({v1, v2}); - fix_flag.corners_edge = true; - PRINT_ERR("\tCorner %d has invalid edge reference (%d), fixed using edge %d", - corner, - prev_e, - corner_edges[corner]); - } - else { - PRINT_ERR("\tCorner %d has invalid edge reference (%d)", corner, edge_i); - sp->invalid = true; - } - } - else { - const blender::int2 &edge = edges[edge_i]; - if (IS_REMOVED_EDGE(edge) || - !((edge[0] == v1 && edge[1] == v2) || (edge[0] == v2 && edge[1] == v1))) - { - /* The pointed edge is invalid (tagged as removed, or vert idx mismatch), - * and we already know from previous test that a valid one exists, - * use it (if allowed)! */ - if (do_fixes) { - int prev_e = edge_i; - corner_edges[corner] = edge_hash.lookup({v1, v2}); - fix_flag.corners_edge = true; - PRINT_ERR( - "\tFace %u has invalid edge reference (%d, is_removed: %d), fixed using edge " - "%d", - sp->index, - prev_e, - IS_REMOVED_EDGE(edge), - corner_edges[corner]); - } - else { - PRINT_ERR("\tFace %u has invalid edge reference (%d)", sp->index, edge_i); - sp->invalid = true; - } - } + if (mesh_mut) { + fixed_weights.append({def_nr, 0.0f}); } + continue; } - - if (!sp->invalid) { - /* Needed for checking faces using same verts below. */ - std::sort(sp->verts, sp->verts + sp->numverts); + if (dw.weight < 0.0f || dw.weight > 1.0f) { + invalid = true; + if (verbose) { + std::lock_guard lock(mutex); + errors.append(fmt::format( + "Vertex {} deform group {} has invalid weight {}", vert, def_nr, dw.weight)); + } + if (mesh_mut) { + fixed_weights.append({def_nr, std::clamp(dw.weight, 0.0f, 1.0f)}); + } + continue; + } + if (mesh_mut) { + fixed_weights.append(dw); } } - sp++; - } - BLI_assert(sort_face_verts_offset <= sort_face_verts.size()); - vert_tag.clear_and_shrink(); - - /* Second check pass, testing faces using the same verts. */ - blender::parallel_sort(sort_faces.begin(), sort_faces.end(), search_face_cmp); - SortFace *sp, *prev_sp; - sp = prev_sp = sort_faces.data(); - sp++; - - for (i = 1; i < faces_num; i++, sp++) { - int p1_nv = sp->numverts, p2_nv = prev_sp->numverts; - const int *p1_v = sp->verts, *p2_v = prev_sp->verts; - - if (sp->invalid) { - /* Break, because all known invalid faces have been put at the end by the sort above. */ - break; - } - - /* Test same faces. */ - if ((p1_nv == p2_nv) && (memcmp(p1_v, p2_v, p1_nv * sizeof(*p1_v)) == 0)) { - if (do_verbose) { - /* TODO: convert list to string */ - PRINT_ERR("\tFaces %u and %u use same vertices (%d", prev_sp->index, sp->index, *p1_v); - for (j = 1; j < p1_nv; j++) { - PRINT_ERR(", %d", p1_v[j]); - } - PRINT_ERR("), considering face %u as invalid.", sp->index); - } - else { - is_valid = false; - } - sp->invalid = true; - } - else { - prev_sp = sp; + if (invalid) { + std::lock_guard lock(mutex); + replacements.append({vert, std::move(fixed_weights)}); } } - - /* Third check pass, testing corners used by none or more than one face. */ - blender::parallel_sort(sort_faces.begin(), sort_faces.end(), search_face_corner_cmp); - sp = sort_faces.data(); - prev_sp = nullptr; - int prev_end = 0; - for (i = 0; i < faces_num; i++, sp++) { - /* We don't need the verts anymore, and avoid us another corner! */ - sp->verts = nullptr; - - /* Note above prev_sp: in following code, we make sure it is always valid face (or nullptr). - */ - if (sp->invalid) { - if (do_fixes) { - faces_to_remove[sp->index].set(); - free_flag.face_corners = do_fixes; - /* DO NOT REMOVE ITS corners!!! - * As already invalid faces are at the end of the SortFace list, the corners they - * were the only users have already been tagged as "to remove" during previous - * iterations, and we don't want to remove some corners that may be used by - * another valid face! */ - } - } - /* Test corners users. */ - else { - /* Unused corners. */ - if (prev_end < sp->corner_start) { - int corner; - for (j = prev_end, corner = prev_end; j < sp->corner_start; j++, corner++) { - PRINT_ERR("\tCorner %u is unused.", j); - if (do_fixes) { - REMOVE_CORNER_TAG(corner); - } - } - prev_end = sp->corner_start + sp->numverts; - prev_sp = sp; - } - /* Multi-used corners. */ - else if (prev_end > sp->corner_start) { - PRINT_ERR( - "\tFaces %u and %u share corners from %d to %d, considering face %u as invalid.", - prev_sp->index, - sp->index, - sp->corner_start, - prev_end, - sp->index); - if (do_fixes) { - faces_to_remove[sp->index].set(); - free_flag.face_corners = do_fixes; - /* DO NOT REMOVE ITS corners!!! - * They might be used by some next, valid face! - * Just not updating prev_end/prev_sp vars is enough to ensure the corners - * effectively no more needed will be marked as "to be removed"! */ - } - } - else { - prev_end = sp->corner_start + sp->numverts; - prev_sp = sp; - } - } - } - /* We may have some remaining unused corners to get rid of! */ - if (prev_end < corners_num) { - int corner; - for (j = prev_end, corner = prev_end; j < corners_num; j++, corner++) { - PRINT_ERR("\tCorner %u is unused.", j); - if (do_fixes) { - REMOVE_CORNER_TAG(corner); - } + }); + if (replacements.is_empty()) { + return true; + } + if (verbose) { + for (const auto &[vert, errors] : errors) { + for (const std::string &error : errors) { + CLOG_ERROR(&LOG, "%s", error.c_str()); } } } - - /* fix deform verts */ - if (dverts) { - MDeformVert *dv; - for (i = 0, dv = dverts; i < verts_num; i++, dv++) { - MDeformWeight *dw; - - for (j = 0, dw = dv->dw; j < dv->totweight; j++, dw++) { - /* NOTE: greater than max defgroups is accounted for in our code, but not < 0. */ - if (!isfinite(dw->weight)) { - PRINT_ERR("\tVertex deform %u, group %u has weight: %f", i, dw->def_nr, dw->weight); - if (do_fixes) { - dw->weight = 0.0f; - fix_flag.verts_weight = true; - } - } - else if (dw->weight < 0.0f || dw->weight > 1.0f) { - PRINT_ERR("\tVertex deform %u, group %u has weight: %f", i, dw->def_nr, dw->weight); - if (do_fixes) { - CLAMP(dw->weight, 0.0f, 1.0f); - fix_flag.verts_weight = true; - } - } - - /* Not technically incorrect since this is unsigned, however, - * a value over INT_MAX is almost certainly caused by wrapping an uint. */ - if (dw->def_nr >= INT_MAX) { - PRINT_ERR("\tVertex deform %u, has invalid group %u", i, dw->def_nr); - if (do_fixes) { - BKE_defvert_remove_group(dv, dw); - fix_flag.verts_weight = true; - - if (dv->dw) { - /* re-allocated, the new values compensate for stepping - * within the for corner and may not be valid */ - j--; - dw = dv->dw + j; - } - else { /* all freed */ - break; - } - } - } - } + if (mesh_mut) { + MutableSpan dverts = mesh_mut->deform_verts_for_write(); + for (auto &[vert, weights] : replacements) { + MEM_freeN(dverts[vert].dw); + dverts[vert].totweight = weights.size(); + dverts[vert].dw = weights.release().data; } } - -#undef REMOVE_EDGE_TAG -#undef IS_REMOVED_EDGE -#undef REMOVE_CORNER_TAG -#undef REMOVE_FACE_TAG - - if (mesh) { - if (free_flag.faces) { - BKE_mesh_strip_loose_faces(mesh); - } - - if (free_flag.face_corners) { - strip_loose_faces_corners(mesh, faces_to_remove); - } - - if (free_flag.edges) { - mesh_strip_edges(mesh); - } - - if (recalc_flag.edges) { - mesh_calc_edges(*mesh, true, false); - } - } - - if (mesh && mesh->mselect) { - MSelect *msel; - - for (i = 0, msel = mesh->mselect; i < mesh->totselect; i++, msel++) { - int tot_elem = 0; - - if (msel->index < 0) { - PRINT_ERR( - "\tMesh select element %u type %d index is negative, " - "resetting selection stack.\n", - i, - msel->type); - free_flag.mselect = do_fixes; - break; - } - - switch (msel->type) { - case ME_VSEL: - tot_elem = mesh->verts_num; - break; - case ME_ESEL: - tot_elem = mesh->edges_num; - break; - case ME_FSEL: - tot_elem = mesh->faces_num; - break; - } - - if (msel->index > tot_elem) { - PRINT_ERR( - "\tMesh select element %u type %d index %d is larger than data array size %d, " - "resetting selection stack.\n", - i, - msel->type, - msel->index, - tot_elem); - - free_flag.mselect = do_fixes; - break; - } - } - - if (free_flag.mselect) { - MEM_freeN(mesh->mselect); - mesh->mselect = nullptr; - mesh->totselect = 0; - } - } - - material_indices_span.save(); - material_indices.finish(); - - PRINT_MSG("%s: finished\n\n", __func__); - - *r_changed = (fix_flag.as_flag || free_flag.as_flag || recalc_flag.as_flag); - - BLI_assert((*r_changed == false) || (do_fixes == true)); - - return is_valid; + return false; } -static bool mesh_validate_customdata(CustomData *data, - eCustomDataMask mask, - const uint totitems, - const bool do_verbose, - const bool do_fixes, - bool *r_change) +static bool validate_material_indices(const Mesh &mesh, + const bool only_check_negative, + const bool verbose, + Mesh *mesh_mut) { - bool is_valid = true; - bool has_fixes = false; - int i = 0; - - PRINT_MSG("%s: Checking %d CD layers...\n", __func__, data->totlayer); - - /* Set dummy values so the layer-type is always initialized on first access. */ - int layer_num = -1; - int layer_num_type = -1; - - while (i < data->totlayer) { - CustomDataLayer *layer = &data->layers[i]; - const eCustomDataType type = eCustomDataType(layer->type); - bool ok = true; - - /* Count layers when the type changes. */ - if (layer_num_type != type) { - layer_num = CustomData_number_of_layers(data, type); - layer_num_type = type; + const IndexRange materials_range(only_check_negative ? std::numeric_limits::max() : + std::max(int(mesh.totcol), 1)); + const bke::AttributeAccessor attributes = mesh.attributes(); + const VArray material_indices = *attributes.lookup("material_index", bke::AttrDomain::Face); + if (!material_indices) { + return true; + } + if (const std::optional index = material_indices.get_if_single()) { + if (!materials_range.contains(*index)) { + mesh_mut->attributes_for_write().remove("material_index"); + return false; } + return true; + } + const VArraySpan material_indices_span(material_indices); + IndexMaskMemory memory; + const IndexMask invalid_indices = IndexMask::from_predicate( + material_indices.index_range(), GrainSize(4096), memory, [&](const int face) { + return !materials_range.contains(material_indices_span[face]); + }); + if (invalid_indices.is_empty()) { + return true; + } + if (verbose) { + invalid_indices.foreach_index([&](const int face) { + CLOG_ERROR(&LOG, "Face %d has invalid material index %d", face, material_indices_span[face]); + }); + } + if (mesh_mut) { + bke::MutableAttributeAccessor attributes = mesh_mut->attributes_for_write(); + bke::SpanAttributeWriter material_indices = attributes.lookup_for_write_span( + "material_index"); + index_mask::masked_fill(material_indices.span, 0, invalid_indices); + material_indices.finish(); + DEG_id_tag_update(&mesh_mut->id, ID_RECALC_GEOMETRY_ALL_MODES); + } + return false; +} - /* Validate active index, for a time this could be set to a negative value, see: #105860. */ - int *active_index_array[] = { - &layer->active, - &layer->active_rnd, - &layer->active_clone, - &layer->active_mask, - }; - for (int *active_index : Span(active_index_array, ARRAY_SIZE(active_index_array))) { - if (*active_index < 0) { - PRINT_ERR("\tCustomDataLayer type %d has a negative active index (%d)\n", - layer->type, - *active_index); - if (do_fixes) { - *active_index = 0; - has_fixes = true; +static bool validate_selection_history(const Mesh &mesh, const bool verbose, Mesh *mesh_mut) +{ + if (!mesh.mselect) { + return true; + } + const Span mselect(mesh.mselect, mesh.totselect); + IndexMaskMemory memory; + const IndexMask invalid = IndexMask::from_predicate( + mselect.index_range(), GrainSize(4096), memory, [&](const int i) { + if (mselect[i].index < 0) { + return true; } - } - else { - if (*active_index >= layer_num) { - PRINT_ERR("\tCustomDataLayer type %d has an out of bounds active index (%d >= %d)\n", - layer->type, - *active_index, - layer_num); - if (do_fixes) { - BLI_assert(layer_num > 0); - *active_index = layer_num - 1; - has_fixes = true; + const int domain_size = [&]() { + switch (mselect[i].type) { + case ME_VSEL: + return mesh.verts_num; + case ME_ESEL: + return mesh.edges_num; + case ME_FSEL: + return mesh.faces_num; } + return 0; + }(); + if (mselect[i].index >= domain_size) { + return true; } - } - } - - if (CustomData_layertype_is_singleton(type)) { - if (layer_num > 1) { - PRINT_ERR("\tCustomDataLayer type %d is a singleton, found %d in Mesh structure\n", - type, - layer_num); - ok = false; - } - } - - if (mask != 0) { - eCustomDataMask layer_typemask = CD_TYPE_AS_MASK(type); - if ((layer_typemask & mask) == 0) { - PRINT_ERR("\tCustomDataLayer type %d which isn't in the mask\n", type); - ok = false; - } - } - - if (ok == false) { - if (do_fixes) { - CustomData_free_layer(data, type, i); - has_fixes = true; - } - } - - if (ok) { - if (CustomData_layer_validate(layer, totitems, do_fixes)) { - PRINT_ERR("\tCustomDataLayer type %d has some invalid data\n", type); - has_fixes = do_fixes; - } - i++; - } - } - - PRINT_MSG("%s: Finished (is_valid=%d)\n\n", __func__, int(!has_fixes)); - - *r_change = has_fixes; - - return is_valid; -} - -bool BKE_mesh_validate_all_customdata(CustomData *vert_data, - const uint verts_num, - CustomData *edge_data, - const uint edges_num, - CustomData *corner_data, - const uint corners_num, - CustomData *face_data, - const uint faces_num, - const bool check_meshmask, - const bool do_verbose, - const bool do_fixes, - bool *r_change) -{ - bool is_valid = true; - bool is_change_v, is_change_e, is_change_l, is_change_p; - CustomData_MeshMasks mask = {0}; - if (check_meshmask) { - mask = CD_MASK_MESH; - } - - is_valid &= mesh_validate_customdata( - vert_data, mask.vmask, verts_num, do_verbose, do_fixes, &is_change_v); - is_valid &= mesh_validate_customdata( - edge_data, mask.emask, edges_num, do_verbose, do_fixes, &is_change_e); - is_valid &= mesh_validate_customdata( - corner_data, mask.lmask, corners_num, do_verbose, do_fixes, &is_change_l); - is_valid &= mesh_validate_customdata( - face_data, mask.pmask, faces_num, do_verbose, do_fixes, &is_change_p); - - const int uv_maps_num = CustomData_number_of_layers(corner_data, CD_PROP_FLOAT2); - if (uv_maps_num > MAX_MTFACE) { - PRINT_ERR( - "\tMore UV layers than %d allowed, %d last ones won't be available for render, shaders, " - "etc.\n", - MAX_MTFACE, - uv_maps_num - MAX_MTFACE); - } - - /* check indices of clone/stencil */ - if (do_fixes && CustomData_get_clone_layer(corner_data, CD_PROP_FLOAT2) >= uv_maps_num) { - CustomData_set_layer_clone(corner_data, CD_PROP_FLOAT2, 0); - is_change_l = true; - } - if (do_fixes && CustomData_get_stencil_layer(corner_data, CD_PROP_FLOAT2) >= uv_maps_num) { - CustomData_set_layer_stencil(corner_data, CD_PROP_FLOAT2, 0); - is_change_l = true; - } - - *r_change = (is_change_v || is_change_e || is_change_l || is_change_p); - - return is_valid; -} - -bool BKE_mesh_validate(Mesh *mesh, const bool do_verbose, const bool cddata_check_mask) -{ - bool changed; - - if (do_verbose) { - CLOG_INFO(&LOG, "Validating Mesh: %s", mesh->id.name + 2); - } - - BKE_mesh_validate_all_customdata(&mesh->vert_data, - mesh->verts_num, - &mesh->edge_data, - mesh->edges_num, - &mesh->corner_data, - mesh->corners_num, - &mesh->face_data, - mesh->faces_num, - cddata_check_mask, - do_verbose, - true, - &changed); - MutableSpan positions = mesh->vert_positions_for_write(); - MutableSpan edges = mesh->edges_for_write(); - Span face_offsets = mesh->face_offsets(); - Span corner_verts = mesh->corner_verts(); - MutableSpan corner_edges = mesh->corner_edges_for_write(); - - MDeformVert *dverts = static_cast( - CustomData_get_layer_for_write(&mesh->vert_data, CD_MDEFORMVERT, mesh->verts_num)); - BKE_mesh_validate_arrays( - mesh, - reinterpret_cast(positions.data()), - positions.size(), - edges.data(), - edges.size(), - (MFace *)CustomData_get_layer_for_write(&mesh->fdata_legacy, CD_MFACE, mesh->totface_legacy), - mesh->totface_legacy, - corner_verts.data(), - corner_edges.data(), - corner_verts.size(), - face_offsets.data(), - mesh->faces_num, - dverts, - do_verbose, - true, - &changed); - - if (changed) { - BKE_mesh_runtime_clear_cache(mesh); - DEG_id_tag_update(&mesh->id, ID_RECALC_GEOMETRY_ALL_MODES); + return false; + }); + if (invalid.is_empty()) { return true; } + if (verbose) { + invalid.foreach_index([&](const int i) { + CLOG_ERROR(&LOG, + "Selection element (%d, %d) has invalid index, must not be negative", + mselect[i].type, + mselect[i].index); + }); + } + if (mesh_mut) { + MEM_SAFE_FREE(mesh_mut->mselect); + } + return false; } +static IndexMask get_invalid_float_mask(const Span values, + const int floats_per_item, + IndexMaskMemory &memory) +{ + if (floats_per_item == 0) { + return {}; + } + const int num_items = values.size() / floats_per_item; + + return IndexMask::from_predicate( + IndexRange(num_items), GrainSize(4096), memory, [&](const int64_t index) { + const Span item_floats = values.slice(index * floats_per_item, floats_per_item); + return std::any_of(item_floats.begin(), item_floats.end(), [](const float value) { + return !std::isfinite(value); + }); + }); +} + +static void validate_float_attribute(const bke::AttributeIter &iter, + const int floats_per_item, + const bool verbose, + bool &all_attributes_valid, + Mesh *mesh_mut) +{ + const GVArraySpan span = *iter.get(); + const Span float_span(static_cast(span.data()), + span.size_in_bytes() / sizeof(float)); + IndexMaskMemory memory; + const IndexMask invalid = get_invalid_float_mask(float_span, floats_per_item, memory); + if (invalid.is_empty()) { + return; + } + if (verbose) { + print_error_with_indices(invalid, "Attribute {} has invalid values", iter.name); + } + all_attributes_valid = false; + if (mesh_mut) { + bke::MutableAttributeAccessor attributes = mesh_mut->attributes_for_write(); + bke::GSpanAttributeWriter attr = attributes.lookup_for_write_span(iter.name); + const CPPType &type = attr.span.type(); + type.fill_assign_indices(type.default_value(), attr.span.data(), invalid); + attr.finish(); + } +} + +static void validate_bool_attribute(const bke::AttributeIter &iter, + const bool verbose, + bool &all_attributes_valid, + Mesh *mesh_mut) +{ + const VArraySpan span = *iter.get(); + const Span int_span = span.cast(); + IndexMaskMemory memory; + const IndexMask invalid = IndexMask::from_predicate( + int_span.index_range(), GrainSize(4096), memory, [&](const int i) { + return !ELEM(int_span[i], 0, 1); + }); + if (invalid.is_empty()) { + return; + } + if (verbose) { + print_error_with_indices(invalid, "Attribute {} has invalid values", iter.name); + } + all_attributes_valid = false; + if (mesh_mut) { + bke::MutableAttributeAccessor attributes = mesh_mut->attributes_for_write(); + bke::SpanAttributeWriter attr = attributes.lookup_for_write_span(iter.name); + index_mask::masked_fill(attr.span, true, invalid); + attr.finish(); + } +} + +static bool validate_generic_attributes(const Mesh &mesh, const bool verbose, Mesh *mesh_mut) +{ + bool all_attributes_valid = true; + mesh.attributes().foreach_attribute([&](const bke::AttributeIter &iter) { + switch (iter.data_type) { + case AttrType::Bool: + validate_bool_attribute(iter, verbose, all_attributes_valid, mesh_mut); + break; + case AttrType::Int8: + break; + case AttrType::Int16_2D: + break; + case AttrType::Int32: + break; + case AttrType::Int32_2D: + break; + case AttrType::Float: + validate_float_attribute(iter, 1, verbose, all_attributes_valid, mesh_mut); + break; + case AttrType::Float2: + validate_float_attribute(iter, 2, verbose, all_attributes_valid, mesh_mut); + break; + case AttrType::Float3: + validate_float_attribute(iter, 3, verbose, all_attributes_valid, mesh_mut); + break; + case AttrType::ColorFloat: + validate_float_attribute(iter, 4, verbose, all_attributes_valid, mesh_mut); + break; + case AttrType::Quaternion: + validate_float_attribute(iter, 4, verbose, all_attributes_valid, mesh_mut); + break; + case AttrType::Float4x4: + validate_float_attribute(iter, 16, verbose, all_attributes_valid, mesh_mut); + break; + case AttrType::ColorByte: + break; + case AttrType::String: + break; + } + }); + return all_attributes_valid; +} + +static bool mesh_validate_impl(const Mesh &mesh, const bool verbose, Mesh *mesh_mut) +{ + IndexMaskMemory memory; + + IndexMask valid_edges(mesh.edges_num); + + const IndexMask edges_bad_verts = find_edges_bad_verts(mesh, memory, verbose); + valid_edges = IndexMask::from_difference(valid_edges, edges_bad_verts, memory); + + EdgeMap unique_edges; + const IndexMask edges_duplicate = find_edges_duplicates( + mesh, valid_edges, memory, verbose, unique_edges); + valid_edges = IndexMask::from_difference(valid_edges, edges_duplicate, memory); + + IndexMask valid_faces(mesh.faces_num); + + const IndexMask faces_bad_offsets = find_faces_bad_offsets(mesh, memory, verbose); + valid_faces = IndexMask::from_difference(valid_faces, faces_bad_offsets, memory); + + const IndexMask faces_bad_verts = find_faces_bad_verts(mesh, valid_faces, memory, verbose); + valid_faces = IndexMask::from_difference(valid_faces, faces_bad_verts, memory); + + const IndexMask faces_duplicate_verts = find_faces_duplicate_verts( + mesh, valid_faces, memory, verbose); + valid_faces = IndexMask::from_difference(valid_faces, faces_duplicate_verts, memory); + + const IndexMask faces_missing_edges = find_faces_missing_edges( + mesh, valid_faces, unique_edges, memory, verbose); + valid_faces = IndexMask::from_difference(valid_faces, faces_missing_edges, memory); + + const IndexMask duplicate_faces = find_duplicate_faces(mesh, valid_faces, memory, verbose); + valid_faces = IndexMask::from_difference(valid_faces, duplicate_faces, memory); + + Vector>> corner_edge_fixes; + find_faces_bad_edges(mesh, valid_faces, unique_edges, memory, verbose, corner_edge_fixes); + + bool valid = valid_edges.size() == mesh.edges_num && valid_faces.size() == mesh.faces_num && + corner_edge_fixes.is_empty(); + + if (mesh_mut) { + Mesh &mesh = *mesh_mut; + if (!corner_edge_fixes.is_empty()) { + MutableSpan corner_edges = mesh.corner_edges_for_write(); + for (const Span> replacements : corner_edge_fixes) { + for (const std::pair &replacement : replacements) { + corner_edges[replacement.first] = replacement.second; + } + } + } + + if (valid_faces.size() < mesh.faces_num) { + remove_invalid_faces(mesh, valid_faces); + } + + if (valid_edges.size() < mesh.edges_num) { + remove_invalid_edges(mesh, valid_edges); + } + } + + valid &= validate_vertex_groups(mesh, verbose, mesh_mut); + valid &= validate_material_indices(mesh, true, verbose, mesh_mut); + valid &= validate_selection_history(mesh, verbose, mesh_mut); + valid &= validate_generic_attributes(mesh, verbose, mesh_mut); + + if (valid) { + return true; + } + + if (mesh_mut) { + DEG_id_tag_update(&mesh_mut->id, ID_RECALC_GEOMETRY_ALL_MODES); + } + + return false; +} + +static bool mesh_validate(Mesh &mesh, const bool verbose) +{ + return mesh_validate_impl(mesh, verbose, &mesh); +} + +static bool mesh_is_valid(const Mesh &mesh, const bool verbose) +{ + return mesh_validate_impl(mesh, verbose, nullptr); +} + +} // namespace blender::bke + +bool BKE_mesh_validate(Mesh *mesh, const bool do_verbose, const bool /*cddata_check_mask*/) +{ + if (do_verbose) { + CLOG_INFO(&LOG, "Validating Mesh: %s", mesh->id.name + 2); + } + return !blender::bke::mesh_validate(*mesh, do_verbose); +} + bool BKE_mesh_is_valid(Mesh *mesh) { - const bool do_verbose = true; - const bool do_fixes = false; - - bool is_valid = true; - bool changed = true; - - is_valid &= BKE_mesh_validate_all_customdata( - &mesh->vert_data, - mesh->verts_num, - &mesh->edge_data, - mesh->edges_num, - &mesh->corner_data, - mesh->corners_num, - &mesh->face_data, - mesh->faces_num, - false, /* setting mask here isn't useful, gives false positives */ - do_verbose, - do_fixes, - &changed); - - MutableSpan positions = mesh->vert_positions_for_write(); - MutableSpan edges = mesh->edges_for_write(); - Span face_offsets = mesh->face_offsets(); - Span corner_verts = mesh->corner_verts(); - MutableSpan corner_edges = mesh->corner_edges_for_write(); - - MDeformVert *dverts = static_cast( - CustomData_get_layer_for_write(&mesh->vert_data, CD_MDEFORMVERT, mesh->verts_num)); - is_valid &= BKE_mesh_validate_arrays( - mesh, - reinterpret_cast(positions.data()), - positions.size(), - edges.data(), - edges.size(), - (MFace *)CustomData_get_layer_for_write(&mesh->fdata_legacy, CD_MFACE, mesh->totface_legacy), - mesh->totface_legacy, - corner_verts.data(), - corner_edges.data(), - corner_verts.size(), - face_offsets.data(), - mesh->faces_num, - dverts, - do_verbose, - do_fixes, - &changed); - - BLI_assert(changed == false); - - return is_valid; + return blender::bke::mesh_is_valid(*mesh, true); } bool BKE_mesh_validate_material_indices(Mesh *mesh) { - const int mat_nr_max = max_ii(0, mesh->totcol - 1); - bool is_valid = true; - - blender::bke::AttributeWriter material_indices = - mesh->attributes_for_write().lookup_for_write("material_index"); - blender::MutableVArraySpan material_indices_span(material_indices.varray); - for (const int i : material_indices_span.index_range()) { - if (material_indices_span[i] < 0 || material_indices_span[i] > mat_nr_max) { - material_indices_span[i] = 0; - is_valid = false; - } - } - material_indices_span.save(); - material_indices.finish(); - - if (!is_valid) { - DEG_id_tag_update(&mesh->id, ID_RECALC_GEOMETRY_ALL_MODES); - return true; - } - - return false; + return !blender::bke::validate_material_indices(*mesh, false, false, mesh); } - -/** \} */ - -/* -------------------------------------------------------------------- */ -/** \name Mesh Stripping (removing invalid data) - * \{ */ - -void strip_loose_faces_corners(Mesh *mesh, blender::BitSpan faces_to_remove) -{ - /* Ensure layers are mutable so that #CustomData_copy_data can be used. */ - CustomData_ensure_layers_are_mutable(&mesh->face_data, mesh->faces_num); - CustomData_ensure_layers_are_mutable(&mesh->corner_data, mesh->corners_num); - - MutableSpan face_offsets = mesh->face_offsets_for_write(); - MutableSpan corner_edges = mesh->corner_edges_for_write(); - - int a, b; - /* New corners idx! */ - int *new_idx = MEM_malloc_arrayN(size_t(mesh->corners_num), __func__); - - for (a = b = 0; a < mesh->faces_num; a++) { - bool invalid = false; - int start = face_offsets[a]; - int size = face_offsets[a + 1] - start; - int stop = start + size; - - if (faces_to_remove[a]) { - invalid = true; - } - else if (stop > mesh->corners_num || stop < start || size < 0) { - invalid = true; - } - else { - /* If one of the face's corners is invalid, the whole face is invalid! */ - if (corner_edges.slice(start, size).contains(INVALID_CORNER_EDGE_MARKER)) { - invalid = true; - } - } - - if (size >= 3 && !invalid) { - if (a != b) { - face_offsets[b] = face_offsets[a]; - CustomData_copy_data(&mesh->face_data, &mesh->face_data, a, b, 1); - } - b++; - } - } - if (a != b) { - CustomData_free_elem(&mesh->face_data, b, a - b); - mesh->faces_num = b; - } - - /* And now, get rid of invalid corners. */ - int corner = 0; - for (a = b = 0; a < mesh->corners_num; a++, corner++) { - if (corner_edges[corner] != INVALID_CORNER_EDGE_MARKER) { - if (a != b) { - CustomData_copy_data(&mesh->corner_data, &mesh->corner_data, a, b, 1); - } - new_idx[a] = b; - b++; - } - else { - /* XXX Theoretically, we should be able to not do this, as no remaining face - * should use any stripped corner. But for security's sake... */ - new_idx[a] = -a; - } - } - if (a != b) { - CustomData_free_elem(&mesh->corner_data, b, a - b); - mesh->corners_num = b; - } - - face_offsets[mesh->faces_num] = mesh->corners_num; - - /* And now, update faces' start corner index. */ - /* NOTE: At this point, there should never be any face using a stripped corner! */ - for (const int i : blender::IndexRange(mesh->faces_num)) { - face_offsets[i] = new_idx[face_offsets[i]]; - BLI_assert(face_offsets[i] >= 0); - } - - MEM_freeN(new_idx); -} - -void mesh_strip_edges(Mesh *mesh) -{ - /* Ensure layers are mutable so that #CustomData_copy_data can be used. */ - CustomData_ensure_layers_are_mutable(&mesh->edge_data, mesh->edges_num); - - blender::int2 *e; - int a, b; - uint *new_idx = MEM_malloc_arrayN(size_t(mesh->edges_num), __func__); - MutableSpan edges = mesh->edges_for_write(); - - for (a = b = 0, e = edges.data(); a < mesh->edges_num; a++, e++) { - if ((*e)[0] != (*e)[1]) { - if (a != b) { - memcpy(&edges[b], e, sizeof(edges[b])); - CustomData_copy_data(&mesh->edge_data, &mesh->edge_data, a, b, 1); - } - new_idx[a] = b; - b++; - } - else { - new_idx[a] = INVALID_CORNER_EDGE_MARKER; - } - } - if (a != b) { - CustomData_free_elem(&mesh->edge_data, b, a - b); - mesh->edges_num = b; - } - - /* And now, update corners' edge indices. */ - /* XXX We hope no corner was pointing to a stripped edge! - * Else, its e will be set to INVALID_CORNER_EDGE_MARKER :/ */ - MutableSpan corner_edges = mesh->corner_edges_for_write(); - for (const int i : corner_edges.index_range()) { - corner_edges[i] = new_idx[corner_edges[i]]; - } - - MEM_freeN(new_idx); -} - -/** \} */ diff --git a/source/blender/blenloader/intern/versioning_290.cc b/source/blender/blenloader/intern/versioning_290.cc index 171f24c2afa..cb1d65c427f 100644 --- a/source/blender/blenloader/intern/versioning_290.cc +++ b/source/blender/blenloader/intern/versioning_290.cc @@ -66,6 +66,7 @@ #include "BKE_multires.hh" #include "BKE_node.hh" #include "BKE_node_legacy_types.hh" +#include "BKE_report.hh" #include "IMB_imbuf_enums.h" #include "MEM_guardedalloc.h" @@ -74,10 +75,15 @@ #include "SEQ_sequencer.hh" #include "SEQ_time.hh" +#include "BLO_read_write.hh" #include "BLO_readfile.hh" #include "readfile.hh" #include "versioning_common.hh" +#include "BLT_translation.hh" + +#include + /* Make preferences read-only, use `versioning_userdef.cc`. */ #define U (*((const UserDef *)&U)) @@ -818,27 +824,12 @@ void blo_do_versions_290(FileData *fd, Library * /*lib*/, Main *bmain) CustomData_get_layer(&me->face_data, CD_MPOLY)); for (const int i : blender::IndexRange(me->faces_num)) { if (polys[i].totloop == 2) { - bool changed; - BKE_mesh_legacy_convert_loops_to_corners(me); - BKE_mesh_legacy_convert_polys_to_offsets(me); - BKE_mesh_validate_arrays( - me, - reinterpret_cast(me->vert_positions_for_write().data()), - me->verts_num, - me->edges_for_write().data(), - me->edges_num, - (MFace *)CustomData_get_layer_for_write( - &me->fdata_legacy, CD_MFACE, me->totface_legacy), - me->totface_legacy, - me->corner_verts().data(), - me->corner_edges_for_write().data(), - me->corners_num, - me->face_offsets().data(), - me->faces_num, - me->deform_verts_for_write().data(), - false, - true, - &changed); + std::string message = fmt::format( + fmt::runtime(RPT_("Mesh %s has invalid faces, likely caused by the manifold extrude " + "tool in version 2.90.0. Opening and saving the file in a version " + "prior to 5.1 should resolve the issue\n")), + me->id.name + 2); + BLO_read_invalidate_message((BlendHandle *)fd, bmain, message.c_str()); break; } } diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 5b1bc2cb6b8..c4520e65322 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -414,6 +414,11 @@ if(TEST_SRC_DIR_EXISTS) --python ${TEST_PYTHON_DIR}/mesh_join.py ) + add_blender_test_allow_error( + mesh_validate + --python ${TEST_PYTHON_DIR}/mesh_validate.py + ) + add_blender_test( object_edit --python ${TEST_PYTHON_DIR}/object_edit.py diff --git a/tests/python/mesh_validate.py b/tests/python/mesh_validate.py new file mode 100644 index 00000000000..129320f128d --- /dev/null +++ b/tests/python/mesh_validate.py @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: 2025 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + + +import unittest +import bpy +import sys + + +class TestMeshValidate(unittest.TestCase): + + def setUp(self): + if bpy.context.object and bpy.context.object.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete() + + for mesh in list(bpy.data.meshes): + bpy.data.meshes.remove(mesh) + + def tearDown(self): + for mesh in list(bpy.data.meshes): + bpy.data.meshes.remove(mesh) + + def test_invalid_edge_vertex_indices(self): + verts = [(0, 0, 0), (1, 1, 1)] + edges = [(0, 99)] # Invalid vertex 99 + + mesh = bpy.data.meshes.new("test_mesh") + mesh.from_pydata(verts, edges, []) + + self.assertTrue(mesh.validate(verbose=True)) + self.assertFalse(mesh.validate(verbose=True)) + + def test_duplicate_edge_vertex_indices(self): + verts = [(0, 0, 0), (1, 1, 1)] + edges = [(0, 0)] # Invalid edge from a vertex to itself + + mesh = bpy.data.meshes.new("test_mesh") + mesh.from_pydata(verts, edges, []) + + self.assertTrue(mesh.validate(verbose=True)) + self.assertFalse(mesh.validate(verbose=True)) + + def test_bad_face_offsets(self): + bpy.ops.mesh.primitive_cube_add() + mesh = bpy.context.active_object.data + + mesh.polygons[0].loop_start = 100 + + self.assertTrue(mesh.validate(verbose=True)) + self.assertFalse(mesh.validate(verbose=True)) + + def test_bad_material_indices(self): + bpy.ops.mesh.primitive_plane_add() + obj = bpy.context.active_object + mesh = obj.data + + attr = mesh.attributes.new(name="material_index", type='INT', domain='FACE') + attr.data[0].value = -4 + + self.assertTrue(mesh.validate(verbose=True)) + self.assertFalse(mesh.validate(verbose=True)) + + def test_duplicate_faces(self): + verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0)] + faces = [(0, 1, 2), (2, 0, 1)] + + mesh = bpy.data.meshes.new("test_mesh") + mesh.from_pydata(verts, [], faces) + + self.assertTrue(mesh.validate(verbose=True)) + self.assertFalse(mesh.validate(verbose=True)) + + def test_invalid_float_attributes(self): + bpy.ops.mesh.primitive_plane_add() + mesh = bpy.context.active_object.data + + mesh.vertices[0].co.x = float('nan') + + self.assertTrue(mesh.validate(verbose=True)) + self.assertFalse(mesh.validate(verbose=True)) + + def test_duplicate_edges(self): + verts = [(0, 0, 0), (1, 1, 1)] + edges = [(0, 1), (1, 0)] + + mesh = bpy.data.meshes.new("test_mesh") + mesh.from_pydata(verts, edges, []) + + self.assertTrue(mesh.validate(verbose=True)) + self.assertFalse(mesh.validate(verbose=True)) + + def test_faces_with_bad_edge_references(self): + verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)] + edges = [] + faces = [(0, 1, 2, 3)] + + mesh = bpy.data.meshes.new("test_mesh") + mesh.from_pydata(verts, edges, faces) + + corner_edges = mesh.attributes[".corner_edge"].data + corner_edges[2].value = 0 + + self.assertTrue(mesh.validate(verbose=True)) + self.assertFalse(mesh.validate(verbose=True)) + + +if __name__ == '__main__': + sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []) + unittest.main()