From bec350ba6e3ce4e532037a280bbf60652efb1acc Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Mon, 1 Jul 2024 16:12:02 +0200 Subject: [PATCH] Tests: Simple automated sculpt brush stroke performance test As an initial step to creating automated regression tests for sculpt brushes, make our existing performance test script into an automated performance test. The test uses the brush active in each file and runs the brush stroke operator on a large generated grid. The time is just for the brush evaluation, it doesn't include building the PBVH, drawing, etc. I'm not sure about the consequences of conditionally disabling `view3d_operator_needs_opengl`, but it was needed to make the test work in background mode. Pull Request: https://projects.blender.org/blender/blender/pulls/123148 --- source/blender/editors/sculpt_paint/sculpt.cc | 5 +- tests/performance/tests/sculpt.py | 155 ++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 tests/performance/tests/sculpt.py 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]