diff --git a/source/blender/editors/sculpt_paint/sculpt.cc b/source/blender/editors/sculpt_paint/sculpt.cc index fa7995a0624..c9ba22fb6d2 100644 --- a/source/blender/editors/sculpt_paint/sculpt.cc +++ b/source/blender/editors/sculpt_paint/sculpt.cc @@ -46,6 +46,7 @@ #include "BKE_colortools.hh" #include "BKE_context.hh" #include "BKE_customdata.hh" +#include "BKE_global.hh" #include "BKE_image.h" #include "BKE_key.hh" #include "BKE_layer.hh" @@ -5567,7 +5568,9 @@ static void sculpt_brush_stroke_init(bContext *C) SculptSession &ss = *CTX_data_active_object(C)->sculpt; const Brush *brush = BKE_paint_brush_for_read(&sd.paint); - view3d_operator_needs_opengl(C); + if (!G.background) { + view3d_operator_needs_opengl(C); + } sculpt_brush_init_tex(sd, ss); const bool needs_colors = SCULPT_tool_is_paint(brush->sculpt_tool) && diff --git a/tests/performance/tests/sculpt.py b/tests/performance/tests/sculpt.py new file mode 100644 index 00000000000..5ee4cbf3ac2 --- /dev/null +++ b/tests/performance/tests/sculpt.py @@ -0,0 +1,155 @@ +# SPDX-FileCopyrightText: 2024 Blender Authors +# +# SPDX-License-Identifier: Apache-2.0 + +import api + + +def set_view3d_context_override(context_override): + """ + Set context override to become the first viewport in the active workspace + + The `context_override` is expected to be a copy of an actual current context + obtained by `context.copy()` + """ + + for area in context_override["screen"].areas: + if area.type != 'VIEW_3D': + continue + for space in area.spaces: + if space.type != 'VIEW_3D': + continue + for region in area.regions: + if region.type != 'WINDOW': + continue + context_override["area"] = area + context_override["region"] = region + + +def prepare_sculpt_scene(context): + import bpy + from mathutils import Vector + """ + Prepare a clean state of the scene suitable for benchmarking + + It creates a high-res object and moves it to a sculpt mode. + """ + + # Ensure the current mode is object, as it might not be the always the case + # if the benchmark script is run from a non-clean state of the .blend file. + if context.object: + bpy.ops.object.mode_set(mode='OBJECT') + + # Delete all current objects from the scene. + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + bpy.ops.outliner.orphans_purge() + + group = bpy.data.node_groups.new("Test", 'GeometryNodeTree') + group.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') + group_output_node = group.nodes.new('NodeGroupOutput') + + size = 1500 + + grid_node = group.nodes.new('GeometryNodeMeshGrid') + grid_node.inputs["Size X"].default_value = 2.0 + grid_node.inputs["Size Y"].default_value = 2.0 + grid_node.inputs["Vertices X"].default_value = size + grid_node.inputs["Vertices Y"].default_value = size + + group.links.new(grid_node.outputs["Mesh"], group_output_node.inputs[0]) + + bpy.ops.mesh.primitive_plane_add(size=2, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) + + ob = context.object + md = ob.modifiers.new("Test", 'NODES') + md.node_group = group + + bpy.ops.object.modifier_apply(modifier="Test") + + bpy.ops.object.select_all(action='SELECT') + # Move the plane to the sculpt mode. + bpy.ops.object.mode_set(mode='SCULPT') + + +def generate_stroke(context): + """ + Generate stroke for the bpy.ops.sculpt.brush_stroke operator + + The generated stroke coves the full plane diagonal. + """ + from mathutils import Vector + + template = { + "name": "stroke", + "mouse": (0.0, 0.0), + "mouse_event": (0, 0), + "pen_flip": False, + "is_start": True, + "location": (0, 0, 0), + "pressure": 1.0, + "time": 1.0, + "size": 1.0, + "x_tilt": 0, + "y_tilt": 0 + } + + num_steps = 100 + start = Vector((-1, -1, 0)) + end = Vector((1, 1, 0)) + delta = (end - start) / (num_steps - 1) + + stroke = [] + for i in range(num_steps): + step = template.copy() + step["location"] = start + delta * i + # TODO: mouse and mouse_event? + stroke.append(step) + + return stroke + + +def _run(args): + import bpy + import time + context = bpy.context + + # Create an undo stack explicitly. This isn't created by default in background mode. + bpy.ops.ed.undo_push() + + prepare_sculpt_scene(context) + + context_override = context.copy() + set_view3d_context_override(context_override) + + with context.temp_override(**context_override): + start = time.time() + bpy.ops.sculpt.brush_stroke(stroke=generate_stroke(context_override)) + end = time.time() + + result = {'time': end - start} + # bpy.ops.wm.save_mainfile(filepath="/home/hans/Documents/test.blend") + return result + + +class SculptBrushTest(api.Test): + def __init__(self, filepath): + self.filepath = filepath + + def name(self): + return self.filepath.stem + + def category(self): + return "sculpt" + + def run(self, env, device_id): + args = {} + + result, _ = env.run_in_blender(_run, args, [self.filepath]) + + return result + + +def generate(env): + filepaths = env.find_blend_files('sculpt/*') + return [SculptBrushTest(filepath) for filepath in filepaths]