Sculpt: Reuse existing mesh triangles cache in sculpt mode

Addresses #121240.
Instead of allocating a new array and recalculating mesh triangulation
every time the user enters sculpt mode, reuse the mesh's existing cache.
Currently in order to avoid recalculating triangulation on every brush update
(which would typically be necessary because the triangulation direction
depends on vertex positions), add a mechanism to "freeze" the cache to
skip recalculations until the user exits sculpt mode. That even avoids
recalculation if vertex positions aren't affected. This is necessary because
we can't use the cache in a dirty state; tagging the cache dirty frees the
triangulation array.

Removing the duplicate triangles array reduces memory usage by 384 MB
in a 16 million vertex sculpt, and makes entering sculpt mode 125ms faster
(tested on a Ryzen 7840u).

In the long term, I hope we find a different solution that's a bit more
transparent and hopefully more integrated with the caching system
in general. In the meantime, this is a relatively safe low impact change
that helps document the needs for such a system anyway.

Pull Request: https://projects.blender.org/blender/blender/pulls/123638
This commit is contained in:
Hans Goudey
2024-06-26 03:04:50 +02:00
committed by Hans Goudey
parent 31de58e161
commit 59d6eae116
5 changed files with 58 additions and 15 deletions

View File

@@ -94,6 +94,19 @@ struct LooseEdgeCache : public LooseGeomCache {};
*/
struct LooseVertCache : public LooseGeomCache {};
struct TrianglesCache {
SharedCache<Array<int3>> data;
bool frozen = false;
bool dirty_while_frozen = false;
/** Delay applying dirty tags from #tag_dirty() until #unfreeze is called. */
void freeze();
/** Apply dirty tags from after #freeze, and make future dirty tags apply immediately. */
void unfreeze();
/** Call instead of `data.tag_dirty()`. */
void tag_dirty();
};
struct MeshRuntime {
/**
* "Evaluated" mesh owned by this mesh. Used for objects which don't have effective modifiers, so
@@ -141,7 +154,7 @@ struct MeshRuntime {
void *batch_cache = nullptr;
/** Cache for derived triangulation of the mesh, accessed with #Mesh::corner_tris(). */
SharedCache<Array<int3>> corner_tris_cache;
TrianglesCache corner_tris_cache;
/** Cache for triangle to original face index map, accessed with #Mesh::corner_tri_faces(). */
SharedCache<Array<int>> corner_tri_faces_cache;

View File

@@ -226,9 +226,38 @@ void Mesh::tag_overlapping_none()
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.ensure([&](blender::Array<blender::int3> &r_data) {
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();
@@ -244,7 +273,7 @@ blender::Span<blender::int3> Mesh::corner_tris() const
}
});
return this->runtime->corner_tris_cache.data();
return this->runtime->corner_tris_cache.data.data();
}
blender::Span<int> Mesh::corner_tri_faces() const
@@ -296,7 +325,7 @@ void BKE_mesh_runtime_clear_geometry(Mesh *mesh)
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.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->subsurf_face_dot_tags.clear_and_shrink();

View File

@@ -654,14 +654,10 @@ std::unique_ptr<PBVH> build_mesh(Mesh *mesh)
pbvh->header.type = PBVH_FACES;
const int totvert = mesh->verts_num;
const int corner_tris_num = poly_to_tri_count(mesh->faces_num, mesh->corners_num);
MutableSpan<float3> vert_positions = mesh->vert_positions_for_write();
const OffsetIndices<int> faces = mesh->faces();
const Span<int> corner_verts = mesh->corner_verts();
pbvh->corner_tris.reinitialize(corner_tris_num);
mesh::corner_tris_calc(vert_positions, faces, corner_verts, pbvh->corner_tris);
const Span<int3> corner_tris = pbvh->corner_tris;
const Span<int3> corner_tris = mesh->corner_tris();
pbvh->corner_tris = corner_tris;
pbvh->mesh = mesh;
@@ -681,7 +677,7 @@ std::unique_ptr<PBVH> build_mesh(Mesh *mesh)
#endif
/* For each face, store the AABB and the AABB centroid */
Array<Bounds<float3>> prim_bounds(corner_tris_num);
Array<Bounds<float3>> prim_bounds(corner_tris.size());
const Bounds<float3> cb = threading::parallel_reduce(
corner_tris.index_range(),
1024,
@@ -701,7 +697,7 @@ std::unique_ptr<PBVH> build_mesh(Mesh *mesh)
},
[](const Bounds<float3> &a, const Bounds<float3> &b) { return bounds::merge(a, b); });
if (corner_tris_num) {
if (!corner_tris.is_empty()) {
const AttributeAccessor attributes = mesh->attributes();
const VArraySpan hide_poly = *attributes.lookup<bool>(".hide_poly", AttrDomain::Face);
const VArraySpan material_index = *attributes.lookup<int>("material_index", AttrDomain::Face);
@@ -716,7 +712,7 @@ std::unique_ptr<PBVH> build_mesh(Mesh *mesh)
vert_bitmap,
&cb,
prim_bounds,
corner_tris_num);
corner_tris.size());
#ifdef TEST_PBVH_FACE_SPLIT
test_face_boundaries(pbvh, tri_faces);

View File

@@ -154,8 +154,7 @@ struct PBVH {
/** Only valid for polygon meshes. */
blender::OffsetIndices<int> faces;
blender::Span<int> corner_verts;
/* Owned by the #PBVH, because after deformations they have to be recomputed. */
blender::Array<blender::int3> corner_tris;
blender::Span<blender::int3> corner_tris;
/* Grid Data */
CCGKey gridkey;

View File

@@ -355,6 +355,10 @@ void ED_object_sculptmode_enter_ex(Main &bmain,
const int mode_flag = OB_MODE_SCULPT;
Mesh *mesh = BKE_mesh_from_object(&ob);
/* Re-triangulating the mesh for position changes in sculpt mode isn't worth the performance
* impact, so delay triangulation updates until the user exits sculpt mode. */
mesh->runtime->corner_tris_cache.freeze();
/* Enter sculpt mode. */
ob.mode |= mode_flag;
@@ -450,6 +454,8 @@ void ED_object_sculptmode_exit_ex(Main &bmain, Depsgraph &depsgraph, Scene &scen
const int mode_flag = OB_MODE_SCULPT;
Mesh *mesh = BKE_mesh_from_object(&ob);
mesh->runtime->corner_tris_cache.unfreeze();
multires_flush_sculpt_updates(&ob);
/* Not needed for now. */