Add Chromium-only Blender WebEngine parity work
This commit is contained in:
3
blender-5.2.0/tests/performance/tests/__init__.py
Normal file
3
blender-5.2.0/tests/performance/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
50
blender-5.2.0/tests/performance/tests/animation.py
Normal file
50
blender-5.2.0/tests/performance/tests/animation.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# SPDX-FileCopyrightText: 2021-2022 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
|
||||
|
||||
def _run(args):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
elapsed_time = 0.0
|
||||
num_frames = 0
|
||||
|
||||
while elapsed_time < 10.0:
|
||||
scene = bpy.context.scene
|
||||
f = scene.frame_current + 1
|
||||
|
||||
if f >= scene.frame_end:
|
||||
f = scene.frame_start
|
||||
scene.frame_set(f)
|
||||
num_frames += 1
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
time_per_frame = elapsed_time / num_frames
|
||||
|
||||
result = {'time': time_per_frame}
|
||||
return result
|
||||
|
||||
|
||||
class AnimationTest(api.Test):
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return self.filepath.stem
|
||||
|
||||
def category(self):
|
||||
return "animation"
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
args = {}
|
||||
result, _ = env.run_in_blender(_run, args, [self.filepath])
|
||||
return result
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('animation/*')
|
||||
return [AnimationTest(filepath) for filepath in filepaths]
|
||||
42
blender-5.2.0/tests/performance/tests/blend_load.py
Normal file
42
blender-5.2.0/tests/performance/tests/blend_load.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# SPDX-FileCopyrightText: 2021-2022 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
|
||||
|
||||
def _run(filepath):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
# Load once to ensure it's cached by OS
|
||||
bpy.ops.wm.open_mainfile(filepath=filepath)
|
||||
bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
|
||||
|
||||
# Measure loading the second time
|
||||
start_time = time.time()
|
||||
bpy.ops.wm.open_mainfile(filepath=filepath)
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
result = {'time': elapsed_time}
|
||||
return result
|
||||
|
||||
|
||||
class BlendLoadTest(api.Test):
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return self.filepath.stem
|
||||
|
||||
def category(self):
|
||||
return "blend_load"
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
result, _ = env.run_in_blender(_run, str(self.filepath))
|
||||
return result
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('*/*')
|
||||
return [BlendLoadTest(filepath) for filepath in filepaths]
|
||||
195
blender-5.2.0/tests/performance/tests/bpy_rna.py
Normal file
195
blender-5.2.0/tests/performance/tests/bpy_rna.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# SPDX-FileCopyrightText: 2021-2022 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
|
||||
|
||||
def _run_id_instance_access(args):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
iterations = args["iterations"]
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(iterations):
|
||||
bpy.data.scenes[0]
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
result = {'time': elapsed_time}
|
||||
return result
|
||||
|
||||
|
||||
def _run_static_subdata_instance_access(args):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
iterations = args["iterations"]
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
sce = bpy.data.scenes[0]
|
||||
|
||||
for i in range(iterations):
|
||||
sce.render
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
result = {'time': elapsed_time}
|
||||
return result
|
||||
|
||||
|
||||
def _run_idproperty_access(args):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
iterations = args["iterations"]
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
sce = bpy.data.scenes[0]
|
||||
|
||||
sce["test"] = 3.14
|
||||
for i in range(iterations):
|
||||
sce["test"] += 0.001
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
result = {'time': elapsed_time}
|
||||
return result
|
||||
|
||||
|
||||
def _run_runtime_group_register_access(args):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
iterations = args["iterations"]
|
||||
do_register = args.get("do_register", False)
|
||||
do_access = args.get("do_access", False)
|
||||
do_get_set = args.get("do_get_set", False)
|
||||
do_transform = args.get("do_transform", False)
|
||||
property_type = args.get("property_type", 'IntProperty')
|
||||
property_definition_cb = getattr(bpy.props, property_type)
|
||||
|
||||
assert (not (do_get_set and do_transform))
|
||||
|
||||
# Define basic 'transform' callbacks to test setting value,
|
||||
# default to just setting untransformed value for 'unknown'/undefined property types.
|
||||
property_transform_set_cb = {
|
||||
'BoolProperty': lambda v: not v,
|
||||
'IntProperty': lambda v: v + 1,
|
||||
'FloatVectorProperty': lambda v: [v[2] + 1.0, v[0], v[1]],
|
||||
'StringProperty': lambda v: ("B" if (v and v[0] == "A") else "A") + v[1:],
|
||||
}.get(property_type, lambda v: v)
|
||||
|
||||
property_transform_get_cb = {
|
||||
'BoolProperty': lambda v: not v,
|
||||
'IntProperty': lambda v: v - 1,
|
||||
'FloatVectorProperty': lambda v: [v[0], v[1], v[2]],
|
||||
'StringProperty': lambda v: ("B" if (v and v[0] == "A") else "A") + v[1:],
|
||||
}.get(property_type, lambda v: v)
|
||||
|
||||
if do_get_set:
|
||||
class DummyGroup(bpy.types.PropertyGroup):
|
||||
dummy_prop: property_definition_cb(
|
||||
get=lambda self:
|
||||
self.bl_system_properties_get().get(
|
||||
"dummy_prop",
|
||||
(self.bl_rna.properties["dummy_prop"].default_array if
|
||||
self.bl_rna.properties["dummy_prop"].is_array else
|
||||
self.bl_rna.properties["dummy_prop"].default)),
|
||||
set=lambda self, val:
|
||||
self.bl_system_properties_get().__setitem__(
|
||||
"dummy_prop",
|
||||
val),
|
||||
)
|
||||
elif do_transform:
|
||||
class DummyGroup(bpy.types.PropertyGroup):
|
||||
dummy_prop: property_definition_cb(
|
||||
get_transform=lambda self, curr_v, is_set: property_transform_get_cb(curr_v),
|
||||
set_transform=lambda self, curr_v, new_v, is_set: property_transform_set_cb(curr_v),
|
||||
)
|
||||
else:
|
||||
class DummyGroup(bpy.types.PropertyGroup):
|
||||
dummy_prop: property_definition_cb()
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
sce = bpy.data.scenes[0]
|
||||
|
||||
# Test Registration & Unregistration.
|
||||
if do_register:
|
||||
for i in range(iterations):
|
||||
bpy.utils.register_class(DummyGroup)
|
||||
bpy.types.Scene.dummy_group = bpy.props.PointerProperty(type=DummyGroup)
|
||||
del bpy.types.Scene.dummy_group
|
||||
bpy.utils.unregister_class(DummyGroup)
|
||||
|
||||
if do_access:
|
||||
bpy.utils.register_class(DummyGroup)
|
||||
bpy.types.Scene.dummy_group = bpy.props.PointerProperty(type=DummyGroup)
|
||||
|
||||
if do_transform:
|
||||
for i in range(iterations):
|
||||
v = sce.dummy_group.dummy_prop
|
||||
sce.dummy_group.dummy_prop = v
|
||||
else:
|
||||
for i in range(iterations):
|
||||
v = sce.dummy_group.dummy_prop
|
||||
sce.dummy_group.dummy_prop = property_transform_set_cb(v)
|
||||
|
||||
del bpy.types.Scene.dummy_group
|
||||
bpy.utils.unregister_class(DummyGroup)
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
result = {'time': elapsed_time}
|
||||
return result
|
||||
|
||||
|
||||
class BPYRNATest(api.Test):
|
||||
def __init__(self, name, callback, iterations, args={}):
|
||||
self.name_ = name
|
||||
self.callback = callback
|
||||
self.iterations = iterations
|
||||
self.args = args
|
||||
|
||||
def name(self):
|
||||
return f"{self.name_} ({int(self.iterations / 1000)}k)"
|
||||
|
||||
def category(self):
|
||||
return "bpy_rna"
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
args = self.args
|
||||
args["iterations"] = self.iterations
|
||||
result, _ = env.run_in_blender(self.callback, args, ["--factory-startup"])
|
||||
return result
|
||||
|
||||
|
||||
def generate(env):
|
||||
return [
|
||||
BPYRNATest("ID Instance Access", _run_id_instance_access, 10000 * 1000),
|
||||
BPYRNATest("Static RNA Struct Instance Access", _run_static_subdata_instance_access, 10000 * 1000),
|
||||
BPYRNATest("IDProperty Access", _run_idproperty_access, 10000 * 1000),
|
||||
BPYRNATest("Py-Defined Struct Register", _run_runtime_group_register_access, 100 * 1000,
|
||||
{"do_register": True}),
|
||||
BPYRNATest("Py-Defined IntProperty Access", _run_runtime_group_register_access, 10000 * 1000,
|
||||
{"do_access": True, "property_type": 'IntProperty'}),
|
||||
BPYRNATest("Py-Defined IntProperty Custom Get/Set Access", _run_runtime_group_register_access, 100 * 1000,
|
||||
{"do_access": True, "do_get_set": True, "property_type": 'IntProperty'}),
|
||||
BPYRNATest("Py-Defined BoolProperty Custom Get/Set Access", _run_runtime_group_register_access, 100 * 1000,
|
||||
{"do_access": True, "do_get_set": True, "property_type": 'BoolProperty'}),
|
||||
BPYRNATest("Py-Defined FloatVectorProperty Custom Get/Set Access", _run_runtime_group_register_access, 100 * 1000,
|
||||
{"do_access": True, "do_get_set": True, "property_type": 'FloatVectorProperty'}),
|
||||
BPYRNATest("Py-Defined StringProperty Custom Get/Set Access", _run_runtime_group_register_access, 10 * 1000,
|
||||
{"do_access": True, "do_get_set": True, "property_type": 'StringProperty'}),
|
||||
BPYRNATest("Py-Defined BoolProperty Custom Transform Access", _run_runtime_group_register_access, 1000 * 1000,
|
||||
{"do_access": True, "do_transform": True, "property_type": 'BoolProperty'}),
|
||||
BPYRNATest("Py-Defined FloatVectorProperty Custom Transform Access", _run_runtime_group_register_access, 1000 * 1000,
|
||||
{"do_access": True, "do_transform": True, "property_type": 'FloatVectorProperty'}),
|
||||
BPYRNATest("Py-Defined StringProperty Custom Transform Access", _run_runtime_group_register_access, 1000 * 1000,
|
||||
{"do_access": True, "do_transform": True, "property_type": 'StringProperty'}),
|
||||
]
|
||||
63
blender-5.2.0/tests/performance/tests/compositor.py
Normal file
63
blender-5.2.0/tests/performance/tests/compositor.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
|
||||
|
||||
def _run(args):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
device_type, _ = (args['device_type'].split("-") + [""])[:2]
|
||||
scene = bpy.context.scene
|
||||
scene.render.compositor_device = ('CPU' if device_type == 'CPU' else 'GPU')
|
||||
|
||||
test_time_start = time.time()
|
||||
measured_times = []
|
||||
|
||||
min_measurements = 5
|
||||
max_measurements = 100
|
||||
timeout = 10
|
||||
|
||||
while True:
|
||||
start_time = time.time()
|
||||
bpy.ops.render.render()
|
||||
elapsed_time = time.time() - start_time
|
||||
measured_times.append(elapsed_time)
|
||||
|
||||
if len(measured_times) >= min_measurements and test_time_start + timeout < time.time():
|
||||
break
|
||||
if len(measured_times) >= max_measurements:
|
||||
break
|
||||
|
||||
average_time = sum(measured_times) / len(measured_times)
|
||||
result = {'time': average_time}
|
||||
return result
|
||||
|
||||
|
||||
class CompositorTest(api.Test):
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return self.filepath.stem
|
||||
|
||||
def category(self):
|
||||
return "compositor"
|
||||
|
||||
def use_device(self):
|
||||
return True
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
tokens = device_id.split('_')
|
||||
device_type = tokens[0]
|
||||
args = {'device_type': device_type}
|
||||
|
||||
result, _ = env.run_in_blender(_run, args, [self.filepath])
|
||||
return result
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('compositor/*')
|
||||
return [CompositorTest(filepath) for filepath in filepaths]
|
||||
135
blender-5.2.0/tests/performance/tests/cycles.py
Normal file
135
blender-5.2.0/tests/performance/tests/cycles.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# SPDX-FileCopyrightText: 2021-2022 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
|
||||
|
||||
def _run(args):
|
||||
import bpy
|
||||
|
||||
device_info = args['device_type'].split("-")
|
||||
device_type = device_info[0]
|
||||
|
||||
device_suffixes = device_info[1:]
|
||||
use_hwrt = "RT" in device_suffixes
|
||||
use_osl = "OSL" in device_suffixes
|
||||
|
||||
for suffix in device_suffixes:
|
||||
if suffix not in {"RT", "OSL"}:
|
||||
raise SystemExit(f"Unknown device type suffix {suffix}")
|
||||
|
||||
device_index = args['device_index']
|
||||
|
||||
scene = bpy.context.scene
|
||||
scene.render.engine = 'CYCLES'
|
||||
scene.render.filepath = args['render_filepath']
|
||||
scene.render.image_settings.media_type = 'IMAGE'
|
||||
scene.render.image_settings.file_format = 'PNG'
|
||||
scene.cycles.device = 'CPU' if device_type == 'CPU' else 'GPU'
|
||||
|
||||
if scene.cycles.use_adaptive_sampling:
|
||||
# Render samples specified in file, no other way to measure
|
||||
# adaptive sampling performance reliably.
|
||||
scene.cycles.time_limit = 0.0
|
||||
else:
|
||||
# Render for fixed amount of time so it's adaptive to the
|
||||
# machine and devices.
|
||||
scene.cycles.samples = 16384
|
||||
scene.cycles.time_limit = 10.0
|
||||
|
||||
if use_osl:
|
||||
scene.cycles.shading_system = True
|
||||
|
||||
if scene.cycles.device == 'GPU':
|
||||
# Enable specified GPU in preferences.
|
||||
prefs = bpy.context.preferences
|
||||
cprefs = prefs.addons['cycles'].preferences
|
||||
cprefs.compute_device_type = device_type
|
||||
devices = cprefs.get_devices_for_type(device_type)
|
||||
for device in devices:
|
||||
device.use = False
|
||||
|
||||
index = 0
|
||||
for device in devices:
|
||||
if device.type == device_type:
|
||||
if index == device_index:
|
||||
device.use = True
|
||||
break
|
||||
else:
|
||||
index += 1
|
||||
|
||||
cprefs.use_hiprt = use_hwrt
|
||||
cprefs.use_oneapirt = use_hwrt
|
||||
cprefs.metalrt = 'ON' if use_hwrt else 'OFF'
|
||||
|
||||
# Render
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class CyclesTest(api.Test):
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return self.filepath.stem
|
||||
|
||||
def category(self):
|
||||
return "cycles"
|
||||
|
||||
def use_device(self):
|
||||
return True
|
||||
|
||||
def supported_device_types(self):
|
||||
return [
|
||||
"CPU", "CPU-OSL", "CUDA", "OPTIX", "OPTIX-OSL", "ONEAPI", "ONEAPI-RT", "HIP", "HIP-RT", "METAL", "METAL-RT"
|
||||
]
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
tokens = device_id.split('_')
|
||||
device_type = tokens[0]
|
||||
device_index = int(tokens[1]) if len(tokens) > 1 else 0
|
||||
args = {'device_type': device_type,
|
||||
'device_index': device_index,
|
||||
'render_filepath': str(env.log_file.parent / (env.log_file.stem + '.png'))}
|
||||
|
||||
_, lines = env.run_in_blender(_run, args, ['--debug-cycles', '--verbose', '2', self.filepath])
|
||||
|
||||
# Parse render time from output
|
||||
prefix_time = "Render time (without synchronization): "
|
||||
prefix_memory = "Peak: "
|
||||
prefix_time_per_sample = "Average time per sample: "
|
||||
time = None
|
||||
time_per_sample = None
|
||||
memory = None
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
offset = line.find(prefix_time)
|
||||
if offset != -1:
|
||||
time = line[offset + len(prefix_time):]
|
||||
time = float(time)
|
||||
offset = line.find(prefix_time_per_sample)
|
||||
if offset != -1:
|
||||
time_per_sample = line[offset + len(prefix_time_per_sample):]
|
||||
time_per_sample = time_per_sample.split()[0]
|
||||
time_per_sample = float(time_per_sample)
|
||||
offset = line.find(prefix_memory)
|
||||
if offset != -1:
|
||||
memory = line[offset + len(prefix_memory):]
|
||||
memory = memory.split()[0].replace(',', '')
|
||||
memory = float(memory)
|
||||
|
||||
if time_per_sample:
|
||||
time = time_per_sample
|
||||
|
||||
if not (time and memory):
|
||||
raise Exception("Error parsing render time output")
|
||||
|
||||
return {'time': time, 'peak_memory': memory}
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('cycles/*')
|
||||
return [CyclesTest(filepath) for filepath in filepaths]
|
||||
161
blender-5.2.0/tests/performance/tests/eevee.py
Normal file
161
blender-5.2.0/tests/performance/tests/eevee.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# SPDX-FileCopyrightText: 2022 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import enum
|
||||
import time
|
||||
|
||||
|
||||
class RecordStage(enum.Enum):
|
||||
INIT = 0,
|
||||
WAIT_SHADERS = 1,
|
||||
WARMUP = 2,
|
||||
RECORD = 3,
|
||||
FINISHED = 4
|
||||
|
||||
|
||||
WARMUP_SECONDS = 3
|
||||
WARMUP_FRAMES = 10
|
||||
SHADER_FALLBACK_SECONDS = 60
|
||||
RECORD_PLAYBACK_ITER = 3
|
||||
MIN_NUM_FRAMES_TOTAL = 250
|
||||
LOG_KEY = "ANIMATION_PERFORMANCE: "
|
||||
|
||||
|
||||
def _run(args):
|
||||
import bpy
|
||||
|
||||
global record_stage
|
||||
record_stage = RecordStage.INIT
|
||||
|
||||
bpy.app.handlers.frame_change_post.append(frame_change_handler)
|
||||
bpy.ops.screen.animation_play()
|
||||
|
||||
|
||||
def frame_change_handler(scene):
|
||||
import bpy
|
||||
|
||||
global record_stage
|
||||
global frame_set_mode
|
||||
global start_time
|
||||
global start_record_time
|
||||
global start_warmup_time
|
||||
global warmup_frame
|
||||
global stop_record_time
|
||||
global playback_iteration
|
||||
global num_frames
|
||||
|
||||
if record_stage == RecordStage.INIT:
|
||||
screen = bpy.context.window_manager.windows[0].screen
|
||||
bpy.context.scene.sync_mode = 'NONE'
|
||||
frame_set_mode = False
|
||||
# Overwrite animation FPS limit set by .blend files.
|
||||
bpy.context.scene.render.fps = 1000
|
||||
|
||||
for area in screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
space = area.spaces[0]
|
||||
space.shading.type = 'RENDERED'
|
||||
space.overlay.show_overlays = False
|
||||
|
||||
start_time = time.perf_counter()
|
||||
record_stage = RecordStage.WAIT_SHADERS
|
||||
|
||||
elif record_stage == RecordStage.WAIT_SHADERS:
|
||||
shaders_compiled = False
|
||||
if hasattr(bpy.app, 'is_job_running'):
|
||||
shaders_compiled = not bpy.app.is_job_running("SHADER_COMPILATION")
|
||||
else:
|
||||
# Fallback when is_job_running doesn't exists by waiting for a time.
|
||||
shaders_compiled = time.perf_counter() - start_time > SHADER_FALLBACK_SECONDS
|
||||
|
||||
if shaders_compiled:
|
||||
start_warmup_time = time.perf_counter()
|
||||
warmup_frame = 0
|
||||
record_stage = RecordStage.WARMUP
|
||||
|
||||
elif record_stage == RecordStage.WARMUP:
|
||||
if frame_set_mode:
|
||||
# scene.frame_set results in a recursive call to frame_change_handler.
|
||||
# Avoid running into a RecursionError.
|
||||
return
|
||||
warmup_frame += 1
|
||||
# Check for two-stage shader compilation that can happen later than the first frame.
|
||||
if hasattr(bpy.app, 'is_job_running') and bpy.app.is_job_running("SHADER_COMPILATION"):
|
||||
record_stage = RecordStage.WAIT_SHADERS
|
||||
elif time.perf_counter() - start_warmup_time > WARMUP_SECONDS and warmup_frame > WARMUP_FRAMES:
|
||||
start_record_time = time.perf_counter()
|
||||
playback_iteration = 0
|
||||
num_frames = 0
|
||||
scene = bpy.context.scene
|
||||
frame_set_mode = True
|
||||
scene.frame_set(scene.frame_start)
|
||||
frame_set_mode = False
|
||||
record_stage = RecordStage.RECORD
|
||||
|
||||
elif record_stage == RecordStage.RECORD:
|
||||
current_time = time.perf_counter()
|
||||
scene = bpy.context.scene
|
||||
num_frames += 1
|
||||
if scene.frame_current == scene.frame_end:
|
||||
playback_iteration += 1
|
||||
|
||||
if playback_iteration >= RECORD_PLAYBACK_ITER and num_frames >= MIN_NUM_FRAMES_TOTAL:
|
||||
stop_record_time = current_time
|
||||
record_stage = RecordStage.FINISHED
|
||||
|
||||
elif record_stage == RecordStage.FINISHED:
|
||||
bpy.ops.screen.animation_cancel()
|
||||
elapsed_seconds = stop_record_time - start_record_time
|
||||
avg_frame_time = elapsed_seconds / num_frames
|
||||
fps = 1.0 / avg_frame_time
|
||||
print(f"{LOG_KEY}{{'fps': {fps} }}")
|
||||
bpy.app.handlers.frame_change_post.remove(frame_change_handler)
|
||||
bpy.ops.wm.quit_blender()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_run(None)
|
||||
|
||||
else:
|
||||
import api
|
||||
|
||||
class EeveeTest(api.Test):
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return self.filepath.stem
|
||||
|
||||
def category(self):
|
||||
return "eevee"
|
||||
|
||||
def use_device(self) -> bool:
|
||||
return True
|
||||
|
||||
def supported_device_types(self):
|
||||
return [
|
||||
"METAL", "VULKAN", "OPENGL",
|
||||
]
|
||||
|
||||
def use_background(self):
|
||||
return False
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
args = {}
|
||||
|
||||
blender_args = api.test.Test.blender_gpu_arguments(device_id, gpu_backend)
|
||||
blender_args.append(self.filepath)
|
||||
|
||||
_, log = env.run_in_blender(_run, args, blender_args, foreground=True)
|
||||
for line in log:
|
||||
if line.startswith(LOG_KEY):
|
||||
result_str = line[len(LOG_KEY):]
|
||||
result = eval(result_str)
|
||||
return result
|
||||
|
||||
raise Exception("No playback performance result found in log.")
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('eevee/*')
|
||||
return [EeveeTest(filepath) for filepath in filepaths]
|
||||
65
blender-5.2.0/tests/performance/tests/geometry_nodes.py
Normal file
65
blender-5.2.0/tests/performance/tests/geometry_nodes.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# SPDX-FileCopyrightText: 2022-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
|
||||
|
||||
def _run(args):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
# Evaluate objects once first, to avoid any possible lazy evaluation later.
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
test_time_start = time.time()
|
||||
measured_times = []
|
||||
|
||||
min_measurements = 5
|
||||
max_measurements = 100
|
||||
timeout = 5
|
||||
|
||||
while True:
|
||||
# Tag all objects with geometry nodes modifiers to be recalculated.
|
||||
for ob in bpy.context.view_layer.objects:
|
||||
for modifier in ob.modifiers:
|
||||
if modifier.type == 'NODES':
|
||||
ob.update_tag()
|
||||
break
|
||||
|
||||
start_time = time.time()
|
||||
bpy.context.view_layer.update()
|
||||
elapsed_time = time.time() - start_time
|
||||
measured_times.append(elapsed_time)
|
||||
|
||||
if len(measured_times) >= min_measurements and test_time_start + timeout < time.time():
|
||||
break
|
||||
if len(measured_times) >= max_measurements:
|
||||
break
|
||||
|
||||
average_time = sum(measured_times) / len(measured_times)
|
||||
result = {'time': average_time}
|
||||
return result
|
||||
|
||||
|
||||
class GeometryNodesTest(api.Test):
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return self.filepath.stem
|
||||
|
||||
def category(self):
|
||||
return "geometry_nodes"
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
args = {}
|
||||
|
||||
result, _ = env.run_in_blender(_run, args, [self.filepath])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('geometry_nodes/*')
|
||||
return [GeometryNodesTest(filepath) for filepath in filepaths]
|
||||
132
blender-5.2.0/tests/performance/tests/grease_pencil.py
Normal file
132
blender-5.2.0/tests/performance/tests/grease_pencil.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
import enum
|
||||
import time
|
||||
|
||||
|
||||
class RecordStage(enum.Enum):
|
||||
INIT = 0,
|
||||
WARMUP = 1,
|
||||
RECORD = 2,
|
||||
FINISHED = 3
|
||||
|
||||
|
||||
WARMUP_SECONDS = 4
|
||||
WARMUP_FRAMES = 10
|
||||
RECORD_PLAYBACK_ITER = 3
|
||||
MIN_NUM_FRAMES_TOTAL = 250
|
||||
LOG_KEY = "VIEWPORT_PERFORMANCE: "
|
||||
|
||||
|
||||
def _run(args):
|
||||
import bpy
|
||||
|
||||
global record_stage
|
||||
record_stage = RecordStage.INIT
|
||||
|
||||
bpy.app.handlers.frame_change_post.append(frame_change_handler)
|
||||
bpy.ops.screen.animation_play()
|
||||
|
||||
|
||||
def frame_change_handler(scene):
|
||||
import bpy
|
||||
|
||||
global record_stage
|
||||
global frame_set_mode
|
||||
global start_record_time
|
||||
global start_warmup_time
|
||||
global warmup_frame
|
||||
global stop_record_time
|
||||
global playback_iteration
|
||||
global num_frames
|
||||
|
||||
if record_stage == RecordStage.INIT:
|
||||
bpy.context.scene.sync_mode = 'NONE'
|
||||
frame_set_mode = False
|
||||
# Overwrite animation FPS limit set by .blend files.
|
||||
bpy.context.scene.render.fps = 1000
|
||||
|
||||
start_warmup_time = time.perf_counter()
|
||||
warmup_frame = 0
|
||||
record_stage = RecordStage.WARMUP
|
||||
|
||||
elif record_stage == RecordStage.WARMUP:
|
||||
if frame_set_mode:
|
||||
# scene.frame_set results in a recursive call to frame_change_handler.
|
||||
# Avoid running into a RecursionError.
|
||||
return
|
||||
warmup_frame += 1
|
||||
if time.perf_counter() - start_warmup_time > WARMUP_SECONDS and warmup_frame > WARMUP_FRAMES:
|
||||
start_record_time = time.perf_counter()
|
||||
playback_iteration = 0
|
||||
num_frames = 0
|
||||
scene = bpy.context.scene
|
||||
frame_set_mode = True
|
||||
scene.frame_set(scene.frame_start)
|
||||
frame_set_mode = False
|
||||
record_stage = RecordStage.RECORD
|
||||
|
||||
elif record_stage == RecordStage.RECORD:
|
||||
current_time = time.perf_counter()
|
||||
scene = bpy.context.scene
|
||||
num_frames += 1
|
||||
if scene.frame_current == scene.frame_end:
|
||||
playback_iteration += 1
|
||||
|
||||
if playback_iteration >= RECORD_PLAYBACK_ITER and num_frames >= MIN_NUM_FRAMES_TOTAL:
|
||||
stop_record_time = current_time
|
||||
record_stage = RecordStage.FINISHED
|
||||
|
||||
elif record_stage == RecordStage.FINISHED:
|
||||
bpy.ops.screen.animation_cancel()
|
||||
elapsed_seconds = stop_record_time - start_record_time
|
||||
avg_frame_time = elapsed_seconds / num_frames
|
||||
fps = 1.0 / avg_frame_time
|
||||
print(f"{LOG_KEY}{{'fps': {fps} }}")
|
||||
bpy.app.handlers.frame_change_post.remove(frame_change_handler)
|
||||
bpy.ops.wm.quit_blender()
|
||||
|
||||
|
||||
class GreasePencilTest(api.Test):
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return self.filepath.stem
|
||||
|
||||
def category(self):
|
||||
return "grease_pencil"
|
||||
|
||||
def use_device(self) -> bool:
|
||||
return True
|
||||
|
||||
def supported_device_types(self):
|
||||
return [
|
||||
"METAL", "VULKAN", "OPENGL"
|
||||
]
|
||||
|
||||
def use_background(self):
|
||||
return False
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
args = {}
|
||||
|
||||
blender_args = api.test.Test.blender_gpu_arguments(device_id, gpu_backend)
|
||||
blender_args.append(self.filepath)
|
||||
|
||||
_, log = env.run_in_blender(_run, args, blender_args, foreground=True)
|
||||
for line in log:
|
||||
if line.startswith(LOG_KEY):
|
||||
result_str = line[len(LOG_KEY):]
|
||||
result = eval(result_str)
|
||||
return result
|
||||
|
||||
raise Exception("No playback performance result found in log.")
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('grease_pencil/*')
|
||||
return [GreasePencilTest(filepath) for filepath in filepaths]
|
||||
382
blender-5.2.0/tests/performance/tests/sculpt.py
Normal file
382
blender-5.2.0/tests/performance/tests/sculpt.py
Normal file
@@ -0,0 +1,382 @@
|
||||
# SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
import enum
|
||||
import pathlib
|
||||
|
||||
|
||||
class SculptMode(enum.IntEnum):
|
||||
MESH = 1
|
||||
MULTIRES = 2
|
||||
DYNTOPO = 3
|
||||
|
||||
|
||||
class BrushType(enum.Enum):
|
||||
DRAW = "Draw"
|
||||
CLAY_STRIPS = "Clay Strips"
|
||||
SMOOTH = "Smooth"
|
||||
|
||||
|
||||
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: any, mode: SculptMode, subdivision_level=3):
|
||||
"""
|
||||
Prepare a clean state of the scene suitable for benchmarking
|
||||
|
||||
It creates a high-res object and moves it to a sculpt mode.
|
||||
|
||||
For dyntopo & normal mesh sculpting, we create a grid with 2.2M vertices.
|
||||
For multires sculpting, we create a grid with 22k vertices - with a multires
|
||||
modifier set to level 3, this results in an equivalent number of 2.2M vertices
|
||||
inside sculpt mode.
|
||||
"""
|
||||
import bpy
|
||||
|
||||
# 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')
|
||||
|
||||
if mode == SculptMode.MESH:
|
||||
size = 1500
|
||||
elif mode == SculptMode.MULTIRES:
|
||||
size = 150
|
||||
elif mode == SculptMode.DYNTOPO:
|
||||
size = 500
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
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')
|
||||
|
||||
if mode == SculptMode.MULTIRES:
|
||||
bpy.ops.object.subdivision_set(level=subdivision_level)
|
||||
elif mode == SculptMode.DYNTOPO:
|
||||
bpy.ops.sculpt.dynamic_topology_toggle()
|
||||
|
||||
|
||||
def prepare_brush(context: any, brush_type: BrushType):
|
||||
"""Activates and sets common brush settings"""
|
||||
import bpy
|
||||
bpy.ops.brush.asset_activate(
|
||||
asset_library_type='ESSENTIALS',
|
||||
relative_asset_identifier='brushes/essentials_brushes-mesh_sculpt.blend/Brush/' +
|
||||
brush_type.value)
|
||||
|
||||
# Reduce the brush strength to avoid deforming the mesh too much and influencing multiple strokes
|
||||
context.tool_settings.sculpt.brush.strength = 0.1
|
||||
|
||||
|
||||
def generate_stroke(context):
|
||||
"""
|
||||
Generate stroke for the bpy.ops.sculpt.brush_stroke operator
|
||||
|
||||
The generated stroke coves the full plane diagonal.
|
||||
"""
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
template = {
|
||||
"name": "stroke",
|
||||
"mouse": (0.0, 0.0),
|
||||
"mouse_event": (0, 0),
|
||||
"is_start": True,
|
||||
"location": (0, 0, 0),
|
||||
"pressure": 1.0,
|
||||
"time": 1.0,
|
||||
"size": 1.0,
|
||||
"x_tilt": 0,
|
||||
"y_tilt": 0
|
||||
}
|
||||
|
||||
version = bpy.app.version
|
||||
if version[0] <= 4 and version[1] <= 3:
|
||||
template["pen_flip"] = False
|
||||
|
||||
num_steps = 100
|
||||
start = Vector((context['area'].width, context['area'].height))
|
||||
end = Vector((0, 0))
|
||||
delta = (end - start) / (num_steps - 1)
|
||||
|
||||
stroke = []
|
||||
for i in range(num_steps):
|
||||
step = template.copy()
|
||||
step["mouse_event"] = start + delta * i
|
||||
stroke.append(step)
|
||||
|
||||
return stroke
|
||||
|
||||
|
||||
def _run_brush_test(args: dict):
|
||||
import bpy
|
||||
import time
|
||||
context = bpy.context
|
||||
|
||||
timeout = 10
|
||||
total_time_start = time.time()
|
||||
|
||||
# Create an undo stack explicitly. This isn't created by default in background mode.
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
prepare_brush(context, args['brush_type'])
|
||||
|
||||
min_measurements = 5
|
||||
max_measurements = 100
|
||||
measurements = []
|
||||
while True:
|
||||
prepare_sculpt_scene(context, args['mode'])
|
||||
context_override = context.copy()
|
||||
set_view3d_context_override(context_override)
|
||||
with context.temp_override(**context_override):
|
||||
if args.get('spatial_reorder', False):
|
||||
bpy.ops.mesh.reorder_vertices_spatial()
|
||||
bpy.ops.ed.undo_push()
|
||||
start = time.time()
|
||||
bpy.ops.sculpt.brush_stroke(stroke=generate_stroke(context_override), override_location=True)
|
||||
bpy.ops.ed.undo_push()
|
||||
measurements.append(time.time() - start)
|
||||
memory_info = bpy.app.memory_usage_undo()
|
||||
if len(measurements) >= min_measurements and (time.time() - total_time_start) > timeout:
|
||||
break
|
||||
if len(measurements) >= max_measurements:
|
||||
break
|
||||
|
||||
return {'time': sum(measurements) / len(measurements), 'memory': memory_info}
|
||||
|
||||
|
||||
def _run_bvh_test(args: dict):
|
||||
import bpy
|
||||
import time
|
||||
context = bpy.context
|
||||
|
||||
timeout = 10
|
||||
total_time_start = time.time()
|
||||
|
||||
# Create an undo stack explicitly. This isn't created by default in background mode.
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
min_measurements = 5
|
||||
max_measurements = 100
|
||||
|
||||
measurements = []
|
||||
while True:
|
||||
prepare_sculpt_scene(context, args['mode'])
|
||||
context_override = context.copy()
|
||||
set_view3d_context_override(context_override)
|
||||
with context.temp_override(**context_override):
|
||||
if args.get('spatial_reorder', False):
|
||||
bpy.ops.mesh.reorder_vertices_spatial()
|
||||
start = time.time()
|
||||
bpy.ops.sculpt.optimize()
|
||||
measurements.append(time.time() - start)
|
||||
|
||||
if len(measurements) >= min_measurements and (time.time() - total_time_start) > timeout:
|
||||
break
|
||||
if len(measurements) >= max_measurements:
|
||||
break
|
||||
|
||||
return sum(measurements) / len(measurements)
|
||||
|
||||
|
||||
def _run_subdivide_test(_args: dict):
|
||||
import bpy
|
||||
import time
|
||||
context = bpy.context
|
||||
|
||||
timeout = 10
|
||||
total_time_start = time.time()
|
||||
|
||||
# Create an undo stack explicitly. This isn't created by default in background mode.
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
min_measurements = 5
|
||||
max_measurements = 100
|
||||
|
||||
measurements = []
|
||||
while True:
|
||||
prepare_sculpt_scene(context, SculptMode.MULTIRES, subdivision_level=2)
|
||||
context_override = context.copy()
|
||||
set_view3d_context_override(context_override)
|
||||
with context.temp_override(**context_override):
|
||||
start = time.time()
|
||||
bpy.ops.object.multires_subdivide(modifier="Multires")
|
||||
measurements.append(time.time() - start)
|
||||
|
||||
if len(measurements) >= min_measurements and (time.time() - total_time_start) > timeout:
|
||||
break
|
||||
if len(measurements) >= max_measurements:
|
||||
break
|
||||
|
||||
return sum(measurements) / len(measurements)
|
||||
|
||||
|
||||
class SculptBrushTest(api.Test):
|
||||
def __init__(self, filepath: pathlib.Path, mode: SculptMode, brush_type: BrushType):
|
||||
self.filepath = filepath
|
||||
self.mode = mode
|
||||
self.brush_type = brush_type
|
||||
|
||||
def name(self):
|
||||
return "{}_{}".format(self.mode.name.lower(), self.brush_type.name.lower())
|
||||
|
||||
def category(self):
|
||||
return "sculpt"
|
||||
|
||||
def run(self, env, _device_id, _gpu_backend):
|
||||
args = {
|
||||
'mode': self.mode,
|
||||
'brush_type': self.brush_type,
|
||||
'spatial_reorder': False,
|
||||
}
|
||||
|
||||
result, _ = env.run_in_blender(_run_brush_test, args, [self.filepath])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class SculptBrushAfterSpatialReorderingTest(api.Test):
|
||||
def __init__(self, filepath: pathlib.Path, mode: SculptMode, brush_type: BrushType):
|
||||
self.filepath = filepath
|
||||
self.mode = mode
|
||||
self.brush_type = brush_type
|
||||
|
||||
def name(self):
|
||||
return "{}_{}_{}".format(self.mode.name.lower(), self.brush_type.name.lower(), "after_reordering")
|
||||
|
||||
def category(self):
|
||||
return "sculpt"
|
||||
|
||||
def run(self, env, _device_id, _gpu_backend):
|
||||
args = {
|
||||
'mode': self.mode,
|
||||
'brush_type': self.brush_type,
|
||||
'spatial_reorder': True,
|
||||
}
|
||||
|
||||
result, _ = env.run_in_blender(_run_brush_test, args, [self.filepath])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class SculptRebuildBVHTest(api.Test):
|
||||
def __init__(self, filepath: pathlib.Path, mode: SculptMode):
|
||||
self.filepath = filepath
|
||||
self.mode = mode
|
||||
|
||||
def name(self):
|
||||
return "{}_rebuild_bvh".format(self.mode.name.lower())
|
||||
|
||||
def category(self):
|
||||
return "sculpt"
|
||||
|
||||
def run(self, env, _device_id, _gpu_backend):
|
||||
args = {
|
||||
'mode': self.mode,
|
||||
'spatial_reorder': False,
|
||||
}
|
||||
|
||||
result, _ = env.run_in_blender(_run_bvh_test, args, [self.filepath])
|
||||
|
||||
return {'time': result}
|
||||
|
||||
|
||||
class SculptRebuildSpatialBVHTest(api.Test):
|
||||
def __init__(self, filepath: pathlib.Path, mode: SculptMode):
|
||||
self.filepath = filepath
|
||||
self.mode = mode
|
||||
|
||||
def name(self):
|
||||
return "{}_spatial_rebuild_bvh".format(self.mode.name.lower())
|
||||
|
||||
def category(self):
|
||||
return "sculpt"
|
||||
|
||||
def run(self, env, _device_id, _gpu_backend):
|
||||
args = {
|
||||
'mode': self.mode,
|
||||
'spatial_reorder': True,
|
||||
}
|
||||
|
||||
result, _ = env.run_in_blender(_run_bvh_test, args, [self.filepath])
|
||||
|
||||
return {'time': result}
|
||||
|
||||
|
||||
class SculptMultiresSubdivideTest(api.Test):
|
||||
def __init__(self, filepath: pathlib.Path):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return "multires_subdivide_2_to_3"
|
||||
|
||||
def category(self):
|
||||
return "sculpt"
|
||||
|
||||
def run(self, env, _device_id, _gpu_backend):
|
||||
result, _ = env.run_in_blender(_run_subdivide_test, {}, [self.filepath])
|
||||
|
||||
return {'time': result}
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('sculpt/*')
|
||||
# For now, we only expect there to ever be a single file to use as the basis for generating other brush tests
|
||||
assert len(filepaths) == 1
|
||||
|
||||
brush_tests = [SculptBrushTest(filepaths[0], mode, brush_type) for mode in SculptMode for brush_type in BrushType]
|
||||
brush_tests_after_reordering = [
|
||||
SculptBrushAfterSpatialReorderingTest(
|
||||
filepaths[0],
|
||||
SculptMode.MESH,
|
||||
brush_type)for brush_type in BrushType]
|
||||
bvh_tests = [SculptRebuildBVHTest(filepaths[0], mode) for mode in SculptMode]
|
||||
spatial_bvh_tests = [SculptRebuildSpatialBVHTest(filepaths[0], SculptMode.MESH)]
|
||||
subdivision_tests = [SculptMultiresSubdivideTest(filepaths[0])]
|
||||
return brush_tests + brush_tests_after_reordering + bvh_tests + spatial_bvh_tests + subdivision_tests
|
||||
216
blender-5.2.0/tests/performance/tests/texture_paint.py
Normal file
216
blender-5.2.0/tests/performance/tests/texture_paint.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
import enum
|
||||
import pathlib
|
||||
|
||||
|
||||
class MeshType(enum.IntEnum):
|
||||
CUBE = 0
|
||||
MONKEY = 1
|
||||
SUBDIV_3_MONKEY = 2
|
||||
|
||||
|
||||
class DataType(enum.IntEnum):
|
||||
BYTE = 0
|
||||
FLOAT = 1
|
||||
|
||||
|
||||
DIMENSIONS = [1024, 4096]
|
||||
|
||||
|
||||
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_scene(context: any, object: MeshType, image_dimension: int, data_type: DataType):
|
||||
"""
|
||||
Prepare a clean state of the scene suitable for benchmarking
|
||||
"""
|
||||
import bpy
|
||||
|
||||
bpy.context.preferences.experimental.use_sculpt_texture_paint = True
|
||||
|
||||
# Ensure the current mode is object, as it might not be 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()
|
||||
|
||||
if object == MeshType.MONKEY:
|
||||
bpy.ops.mesh.primitive_monkey_add(size=2, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
elif object == MeshType.CUBE:
|
||||
bpy.ops.mesh.primitive_cube_add(size=2, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
elif object == MeshType.SUBDIV_3_MONKEY:
|
||||
bpy.ops.mesh.primitive_monkey_add(size=2, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
bpy.ops.object.subdivision_set(level=3, relative=False, ensure_modifier=True)
|
||||
bpy.ops.object.modifier_apply(modifier="Subdivision")
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
context_override = context.copy()
|
||||
set_view3d_context_override(context_override)
|
||||
with context.temp_override(**context_override):
|
||||
bpy.ops.view3d.view_axis(type='FRONT')
|
||||
bpy.ops.view3d.view_selected()
|
||||
bpy.ops.object.mode_set(mode='SCULPT')
|
||||
|
||||
is_float_image = data_type == DataType.FLOAT
|
||||
|
||||
bpy.ops.paint.add_texture_paint_slot(
|
||||
type='BASE_COLOR',
|
||||
slot_type='IMAGE',
|
||||
name="Untitled",
|
||||
color=(
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0),
|
||||
width=image_dimension,
|
||||
height=image_dimension,
|
||||
alpha=True,
|
||||
generated_type='BLANK',
|
||||
float=is_float_image)
|
||||
|
||||
|
||||
def prepare_brush():
|
||||
import bpy
|
||||
bpy.ops.brush.asset_activate(
|
||||
asset_library_type='ESSENTIALS',
|
||||
relative_asset_identifier="brushes/essentials_brushes-mesh_sculpt.blend/Brush/Paint Hard")
|
||||
|
||||
|
||||
def generate_stroke(context):
|
||||
"""
|
||||
Generate stroke for the bpy.ops.sculpt.brush_stroke operator
|
||||
|
||||
The generated stroke coves the full plane diagonal.
|
||||
"""
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
template = {
|
||||
"name": "stroke",
|
||||
"mouse": (0.0, 0.0),
|
||||
"mouse_event": (0, 0),
|
||||
"is_start": True,
|
||||
"location": (0, 0, 0),
|
||||
"pressure": 1.0,
|
||||
"time": 1.0,
|
||||
"size": 1.0,
|
||||
"x_tilt": 0,
|
||||
"y_tilt": 0
|
||||
}
|
||||
|
||||
version = bpy.app.version
|
||||
if version[0] <= 4 and version[1] <= 3:
|
||||
template["pen_flip"] = False
|
||||
|
||||
num_steps = 100
|
||||
start = Vector((context["area"].width, context["area"].height))
|
||||
end = Vector((0, 0))
|
||||
delta = (end - start) / (num_steps - 1)
|
||||
|
||||
stroke = []
|
||||
for i in range(num_steps):
|
||||
step = template.copy()
|
||||
step["mouse_event"] = start + delta * i
|
||||
stroke.append(step)
|
||||
|
||||
return stroke
|
||||
|
||||
|
||||
def _run_brush_test(args: dict):
|
||||
import bpy
|
||||
import time
|
||||
|
||||
# This test can only run in alpha, for now, due to the texture paint mode being an experimental feature
|
||||
if bpy.app.version_cycle != 'alpha':
|
||||
return {"time": 0.0}
|
||||
|
||||
context = bpy.context
|
||||
|
||||
timeout = 10
|
||||
total_time_start = time.time()
|
||||
|
||||
# Create an undo stack explicitly. This isn't created by default in background mode.
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
prepare_brush()
|
||||
|
||||
min_measurements = 5
|
||||
max_measurements = 100
|
||||
measurements = []
|
||||
while True:
|
||||
prepare_scene(context, args["object_type"], args["dimension"], args["data_type"])
|
||||
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), override_location=True)
|
||||
bpy.ops.ed.undo_push()
|
||||
measurements.append(time.time() - start)
|
||||
if len(measurements) >= min_measurements and (time.time() - total_time_start) > timeout:
|
||||
break
|
||||
if len(measurements) >= max_measurements:
|
||||
break
|
||||
|
||||
return {"time": sum(measurements) / len(measurements)}
|
||||
|
||||
|
||||
class TexturePaintBrushTest(api.Test):
|
||||
def __init__(self, filepath: pathlib.Path, object_type: MeshType, dimension: int, data_type: DataType):
|
||||
self.filepath = filepath
|
||||
self.object_type = object_type
|
||||
self.dimension = dimension
|
||||
self.data_type = data_type
|
||||
|
||||
def name(self):
|
||||
return "{}_{}_{}".format(self.object_type.name.lower(), self.data_type.name.lower(), self.dimension)
|
||||
|
||||
def category(self):
|
||||
return "texture_paint"
|
||||
|
||||
def run(self, env, _device_id, _gpu_backend):
|
||||
args = {
|
||||
'object_type': self.object_type,
|
||||
'dimension': self.dimension,
|
||||
'data_type': self.data_type
|
||||
}
|
||||
|
||||
result, _ = env.run_in_blender(_run_brush_test, args, [self.filepath])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('texture_paint/*')
|
||||
# For now, we only expect there to ever be a single file to use as the basis for generating other brush tests
|
||||
assert len(filepaths) == 1
|
||||
|
||||
brush_tests = [TexturePaintBrushTest(filepaths[0], object_type, dimension, data_type)
|
||||
for object_type in MeshType for dimension in DIMENSIONS for data_type in DataType]
|
||||
return brush_tests
|
||||
138
blender-5.2.0/tests/performance/tests/undo.py
Normal file
138
blender-5.2.0/tests/performance/tests/undo.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
|
||||
|
||||
# Validate performances when one heavy geometry is in the scene:
|
||||
# - Writing and loading memfile undo steps of changes in the heavy geometry itself.
|
||||
# - Writing and loading memfile undo steps of changes to the object using the heavy geometry.
|
||||
def _run_heavy_geometry(dummy_):
|
||||
import bpy
|
||||
import mathutils
|
||||
import time
|
||||
|
||||
ob = bpy.data.objects["Cube"]
|
||||
assert (bpy.context.object == ob)
|
||||
ob.modifiers.new(name="Subsurf", type='SUBSURF').levels = 9
|
||||
bpy.ops.object.modifier_apply(modifier="Subsurf")
|
||||
|
||||
start_time = time.time()
|
||||
# NOTE: The first undo push is necessary to be able to undo, since it creates the
|
||||
# initial state for memfile undo (it is not initialized by default in background mode).
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
# Empty undo step.
|
||||
bpy.ops.ed.undo_push()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.redo()
|
||||
|
||||
# Object-modified undo step.
|
||||
ob = bpy.data.objects["Cube"]
|
||||
assert (bpy.context.object == ob)
|
||||
ob.location.x += 1.0
|
||||
bpy.ops.ed.undo_push()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.redo()
|
||||
bpy.ops.ed.redo()
|
||||
|
||||
# Mesh-modified undo step.
|
||||
ob = bpy.data.objects["Cube"]
|
||||
assert (bpy.context.object == ob)
|
||||
ob.data.transform(mathutils.Matrix.Translation((1, 0, 0)))
|
||||
bpy.ops.ed.undo_push()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.redo()
|
||||
bpy.ops.ed.redo()
|
||||
bpy.ops.ed.redo()
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
result = {'time': elapsed_time, 'undo_stack_memory': getattr(bpy.app, "memory_usage_undo", lambda: 0)()}
|
||||
return result
|
||||
|
||||
|
||||
class BlendUndoMemfileHeavyGeometryTest(api.Test):
|
||||
def name(self):
|
||||
return "undo_memfile_heavy_mesh_geometry"
|
||||
|
||||
def category(self):
|
||||
return "undo"
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
result, _ = env.run_in_blender(_run_heavy_geometry, {}, ["--factory-startup"])
|
||||
return result
|
||||
|
||||
|
||||
# Validate performances when an extremely large amount of small independant blocks of data are present.
|
||||
# This is generating many IDProperties in an ID.
|
||||
def _run_many_bheads_and_pointers(args):
|
||||
import bpy
|
||||
import mathutils
|
||||
import time
|
||||
|
||||
num_props_per_level = args["num_props_per_level"]
|
||||
num_levels = args["num_levels"]
|
||||
|
||||
# Recursively generate idproperties containing other idproperties.
|
||||
def gen_idprops(id_prop_owner, num_props_per_level, num_levels, curr_level):
|
||||
if curr_level == num_levels:
|
||||
for i in range(num_props_per_level):
|
||||
id_prop_owner[str(i)] = i
|
||||
else:
|
||||
for i in range(num_props_per_level):
|
||||
id_prop_owner[str(i)] = {}
|
||||
gen_idprops(id_prop_owner[str(i)], num_props_per_level, num_levels, curr_level + 1)
|
||||
|
||||
ob = bpy.data.objects["Cube"]
|
||||
|
||||
# Generate many IDProps in the object.
|
||||
ob['test_idproperties'] = {}
|
||||
gen_idprops(ob['test_idproperties'], num_props_per_level, num_levels, 1)
|
||||
|
||||
start_time = time.time()
|
||||
# NOTE: The first undo push is necessary to be able to undo, since it creates the
|
||||
# initial state for memfile undo (it is not initialized by default in background mode).
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
# Empty undo step.
|
||||
bpy.ops.ed.undo_push()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.redo()
|
||||
|
||||
# Object-modified undo step.
|
||||
ob = bpy.data.objects["Cube"]
|
||||
ob['test_idproperties_empty'] = {}
|
||||
bpy.ops.ed.undo_push()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.redo()
|
||||
bpy.ops.ed.redo()
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
result = {'time': elapsed_time, 'undo_stack_memory': getattr(bpy.app, "memory_usage_undo", lambda: 0)()}
|
||||
return result
|
||||
|
||||
|
||||
class BlendUndoMemfileManyPointersTest(api.Test):
|
||||
def name(self):
|
||||
return "undo_memfile_1M_bheads_and_pointers"
|
||||
|
||||
def category(self):
|
||||
return "undo"
|
||||
|
||||
def run(self, env, device_id, gpu_backend):
|
||||
result, _ = env.run_in_blender(
|
||||
_run_many_bheads_and_pointers,
|
||||
# Will generate 100^3, i.e. 1M idprops.
|
||||
{"num_props_per_level": 100, "num_levels": 3},
|
||||
["--factory-startup"]
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def generate(env):
|
||||
return [BlendUndoMemfileHeavyGeometryTest(), BlendUndoMemfileManyPointersTest()]
|
||||
Reference in New Issue
Block a user