**Problem Description** Blender's current mesh data layout often lacks spatial coherence, causing performance bottlenecks during BVH construction for sculpting and painting operations. Each time a BVH is built, the system must recompute spatial partitioning and vertex groupings from scratch, leading to redundant calculations and suboptimal memory access patterns. **Proposed Solution** This patch implements pre-computed spatial organization of mesh data through a new `mesh_apply_spatial_organization()` function that: - Reorders vertices and faces based on spatial locality using recursive spatial partitioning. - Stores pre-computed MeshGroup hierarchies in MeshRuntime for reuse. - Enables the BVH system to bypass expensive spatial computation when pre-organized data is available. This approach separates the expensive spatial computation from more frequent BVH rebuilds, providing sustained performance improvements across multiple sculpting operations. **Limitations** - Requires manual invocation (occurs automatically only during remesh operations). - Additional memory overhead for storing MeshGroup metadata. - One-time computational cost during initial organization. - Spatial group references are not yet stored in files. **User Interface** The feature is accessible via a new "Reorder Mesh Spatially" operator in the Mesh Data Properties panel under the Geometry Data section. Users can invoke it manually when needed, or it will be applied automatically during quadriflow and voxel remesh operations. The operator provides feedback confirming successful spatial reordering. Pull Request: https://projects.blender.org/blender/blender/pulls/139536
537 lines
16 KiB
C++
537 lines
16 KiB
C++
/* SPDX-FileCopyrightText: 2005 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup bke
|
|
*/
|
|
|
|
#include "BLI_array_utils.hh"
|
|
#include "BLI_math_geom.h"
|
|
|
|
#include "BKE_bake_data_block_id.hh"
|
|
#include "BKE_bvhutils.hh"
|
|
#include "BKE_customdata.hh"
|
|
#include "BKE_editmesh_cache.hh"
|
|
#include "BKE_lib_id.hh"
|
|
#include "BKE_mesh.hh"
|
|
#include "BKE_mesh_mapping.hh"
|
|
#include "BKE_mesh_runtime.hh"
|
|
#include "BKE_shrinkwrap.hh"
|
|
#include "BKE_subdiv_ccg.hh"
|
|
|
|
using blender::float3;
|
|
using blender::MutableSpan;
|
|
using blender::Span;
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Mesh Runtime Struct Utils
|
|
* \{ */
|
|
|
|
namespace blender::bke {
|
|
|
|
static void free_mesh_eval(MeshRuntime &mesh_runtime)
|
|
{
|
|
if (mesh_runtime.mesh_eval != nullptr) {
|
|
BKE_id_free(nullptr, mesh_runtime.mesh_eval);
|
|
mesh_runtime.mesh_eval = nullptr;
|
|
}
|
|
}
|
|
|
|
static void free_batch_cache(MeshRuntime &mesh_runtime)
|
|
{
|
|
if (mesh_runtime.batch_cache) {
|
|
BKE_mesh_batch_cache_free(mesh_runtime.batch_cache);
|
|
mesh_runtime.batch_cache = nullptr;
|
|
}
|
|
}
|
|
|
|
static void free_bvh_caches(MeshRuntime &mesh_runtime)
|
|
{
|
|
mesh_runtime.bvh_cache_verts.tag_dirty();
|
|
mesh_runtime.bvh_cache_edges.tag_dirty();
|
|
mesh_runtime.bvh_cache_faces.tag_dirty();
|
|
mesh_runtime.bvh_cache_corner_tris.tag_dirty();
|
|
mesh_runtime.bvh_cache_corner_tris_no_hidden.tag_dirty();
|
|
mesh_runtime.bvh_cache_loose_verts.tag_dirty();
|
|
mesh_runtime.bvh_cache_loose_verts_no_hidden.tag_dirty();
|
|
mesh_runtime.bvh_cache_loose_edges.tag_dirty();
|
|
mesh_runtime.bvh_cache_loose_edges_no_hidden.tag_dirty();
|
|
}
|
|
|
|
MeshRuntime::MeshRuntime() = default;
|
|
|
|
MeshRuntime::~MeshRuntime()
|
|
{
|
|
free_mesh_eval(*this);
|
|
free_batch_cache(*this);
|
|
}
|
|
|
|
static int reset_bits_and_count(MutableBitSpan bits, const Span<int> indices_to_reset)
|
|
{
|
|
int count = bits.size();
|
|
for (const int i : indices_to_reset) {
|
|
if (bits[i]) {
|
|
bits[i].reset();
|
|
count--;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void bit_vector_with_reset_bits_or_empty(const Span<int> indices_to_reset,
|
|
const int indexed_elems_num,
|
|
BitVector<> &r_bits,
|
|
int &r_count)
|
|
{
|
|
r_bits.resize(0);
|
|
r_bits.resize(indexed_elems_num, true);
|
|
r_count = reset_bits_and_count(r_bits, indices_to_reset);
|
|
if (r_count == 0) {
|
|
r_bits.clear_and_shrink();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If there are no loose edges and no loose vertices, all vertices are used by faces.
|
|
*/
|
|
static void try_tag_verts_no_face_none(const Mesh &mesh)
|
|
{
|
|
if (!mesh.runtime->loose_edges_cache.is_cached() || mesh.loose_edges().count > 0) {
|
|
return;
|
|
}
|
|
if (!mesh.runtime->loose_verts_cache.is_cached() || mesh.loose_verts().count > 0) {
|
|
return;
|
|
}
|
|
mesh.runtime->verts_no_face_cache.ensure([&](LooseVertCache &r_data) {
|
|
r_data.is_loose_bits.clear_and_shrink();
|
|
r_data.count = 0;
|
|
});
|
|
}
|
|
|
|
} // namespace blender::bke
|
|
|
|
blender::Span<int> Mesh::corner_to_face_map() const
|
|
{
|
|
using namespace blender;
|
|
this->runtime->corner_to_face_map_cache.ensure([&](Array<int> &r_data) {
|
|
const OffsetIndices faces = this->faces();
|
|
r_data = bke::mesh::build_corner_to_face_map(faces);
|
|
});
|
|
return this->runtime->corner_to_face_map_cache.data();
|
|
}
|
|
|
|
blender::OffsetIndices<int> Mesh::vert_to_face_map_offsets() const
|
|
{
|
|
using namespace blender;
|
|
this->runtime->vert_to_face_offset_cache.ensure([&](Array<int> &r_data) {
|
|
r_data = Array<int>(this->verts_num + 1, 0);
|
|
offset_indices::build_reverse_offsets(this->corner_verts(), r_data);
|
|
});
|
|
return OffsetIndices<int>(this->runtime->vert_to_face_offset_cache.data());
|
|
}
|
|
|
|
blender::GroupedSpan<int> Mesh::vert_to_face_map() const
|
|
{
|
|
using namespace blender;
|
|
const OffsetIndices offsets = this->vert_to_face_map_offsets();
|
|
this->runtime->vert_to_face_map_cache.ensure([&](Array<int> &r_data) {
|
|
r_data.reinitialize(this->corners_num);
|
|
if (this->runtime->vert_to_corner_map_cache.is_cached() &&
|
|
this->runtime->corner_to_face_map_cache.is_cached())
|
|
{
|
|
/* The vertex to face cache can be built from the vertex to face corner
|
|
* and face corner to face maps if they are both already cached. */
|
|
array_utils::gather(this->runtime->corner_to_face_map_cache.data().as_span(),
|
|
this->runtime->vert_to_corner_map_cache.data().as_span(),
|
|
r_data.as_mutable_span());
|
|
}
|
|
else {
|
|
bke::mesh::build_vert_to_face_indices(this->faces(), this->corner_verts(), offsets, r_data);
|
|
}
|
|
});
|
|
return {offsets, this->runtime->vert_to_face_map_cache.data()};
|
|
}
|
|
|
|
blender::GroupedSpan<int> Mesh::vert_to_corner_map() const
|
|
{
|
|
using namespace blender;
|
|
const OffsetIndices offsets = this->vert_to_face_map_offsets();
|
|
this->runtime->vert_to_corner_map_cache.ensure([&](Array<int> &r_data) {
|
|
r_data = bke::mesh::build_vert_to_corner_indices(this->corner_verts(), offsets);
|
|
});
|
|
return {offsets, this->runtime->vert_to_corner_map_cache.data()};
|
|
}
|
|
|
|
const blender::bke::LooseVertCache &Mesh::loose_verts() const
|
|
{
|
|
using namespace blender::bke;
|
|
this->runtime->loose_verts_cache.ensure([&](LooseVertCache &r_data) {
|
|
const Span<int> verts = this->edges().cast<int>();
|
|
bit_vector_with_reset_bits_or_empty(
|
|
verts, this->verts_num, r_data.is_loose_bits, r_data.count);
|
|
});
|
|
return this->runtime->loose_verts_cache.data();
|
|
}
|
|
|
|
const blender::bke::LooseVertCache &Mesh::verts_no_face() const
|
|
{
|
|
using namespace blender::bke;
|
|
this->runtime->verts_no_face_cache.ensure([&](LooseVertCache &r_data) {
|
|
const Span<int> verts = this->corner_verts();
|
|
bit_vector_with_reset_bits_or_empty(
|
|
verts, this->verts_num, r_data.is_loose_bits, r_data.count);
|
|
});
|
|
return this->runtime->verts_no_face_cache.data();
|
|
}
|
|
|
|
bool Mesh::no_overlapping_topology() const
|
|
{
|
|
return this->flag & ME_NO_OVERLAPPING_TOPOLOGY;
|
|
}
|
|
|
|
const blender::bke::LooseEdgeCache &Mesh::loose_edges() const
|
|
{
|
|
using namespace blender::bke;
|
|
this->runtime->loose_edges_cache.ensure([&](LooseEdgeCache &r_data) {
|
|
const Span<int> edges = this->corner_edges();
|
|
bit_vector_with_reset_bits_or_empty(
|
|
edges, this->edges_num, r_data.is_loose_bits, r_data.count);
|
|
});
|
|
return this->runtime->loose_edges_cache.data();
|
|
}
|
|
|
|
void Mesh::tag_loose_verts_none() const
|
|
{
|
|
using namespace blender::bke;
|
|
this->runtime->loose_verts_cache.ensure([&](LooseVertCache &r_data) {
|
|
r_data.is_loose_bits.clear_and_shrink();
|
|
r_data.count = 0;
|
|
});
|
|
try_tag_verts_no_face_none(*this);
|
|
}
|
|
|
|
void Mesh::tag_loose_edges_none() const
|
|
{
|
|
using namespace blender::bke;
|
|
this->runtime->loose_edges_cache.ensure([&](LooseEdgeCache &r_data) {
|
|
r_data.is_loose_bits.clear_and_shrink();
|
|
r_data.count = 0;
|
|
});
|
|
try_tag_verts_no_face_none(*this);
|
|
}
|
|
|
|
void Mesh::tag_overlapping_none()
|
|
{
|
|
using namespace blender::bke;
|
|
this->flag |= ME_NO_OVERLAPPING_TOPOLOGY;
|
|
}
|
|
|
|
namespace blender::bke {
|
|
|
|
void TrianglesCache::freeze()
|
|
{
|
|
this->frozen = true;
|
|
this->dirty_while_frozen = false;
|
|
}
|
|
|
|
void TrianglesCache::unfreeze()
|
|
{
|
|
this->frozen = false;
|
|
if (this->dirty_while_frozen) {
|
|
this->data.tag_dirty();
|
|
}
|
|
this->dirty_while_frozen = false;
|
|
}
|
|
|
|
void TrianglesCache::tag_dirty()
|
|
{
|
|
if (this->frozen) {
|
|
this->dirty_while_frozen = true;
|
|
}
|
|
else {
|
|
this->data.tag_dirty();
|
|
}
|
|
}
|
|
|
|
} // namespace blender::bke
|
|
|
|
blender::Span<blender::int3> Mesh::corner_tris() const
|
|
{
|
|
this->runtime->corner_tris_cache.data.ensure([&](blender::Array<blender::int3> &r_data) {
|
|
const Span<float3> positions = this->vert_positions();
|
|
const blender::OffsetIndices faces = this->faces();
|
|
const Span<int> corner_verts = this->corner_verts();
|
|
|
|
r_data.reinitialize(poly_to_tri_count(faces.size(), corner_verts.size()));
|
|
|
|
if (BKE_mesh_face_normals_are_dirty(this)) {
|
|
blender::bke::mesh::corner_tris_calc(positions, faces, corner_verts, r_data);
|
|
}
|
|
else {
|
|
blender::bke::mesh::corner_tris_calc_with_normals(
|
|
positions, faces, corner_verts, this->face_normals(), r_data);
|
|
}
|
|
});
|
|
|
|
return this->runtime->corner_tris_cache.data.data();
|
|
}
|
|
|
|
blender::Span<int> Mesh::corner_tri_faces() const
|
|
{
|
|
using namespace blender;
|
|
this->runtime->corner_tri_faces_cache.ensure([&](blender::Array<int> &r_data) {
|
|
const OffsetIndices faces = this->faces();
|
|
r_data.reinitialize(poly_to_tri_count(faces.size(), this->corners_num));
|
|
bke::mesh::corner_tris_calc_face_indices(faces, r_data);
|
|
});
|
|
return this->runtime->corner_tri_faces_cache.data();
|
|
}
|
|
|
|
int BKE_mesh_runtime_corner_tris_len(const Mesh *mesh)
|
|
{
|
|
/* Allow returning the size without calculating the cache. */
|
|
return poly_to_tri_count(mesh->faces_num, mesh->corners_num);
|
|
}
|
|
|
|
void BKE_mesh_runtime_ensure_edit_data(Mesh *mesh)
|
|
{
|
|
if (!mesh->runtime->edit_data) {
|
|
mesh->runtime->edit_data = std::make_unique<blender::bke::EditMeshData>();
|
|
}
|
|
}
|
|
|
|
void BKE_mesh_runtime_clear_cache(Mesh *mesh)
|
|
{
|
|
using namespace blender::bke;
|
|
free_mesh_eval(*mesh->runtime);
|
|
free_batch_cache(*mesh->runtime);
|
|
mesh->runtime->edit_data.reset();
|
|
BKE_mesh_runtime_clear_geometry(mesh);
|
|
}
|
|
|
|
void BKE_mesh_runtime_clear_geometry(Mesh *mesh)
|
|
{
|
|
/* Tagging shared caches dirty will free the allocated data if there is only one user. */
|
|
free_bvh_caches(*mesh->runtime);
|
|
mesh->runtime->subdiv_ccg.reset();
|
|
mesh->runtime->bounds_cache.tag_dirty();
|
|
mesh->runtime->vert_to_face_offset_cache.tag_dirty();
|
|
mesh->runtime->vert_to_face_map_cache.tag_dirty();
|
|
mesh->runtime->vert_to_corner_map_cache.tag_dirty();
|
|
mesh->runtime->corner_to_face_map_cache.tag_dirty();
|
|
mesh->runtime->vert_normals_cache.tag_dirty();
|
|
mesh->runtime->vert_normals_true_cache.tag_dirty();
|
|
mesh->runtime->face_normals_cache.tag_dirty();
|
|
mesh->runtime->face_normals_true_cache.tag_dirty();
|
|
mesh->runtime->corner_normals_cache.tag_dirty();
|
|
mesh->runtime->loose_edges_cache.tag_dirty();
|
|
mesh->runtime->loose_verts_cache.tag_dirty();
|
|
mesh->runtime->verts_no_face_cache.tag_dirty();
|
|
mesh->runtime->corner_tris_cache.data.tag_dirty();
|
|
mesh->runtime->corner_tri_faces_cache.tag_dirty();
|
|
mesh->runtime->shrinkwrap_boundary_cache.tag_dirty();
|
|
mesh->runtime->max_material_index.tag_dirty();
|
|
mesh->runtime->subsurf_face_dot_tags.clear_and_shrink();
|
|
mesh->runtime->subsurf_optimal_display_edges.clear_and_shrink();
|
|
mesh->runtime->spatial_groups.reset();
|
|
mesh->flag &= ~ME_NO_OVERLAPPING_TOPOLOGY;
|
|
}
|
|
|
|
void Mesh::tag_edges_split()
|
|
{
|
|
/* Triangulation didn't change because vertex positions and loop vertex indices didn't change. */
|
|
free_bvh_caches(*this->runtime);
|
|
this->runtime->vert_normals_cache.tag_dirty();
|
|
this->runtime->corner_normals_cache.tag_dirty();
|
|
this->runtime->subdiv_ccg.reset();
|
|
this->runtime->vert_to_face_offset_cache.tag_dirty();
|
|
this->runtime->vert_to_face_map_cache.tag_dirty();
|
|
this->runtime->vert_to_corner_map_cache.tag_dirty();
|
|
if (this->runtime->loose_edges_cache.is_cached() &&
|
|
this->runtime->loose_edges_cache.data().count != 0)
|
|
{
|
|
this->runtime->loose_edges_cache.tag_dirty();
|
|
}
|
|
if (this->runtime->loose_verts_cache.is_cached() &&
|
|
this->runtime->loose_verts_cache.data().count != 0)
|
|
{
|
|
this->runtime->loose_verts_cache.tag_dirty();
|
|
}
|
|
if (this->runtime->verts_no_face_cache.is_cached() &&
|
|
this->runtime->verts_no_face_cache.data().count != 0)
|
|
{
|
|
this->runtime->verts_no_face_cache.tag_dirty();
|
|
}
|
|
this->runtime->subsurf_face_dot_tags.clear_and_shrink();
|
|
this->runtime->subsurf_optimal_display_edges.clear_and_shrink();
|
|
this->runtime->shrinkwrap_boundary_cache.tag_dirty();
|
|
}
|
|
|
|
void Mesh::tag_sharpness_changed()
|
|
{
|
|
this->runtime->vert_normals_cache.tag_dirty();
|
|
this->runtime->face_normals_cache.tag_dirty();
|
|
this->runtime->corner_normals_cache.tag_dirty();
|
|
}
|
|
|
|
void Mesh::tag_custom_normals_changed()
|
|
{
|
|
this->runtime->vert_normals_cache.tag_dirty();
|
|
this->runtime->face_normals_cache.tag_dirty();
|
|
this->runtime->corner_normals_cache.tag_dirty();
|
|
}
|
|
|
|
void Mesh::tag_face_winding_changed()
|
|
{
|
|
this->runtime->vert_normals_cache.tag_dirty();
|
|
this->runtime->face_normals_cache.tag_dirty();
|
|
this->runtime->vert_normals_true_cache.tag_dirty();
|
|
this->runtime->face_normals_true_cache.tag_dirty();
|
|
this->runtime->corner_normals_cache.tag_dirty();
|
|
this->runtime->vert_to_corner_map_cache.tag_dirty();
|
|
this->runtime->shrinkwrap_boundary_cache.tag_dirty();
|
|
}
|
|
|
|
void Mesh::tag_positions_changed()
|
|
{
|
|
this->runtime->vert_normals_cache.tag_dirty();
|
|
this->runtime->face_normals_cache.tag_dirty();
|
|
this->runtime->vert_normals_true_cache.tag_dirty();
|
|
this->runtime->face_normals_true_cache.tag_dirty();
|
|
this->runtime->corner_normals_cache.tag_dirty();
|
|
this->runtime->shrinkwrap_boundary_cache.tag_dirty();
|
|
this->tag_positions_changed_no_normals();
|
|
}
|
|
|
|
void Mesh::tag_positions_changed_no_normals()
|
|
{
|
|
free_bvh_caches(*this->runtime);
|
|
this->runtime->corner_tris_cache.tag_dirty();
|
|
this->runtime->bounds_cache.tag_dirty();
|
|
this->runtime->shrinkwrap_boundary_cache.tag_dirty();
|
|
}
|
|
|
|
void Mesh::tag_positions_changed_uniformly()
|
|
{
|
|
/* The normals and triangulation didn't change, since all verts moved by the same amount. */
|
|
free_bvh_caches(*this->runtime);
|
|
this->runtime->bounds_cache.tag_dirty();
|
|
}
|
|
|
|
void Mesh::tag_topology_changed()
|
|
{
|
|
BKE_mesh_runtime_clear_geometry(this);
|
|
}
|
|
|
|
void Mesh::tag_visibility_changed()
|
|
{
|
|
this->runtime->bvh_cache_corner_tris_no_hidden.tag_dirty();
|
|
this->runtime->bvh_cache_loose_verts_no_hidden.tag_dirty();
|
|
this->runtime->bvh_cache_loose_edges_no_hidden.tag_dirty();
|
|
}
|
|
|
|
void Mesh::tag_material_index_changed()
|
|
{
|
|
this->runtime->max_material_index.tag_dirty();
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Mesh Batch Cache Callbacks
|
|
* \{ */
|
|
|
|
/* Draw Engine */
|
|
|
|
void (*BKE_mesh_batch_cache_dirty_tag_cb)(Mesh *mesh, eMeshBatchDirtyMode mode) = nullptr;
|
|
void (*BKE_mesh_batch_cache_free_cb)(void *batch_cache) = nullptr;
|
|
|
|
void BKE_mesh_batch_cache_dirty_tag(Mesh *mesh, eMeshBatchDirtyMode mode)
|
|
{
|
|
if (mesh->runtime->batch_cache) {
|
|
BKE_mesh_batch_cache_dirty_tag_cb(mesh, mode);
|
|
}
|
|
|
|
/* Also tag batch cache for subdivided mesh, if it exists this will be
|
|
* the mesh that is actually being drawn. */
|
|
Mesh *mesh_eval = mesh->runtime->mesh_eval;
|
|
if (mesh_eval && mesh_eval->runtime->batch_cache) {
|
|
BKE_mesh_batch_cache_dirty_tag_cb(mesh_eval, mode);
|
|
}
|
|
}
|
|
void BKE_mesh_batch_cache_free(void *batch_cache)
|
|
{
|
|
BKE_mesh_batch_cache_free_cb(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<float3> positions = mesh_eval->vert_positions_for_write();
|
|
MutableSpan<blender::int2> edges = mesh_eval->edges_for_write();
|
|
Span<int> face_offsets = mesh_eval->face_offsets();
|
|
Span<int> corner_verts = mesh_eval->corner_verts();
|
|
MutableSpan<int> 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<MDeformVert *>(
|
|
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<float(*)[3]>(positions.data()),
|
|
positions.size(),
|
|
edges.data(),
|
|
edges.size(),
|
|
static_cast<MFace *>(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 */
|
|
|
|
/** \} */
|