diff --git a/source/blender/editors/geometry/node_group_operator.cc b/source/blender/editors/geometry/node_group_operator.cc index 8018b2167bf..f3eaa8ba23e 100644 --- a/source/blender/editors/geometry/node_group_operator.cc +++ b/source/blender/editors/geometry/node_group_operator.cc @@ -168,6 +168,41 @@ static void find_socket_log_contexts(const Main &bmain, } } +/** + * This class adds a user to shared mesh data, requiring modifications of the mesh to reallocate + * the data and its sharing info. This allows tracking which data is modified without having to + * explicitly compare it. + */ +class MeshState { + VectorSet sharing_infos_; + + public: + MeshState(const Mesh &mesh) + { + if (mesh.runtime->face_offsets_sharing_info) { + this->freeze_shared_state(*mesh.runtime->face_offsets_sharing_info); + } + mesh.attributes().foreach_attribute([&](const bke::AttributeIter &iter) { + const bke::GAttributeReader attribute = iter.get(); + this->freeze_shared_state(*attribute.sharing_info); + }); + } + + void freeze_shared_state(const ImplicitSharingInfo &sharing_info) + { + if (sharing_infos_.add(&sharing_info)) { + sharing_info.add_user(); + } + } + + ~MeshState() + { + for (const ImplicitSharingInfo *sharing_info : sharing_infos_) { + sharing_info->remove_user_and_delete_if_last(); + } + } +}; + /** * Geometry nodes currently requires working on "evaluated" data-blocks (rather than "original" * data-blocks that are part of a #Main data-base). This could change in the future, but for now, @@ -175,7 +210,8 @@ static void find_socket_log_contexts(const Main &bmain, * sharing lets us avoid copying attribute data though. */ static bke::GeometrySet get_original_geometry_eval_copy(Object &object, - nodes::GeoNodesOperatorData &operator_data) + nodes::GeoNodesOperatorData &operator_data, + Vector &orig_mesh_states) { switch (object.type) { case OB_CURVES: { @@ -199,15 +235,22 @@ static bke::GeometrySet get_original_geometry_eval_copy(Object &object, BKE_id_free(nullptr, mesh_copy); return bke::GeometrySet::from_mesh(final_copy); } - return bke::GeometrySet::from_mesh(BKE_mesh_copy_for_eval(*mesh)); + Mesh *mesh_copy = BKE_mesh_copy_for_eval(*mesh); + orig_mesh_states.append_as(*mesh_copy); + return bke::GeometrySet::from_mesh(mesh_copy); } default: return {}; } } -static void store_result_geometry( - const wmOperator &op, Main &bmain, Scene &scene, Object &object, bke::GeometrySet geometry) +static void store_result_geometry(const wmOperator &op, + const Depsgraph &depsgraph, + Main &bmain, + Scene &scene, + Object &object, + const RegionView3D *rv3d, + bke::GeometrySet geometry) { geometry.ensure_owns_direct_data(); switch (object.type) { @@ -224,6 +267,7 @@ static void store_result_geometry( curves.geometry.wrap() = std::move(new_curves->geometry.wrap()); BKE_object_material_from_eval_data(&bmain, &object, &new_curves->id); + DEG_id_tag_update(&curves.id, ID_RECALC_GEOMETRY); break; } case OB_POINTCLOUD: { @@ -241,6 +285,7 @@ static void store_result_geometry( BKE_object_material_from_eval_data(&bmain, &object, &new_points->id); BKE_pointcloud_nomain_to_pointcloud(new_points, &points); + DEG_id_tag_update(&points.id, ID_RECALC_GEOMETRY); break; } case OB_MESH: { @@ -248,37 +293,33 @@ static void store_result_geometry( const bool has_shape_keys = mesh.key != nullptr; - if (object.mode == OB_MODE_SCULPT) { - sculpt_paint::undo::geometry_begin(scene, object, &op); - } - Mesh *new_mesh = geometry.get_component_for_write().release(); - if (!new_mesh) { - BKE_mesh_clear_geometry(&mesh); - } - else { + if (new_mesh) { /* Anonymous attributes shouldn't be available on the applied geometry. */ new_mesh->attributes_for_write().remove_anonymous(); - BKE_object_material_from_eval_data(&bmain, &object, &new_mesh->id); - if (object.mode == OB_MODE_EDIT) { - EDBM_mesh_make_from_mesh(&object, new_mesh, scene.toolsettings->selectmode, true); - BKE_editmesh_looptris_and_normals_calc(mesh.runtime->edit_mesh.get()); - BKE_id_free(nullptr, new_mesh); - } - else { - BKE_mesh_nomain_to_mesh(new_mesh, &mesh, &object); - } + } + else { + new_mesh = BKE_mesh_new_nomain(0, 0, 0, 0); + } + + if (object.mode == OB_MODE_SCULPT) { + sculpt_paint::store_mesh_from_eval(op, scene, depsgraph, rv3d, object, new_mesh); + } + else if (object.mode == OB_MODE_EDIT) { + EDBM_mesh_make_from_mesh(&object, new_mesh, scene.toolsettings->selectmode, true); + BKE_editmesh_looptris_and_normals_calc(mesh.runtime->edit_mesh.get()); + BKE_id_free(nullptr, new_mesh); + DEG_id_tag_update(&mesh.id, ID_RECALC_GEOMETRY); + } + else { + BKE_mesh_nomain_to_mesh(new_mesh, &mesh, &object); + DEG_id_tag_update(&mesh.id, ID_RECALC_GEOMETRY); } if (has_shape_keys && !mesh.key) { BKE_report(op.reports, RPT_WARNING, "Mesh shape key data removed"); } - - if (object.mode == OB_MODE_SCULPT) { - sculpt_paint::undo::geometry_end(object); - BKE_sculptsession_free_pbvh(object); - } break; } } @@ -539,6 +580,10 @@ static int run_node_group_exec(bContext *C, wmOperator *op) eval_log.node_group_name = node_tree->id.name + 2; find_socket_log_contexts(*bmain, socket_log_contexts); + /* May be null if operator called from outside 3D view context. */ + const RegionView3D *rv3d = CTX_wm_region_view3d(C); + Vector orig_mesh_states; + for (Object *object : objects) { nodes::GeoNodesOperatorData operator_eval_data{}; operator_eval_data.mode = mode; @@ -564,14 +609,14 @@ static int run_node_group_exec(bContext *C, wmOperator *op) call_data.socket_log_contexts = &socket_log_contexts; } - bke::GeometrySet geometry_orig = get_original_geometry_eval_copy(*object, operator_eval_data); + bke::GeometrySet geometry_orig = get_original_geometry_eval_copy( + *object, operator_eval_data, orig_mesh_states); bke::GeometrySet new_geometry = nodes::execute_geometry_nodes_on_geometry( *node_tree, properties, compute_context, call_data, std::move(geometry_orig)); - store_result_geometry(*op, *bmain, *scene, *object, std::move(new_geometry)); - - DEG_id_tag_update(static_cast(object->data), ID_RECALC_GEOMETRY); + store_result_geometry( + *op, *depsgraph_active, *bmain, *scene, *object, rv3d, std::move(new_geometry)); WM_event_add_notifier(C, NC_GEOM | ND_DATA, object->data); } diff --git a/source/blender/editors/include/ED_sculpt.hh b/source/blender/editors/include/ED_sculpt.hh index 2a3536822bf..8796ad183f5 100644 --- a/source/blender/editors/include/ED_sculpt.hh +++ b/source/blender/editors/include/ED_sculpt.hh @@ -12,6 +12,7 @@ struct Depsgraph; struct Main; struct Mesh; struct Object; +struct RegionView3D; struct ReportList; struct Scene; struct UndoType; @@ -91,4 +92,19 @@ int active_update_and_get(bContext *C, Object &ob, const float mval_fl[2]); */ bool object_active_color_fill(Object &ob, const float fill_color[4], bool only_selected); +/** + * Fully replace the sculpt mesh with a mesh outside of #Main. This implements various checks to + * avoid pushing full geometry-type undo steps when possible, allowing for better performance. + * + * \warning To avoid false negatives when detecting mesh changes, it is critical that the caller + * adds an owner to the attribute data arrays before modifying the original object's mesh. This + * allows constant time checks for whether the mesh has changed. + */ +void store_mesh_from_eval(const wmOperator &op, + const Scene &scene, + const Depsgraph &depsgraph, + const RegionView3D *rv3d, + Object &object, + Mesh *new_mesh); + } // namespace blender::ed::sculpt_paint diff --git a/source/blender/editors/sculpt_paint/sculpt.cc b/source/blender/editors/sculpt_paint/sculpt.cc index 4faad00384f..5fa0c551bd5 100644 --- a/source/blender/editors/sculpt_paint/sculpt.cc +++ b/source/blender/editors/sculpt_paint/sculpt.cc @@ -50,6 +50,7 @@ #include "BKE_image.hh" #include "BKE_key.hh" #include "BKE_layer.hh" +#include "BKE_lib_id.hh" #include "BKE_mesh.hh" #include "BKE_modifier.hh" #include "BKE_multires.hh" @@ -5169,6 +5170,51 @@ static void restore_from_undo_step_if_necessary(const Depsgraph &depsgraph, namespace blender::ed::sculpt_paint { +static void tag_mesh_positions_changed(Object &object, const bool use_pbvh_draw) +{ + Mesh &mesh = *static_cast(object.data); + + /* Various operations inside sculpt mode can cause either the #MeshRuntimeData or the entire + * Mesh to be changed (e.g. Undoing the very first operation after opening a file, performing + * remesh, etc). + * + * This isn't an ideal fix for the core issue here, but to mitigate the drastic performance + * falloff, we refreeze the cache before we do any operation that would tag this runtime + * cache as dirty. + * + * See #130636. */ + if (!mesh.runtime->corner_tris_cache.frozen) { + mesh.runtime->corner_tris_cache.freeze(); + } + + /* Updating mesh positions without marking caches dirty is generally not good, but since + * sculpt mode has special requirements and is expected to have sole ownership of the mesh it + * modifies, it's generally okay. */ + if (use_pbvh_draw) { + /* When drawing from bke::pbvh::Tree is used, vertex and face normals are updated + * later in #bke::pbvh::update_normals. However, we update the mesh's bounds eagerly here + * since they are trivial to access from the bke::pbvh::Tree. Updating the + * object's evaluated geometry bounding box is necessary because sculpt strokes don't cause + * an object reevaluation. */ + mesh.tag_positions_changed_no_normals(); + /* Sculpt mode does not use or recalculate face corner normals, so they are cleared. */ + mesh.runtime->corner_normals_cache.tag_dirty(); + } + else { + /* Drawing happens from the modifier stack evaluation result. + * Tag both coordinates and normals as modified, as both needed for proper drawing and the + * modifier stack is not guaranteed to tag normals for update. */ + mesh.tag_positions_changed(); + } + + if (const bke::pbvh::Tree *pbvh = bke::object::pbvh_get(object)) { + mesh.bounds_set_eager(bke::pbvh::bounds_get(*pbvh)); + if (object.runtime->bounds_eval) { + object.runtime->bounds_eval = mesh.bounds_min_max(); + } + } +} + void flush_update_step(bContext *C, UpdateType update_type) { Depsgraph &depsgraph = *CTX_data_depsgraph_pointer(C); @@ -5177,7 +5223,6 @@ void flush_update_step(bContext *C, UpdateType update_type) ARegion ®ion = *CTX_wm_region(C); MultiresModifierData *mmd = ss.multires.modifier; RegionView3D *rv3d = CTX_wm_region_view3d(C); - Mesh *mesh = static_cast(ob.data); bke::pbvh::Tree &pbvh = *bke::object::pbvh_get(ob); @@ -5236,44 +5281,7 @@ void flush_update_step(bContext *C, UpdateType update_type) if (update_type == UpdateType::Position && !ss.shapekey_active) { if (pbvh.type() == bke::pbvh::Type::Mesh) { - /* Various operations inside sculpt mode can cause either the #MeshRuntimeData or the entire - * Mesh to be changed (e.g. Undoing the very first operation after opening a file, performing - * remesh, etc). - * - * This isn't an ideal fix for the core issue here, but to mitigate the drastic performance - * falloff, we refreeze the cache before we do any operation that would tag this runtime - * cache as dirty. - * - * See #130636. - */ - if (!mesh->runtime->corner_tris_cache.frozen) { - mesh->runtime->corner_tris_cache.freeze(); - } - - /* Updating mesh positions without marking caches dirty is generally not good, but since - * sculpt mode has special requirements and is expected to have sole ownership of the mesh it - * modifies, it's generally okay. */ - if (use_pbvh_draw) { - /* When drawing from bke::pbvh::Tree is used, vertex and face normals are updated - * later in #bke::pbvh::update_normals. However, we update the mesh's bounds eagerly here - * since they are trivial to access from the bke::pbvh::Tree. Updating the - * object's evaluated geometry bounding box is necessary because sculpt strokes don't cause - * an object reevaluation. */ - mesh->tag_positions_changed_no_normals(); - /* Sculpt mode does not use or recalculate face corner normals, so they are cleared. */ - mesh->runtime->corner_normals_cache.tag_dirty(); - } - else { - /* Drawing happens from the modifier stack evaluation result. - * Tag both coordinates and normals as modified, as both needed for proper drawing and the - * modifier stack is not guaranteed to tag normals for update. */ - mesh->tag_positions_changed(); - } - - mesh->bounds_set_eager(bke::pbvh::bounds_get(pbvh)); - if (ob.runtime->bounds_eval) { - ob.runtime->bounds_eval = mesh->bounds_min_max(); - } + tag_mesh_positions_changed(ob, use_pbvh_draw); } } } @@ -5348,6 +5356,180 @@ void flush_update_done(const bContext *C, Object &ob, UpdateType update_type) } } +/* Replace an entire attribute using implicit sharing to avoid copies when possible. */ +static void replace_attribute(const bke::AttributeAccessor src_attributes, + const StringRef name, + const bke::AttrDomain domain, + const eCustomDataType data_type, + bke::MutableAttributeAccessor dst_attributes) +{ + dst_attributes.remove(name); + bke::GAttributeReader src = src_attributes.lookup(name, domain, data_type); + if (!src) { + return; + } + if (src.sharing_info && src.varray.is_span()) { + const bke::AttributeInitShared init(src.varray.get_internal_span().data(), *src.sharing_info); + dst_attributes.add(name, domain, data_type, init); + } + else { + const bke::AttributeInitVArray init(*src); + dst_attributes.add(name, domain, data_type, init); + } +} + +static bool attribute_matches(const bke::AttributeAccessor a, + const bke::AttributeAccessor b, + const StringRef name) +{ + const bke::GAttributeReader a_attr = a.lookup(name); + const bke::GAttributeReader b_attr = b.lookup(name); + if (!a_attr.sharing_info || !b_attr.sharing_info) { + return false; + } + return a_attr.sharing_info == b_attr.sharing_info; +} + +static bool topology_matches(const Mesh &a, const Mesh &b) +{ + if (a.verts_num != b.verts_num || a.edges_num != b.edges_num || a.faces_num != b.faces_num || + a.corners_num != b.corners_num) + { + return false; + } + if (a.runtime->face_offsets_sharing_info != b.runtime->face_offsets_sharing_info) { + return false; + } + const bke::AttributeAccessor a_attributes = a.attributes(); + const bke::AttributeAccessor b_attributes = b.attributes(); + if (!attribute_matches(a_attributes, b_attributes, ".edge_verts") || + !attribute_matches(a_attributes, b_attributes, ".corner_vert") || + !attribute_matches(a_attributes, b_attributes, ".corner_edge")) + { + return false; + } + return true; +} + +static void store_sculpt_entire_mesh(const wmOperator &op, + const Scene &scene, + Object &object, + Mesh *new_mesh) +{ + Mesh &mesh = *static_cast(object.data); + sculpt_paint::undo::geometry_begin(scene, object, &op); + BKE_mesh_nomain_to_mesh(new_mesh, &mesh, &object); + sculpt_paint::undo::geometry_end(object); + BKE_sculptsession_free_pbvh(object); +} + +void store_mesh_from_eval(const wmOperator &op, + const Scene &scene, + const Depsgraph &depsgraph, + const RegionView3D *rv3d, + Object &object, + Mesh *new_mesh) +{ + Mesh &mesh = *static_cast(object.data); + const bool changed_topology = !topology_matches(mesh, *new_mesh); + const bool use_pbvh_draw = BKE_sculptsession_use_pbvh_draw(&object, rv3d); + + if (changed_topology) { + store_sculpt_entire_mesh(op, scene, object, new_mesh); + } + else { + /* Detect attributes present in the new mesh which no longer match the original. */ + VectorSet changed_attributes; + new_mesh->attributes().foreach_attribute([&](const bke::AttributeIter &iter) { + if (ELEM(iter.name, ".edge_verts", ".corner_vert", ".corner_edge")) { + return; + } + const bke::GAttributeReader attribute = iter.get(); + if (attribute_matches(new_mesh->attributes(), mesh.attributes(), iter.name)) { + return; + } + changed_attributes.add(iter.name); + }); + /* Detect attributes that were removed in the new mesh. */ + mesh.attributes().foreach_attribute([&](const bke::AttributeIter &iter) { + if (!new_mesh->attributes().contains(iter.name)) { + changed_attributes.add(iter.name); + } + }); + + /* Try to use the few specialized sculpt undo types that result in better performance, mainly + * because redo avoids clearing the BVH, but also because some other updates can be skipped. */ + bke::pbvh::Tree &pbvh = *bke::object::pbvh_get(object); + IndexMaskMemory memory; + const IndexMask leaf_nodes = bke::pbvh::all_leaf_nodes(pbvh, memory); + if (changed_attributes.as_span() == Span{"position"}) { + undo::push_begin(scene, object, &op); + undo::push_nodes(depsgraph, object, leaf_nodes, undo::Type::Position); + undo::push_end(object); + CustomData_free_layer_named(&mesh.vert_data, "position", mesh.verts_num); + mesh.attributes_for_write().remove("position"); + const bke::AttributeReader position = new_mesh->attributes().lookup("position"); + if (position.sharing_info) { + /* Use lower level API to add the position attribute to avoid copying the array and to + * allow using #tag_positions_changed_no_normals instead of #tag_positions_changed (which + * would be called by the attribute API). */ + CustomData_add_layer_named_with_data( + &mesh.vert_data, + CD_PROP_FLOAT3, + const_cast(position.varray.get_internal_span().data()), + mesh.verts_num, + "position", + position.sharing_info); + } + else { + mesh.vert_positions_for_write().copy_from(VArraySpan(*position)); + } + + pbvh.tag_positions_changed(leaf_nodes); + pbvh.update_bounds(depsgraph, object); + tag_mesh_positions_changed(object, use_pbvh_draw); + BKE_mesh_copy_parameters(&mesh, new_mesh); + BKE_id_free(nullptr, new_mesh); + } + else if (changed_attributes.as_span() == Span{".sculpt_mask"}) { + undo::push_begin(scene, object, &op); + undo::push_nodes(depsgraph, object, leaf_nodes, undo::Type::Mask); + undo::push_end(object); + replace_attribute(new_mesh->attributes(), + ".sculpt_mask", + bke::AttrDomain::Point, + CD_PROP_FLOAT, + mesh.attributes_for_write()); + pbvh.tag_masks_changed(leaf_nodes); + BKE_mesh_copy_parameters(&mesh, new_mesh); + BKE_id_free(nullptr, new_mesh); + } + else if (changed_attributes.as_span() == Span{".sculpt_face_set"}) { + undo::push_begin(scene, object, &op); + undo::push_nodes(depsgraph, object, leaf_nodes, undo::Type::FaceSet); + undo::push_end(object); + replace_attribute(new_mesh->attributes(), + ".sculpt_face_set", + bke::AttrDomain::Face, + CD_PROP_INT32, + mesh.attributes_for_write()); + pbvh.tag_face_sets_changed(leaf_nodes); + BKE_mesh_copy_parameters(&mesh, new_mesh); + BKE_id_free(nullptr, new_mesh); + } + else { + /* Non-geometry-type sculpt undo steps can only handle a single change at a time. When + * multiple attributes or attributes that don't have their own undo type are changed, we're + * forced to fall back to the slower geometry undo type. */ + store_sculpt_entire_mesh(op, scene, object, new_mesh); + } + } + DEG_id_tag_update(&mesh.id, ID_RECALC_SHADING); + if (!use_pbvh_draw) { + DEG_id_tag_update(&mesh.id, ID_RECALC_GEOMETRY); + } +} + } // namespace blender::ed::sculpt_paint /* Returns whether the mouse/stylus is over the mesh (1)