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
This commit is contained in:
Hans Goudey 2024-07-01 16:12:02 +02:00 committed by Hans Goudey
parent 057fdf4224
commit bec350ba6e
2 changed files with 159 additions and 1 deletions

@ -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) &&

@ -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]