import datetime import json import os import platform import subprocess import sys import tempfile from threading import Thread import bpy from bpy.types import Panel, Scene from bpy.props import EnumProperty, IntProperty, FloatProperty from ..foundation import (benchrunner, logger, progress, system_info, util) from ..foundation import context as benchmark_context from .global_state import G from . import draw QUICK_SCENES = ["bmw27", "classroom"] COMPLETE_SCENES = ["barbershop_interior", "bmw27", "classroom", "fishy_cat", "koro", "pavillon_barcelona"] handle_draw = bpy.types.SpaceBenchmark.draw_handler_add( draw.benchmark_draw_post_pixel, (None, None), 'WINDOW', 'POST_PIXEL') ################################################################################ # Benchmark foundation integration. class ProgressProviderSink: current_progress = 0.0 current_step = '' current_scene = '' process = None def __init__(self): self.current_progress = 0.0 self.current_step = '' self.process = None def progress(self, count, total, prefix="", suffix=""): with G.progress_lock: if total != 0: self.current_progress = float(count) / float(total) else: self.current_progress = 0.0 def clear(self): with G.progress_lock: self.current_progress = 0 def step(self, step_name): with G.progress_lock: if self.current_step != step_name: self.current_step = step_name self.current_progress = 0 def scene(self, scene_name): with G.progress_lock: self.current_scene = scene_name if scene_name: G.scene_status[scene_name] = "Rendering..." def scene_stats(self, scene_name, stats): with G.progress_lock: if stats: G.scene_status[scene_name] = util.humanReadableTimeDifference( stats.total_render_time) else: G.scene_status[scene_name] = "Crashed :(" def render_process(self, process): self.process = process def is_canceled(self): with G.progress_lock: return G.cancel class LoggerProviderSink: def HEADER(self, *args): pass def WARNING(self, *args): pass def ERROR(self, *args): pass def OK(self, *args): pass def BOLD(self, *args): pass def INFO(self, *args): pass def DEBUG(self, *args): pass def FATAL(self, *args): pass ################################################################################ # Benchmark thread. def string_strip_trademark(name): return name.replace("(R)", "").replace("(TM)", "") def correct_device_name(name): if (name.startswith("TITAN") or name.startswith("Quadro") or name.startswith("GeForce")): return "Nvidia " + name if (name.startswith("Radeon")): return "AMD " + name return name def get_gpu_names(system_info): gpu_names = [] for device in system_info["devices"]: if device["type"] == "CPU": continue gpu_names.append(correct_device_name(device["name"])) return gpu_names def indent_gpu_names(gpu_names): indented_names = [] for name in gpu_names: indented_names.append(" • " + name) return indented_names def construct_platform_string(system_info): """ Construct human readable platform string to show in the interface. """ result = "" result += "OS: {} {}".format(system_info["system"], system_info["bitness"]) result += "\nCPU: {}".format( string_strip_trademark(system_info["cpu_brand"])) gpu_names = get_gpu_names(system_info) num_gpus = len(gpu_names) if num_gpus: if num_gpus == 1: result += "\nGPU: {}".format(gpu_names[0]) else: result += "\nGPUs:\n{}".format("\n".join(indent_gpu_names(gpu_names))) return result def convert_result_to_json_dict(ctx, results): # Convert custom classes to dictionaries for easier JSON dump. json_results = results stats = json_results['scenes'] json_results['scenes'] = [] for scene in ctx.scenes: if scene not in stats: continue stat = stats[scene] if stat: stat = stat.asDict() stat['result'] = 'OK' else: stat = {'result': 'CRASH'} json_results['scenes'].append({'name': scene, 'stats': stat}) return json_results def system_info_get(ctx): # This is all system information Blender knows. # NOTE: We override executable since cpuinfo uses it, and it is set # to blender. old_executable = sys.executable sys.executable = bpy.app.binary_path_python info = system_info.gatherSystemInfo(ctx) sys.executable = old_executable return info def modify_system_info(system_info): compute_units = query_opencl_compute_units() for device in system_info["devices"]: device_type = device["type"] if device_type != "OPENCL": continue index = find_first_device_index(compute_units, device['name']) if index != -1: if device["name"] == "Radeon RX Vega": device["name"] += " " + compute_units[index][1] del compute_units[index] return system_info def modify_device_info(device_info): compute_device = bpy.context.scene.compute_device device_type, device_name, compute_units, device_index = compute_device.split(":") if device_info["device_type"] == "OPENCL": compute_devices = [] for device in device_info["compute_devices"]: if device == "Radeon RX Vega": device += " " + compute_units compute_devices.append(device) device_info["compute_devices"] = compute_devices return device_info def benchmark_thread(ctx): with G.progress_lock: G.progress_status = "Collecting system information." if G.cancel: return blender_system_info = system_info_get(ctx) # This is actual device configuration which is used to render the # benchmark scene. blender_device_info = benchrunner.benchmarkGetDeviceInfo(ctx) if not blender_device_info['device_type']: # TODO(sergey): Report an error somehow. return with G.progress_lock: G.result_platform = construct_platform_string(blender_system_info) if G.cancel: return G.progress_status = "Prepating render." all_stats = benchrunner.benchmarkAll(ctx) # Gather all information together. timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat() result = convert_result_to_json_dict(ctx, { "timestamp": timestamp, "blender_version": system_info.getBlenderVersion(ctx), "system_info": modify_system_info(blender_system_info), "device_info": modify_device_info(blender_device_info), "scenes": all_stats if all_stats else {} }) with G.progress_lock: if G.cancel: return G.result_dict = result ################################################################################ # Panels def ui_scale_factor(x): # To position the buttons exactly we add spacing, but there can be # rounding errors with non-integer DPI scaling. We roughly compensate # for those here. ui_scale = bpy.context.user_preferences.system.ui_scale widget_height = 20 * ui_scale return x * widget_height / int(widget_height) class BENCHMARK_PT_main(Panel): bl_label = "Benchmark" bl_options = {'HIDE_HEADER'} bl_space_type = 'BENCHMARK' bl_region_type = 'WINDOW' def draw_link(self, context): layout = self.layout col = layout.column() col.scale_y = ui_scale_factor(0.35) col.label(text="") col = layout.column() sub = col.row() sub.alignment = 'RIGHT' sub.emboss = 'LINK' sub.scale_y = 1.5 sub.active = False sub.operator("benchmark.opendata_link") def draw_welcome(self, context): layout = self.layout split = layout.split(0.65) split.label() split = split.split(0.97) col = split.column() sub = col.row() sub.scale_y = ui_scale_factor(64.0) sub.separator() compute_device_list_get(None, context) if len(G.cached_compute_devices) > 1: sub = col.row() sub.scale_y = 1.5 sub.prop(context.scene, "compute_device", text="") else: sub = col.row() sub.scale_y = 1.5 sub.label(text="") sub = col.row() sub.scale_y = 2.25 sub.operator("benchmark.run_quick", text="QUICK BENCHMARK") col.separator() sub = col.row() sub.emboss = 'LINK' sub.scale_y = 1.5 sub.operator("benchmark.run_complete", text="RUN COMPLETE BENCHMARK") split.label() self.draw_link(context) def draw_submit(self, context): layout = self.layout split = layout.split(0.65) split.label() split = split.split(0.97) col = split.column() sub = col.row() sub.scale_y = ui_scale_factor(64.0) sub.separator() sub = col.row() sub.enabled = not G.results_submitted sub.scale_y = 2.25 sub.operator("benchmark.share", text="SHARE ONLINE") sub = col.row() subsub = sub.split() subsub.emboss = 'LINK' subsub.scale_y = 1.5 subsub.operator("benchmark.save", text="Save Locally...") subsub = sub.split() subsub.emboss = 'LINK' subsub.scale_y = 1.5 subsub.operator("benchmark.restart", text="Start Again") split.label() def draw(self, context): screen_index = 0 with G.progress_lock: if G.result_dict: screen_index = 2 elif G.result_stats or G.progress_status: screen_index = 1 if screen_index == 0: self.draw_welcome(context) elif screen_index == 2: self.draw_submit(context) ################################################################################ # Operator def blender_benchmark_data_dir_get(): system = platform.system() if system == "Linux" or system == "Windows": return os.path.dirname(bpy.app.binary_path) elif system == "Darwin": return os.path.join(os.path.dirname(bpy.app.binary_path), "..", "Resources") else: raise Exception("Needs implementation") def blender_executable_get(): benchmark_data_dir = blender_benchmark_data_dir_get() system = platform.system() if system == "Linux": return os.path.join(benchmark_data_dir, "blender", "blender") elif system == "Windows": return os.path.join(benchmark_data_dir, "blender", "blender.exe") elif system == "Darwin": return os.path.join(benchmark_data_dir, "blender", "blender.app", "Contents", "MacOS", "blender") else: raise Exception("Needs implementation") def scenes_dir_get(): benchmark_data_dir = blender_benchmark_data_dir_get() return os.path.join(benchmark_data_dir, "scenes") def configure_script_get(): script_directory = os.path.dirname(os.path.realpath(__file__)) benchmark_script_directory = os.path.dirname(script_directory) return os.path.join(benchmark_script_directory, "configure.py") class BENCHMARK_OT_run_base(bpy.types.Operator): run_type = 'QUICK' # or 'COMPLETE' benchmark_context = None thread = None timer = None progress_provider = None logger_provider = None tmpdir = None def setup_sink(self): self.progress_provider = ProgressProviderSink() self.logger_provider = LoggerProviderSink() progress.setProvider(self.progress_provider) logger.setProvider(self.logger_provider) def update_status(self, context): with G.progress_lock: step = self.progress_provider.current_step if G.cancel: G.progress_status = "Canceling..." elif step == 'WARM_UP': G.progress_status = "Rendering warm-up pass..." elif step == 'RUN': G.current_progress = self.progress_provider.current_progress G.progress_status = "Rendering... Press Esc to stop." context.area.tag_redraw() # Path to currently displayed background image. current_scene = self.progress_provider.current_scene if current_scene: G.background_image_path = os.path.join( self.benchmark_context.scenes_dir, current_scene, current_scene + ".png") else: G.background_image_path = "" # Update per-scene status string G.result_stats = "" for scene in G.scene_status: G.result_stats += "{}: {}\n".format( scene, G.scene_status[scene]) def done(self, context): wm = context.window_manager wm.event_timer_remove(self.timer) # Restore all modifications to the benchmark foundation. progress.restoreDefaultProvider() logger.restoreDefaultProvider() # Destroy objects of sinks. del self.progress_provider del self.logger_provider self.progress_provider = None self.logger_provider = None # Construct final stats string if G.cancel: G.reset() elif G.result_dict: G.result_stats = "" for name_stat in G.result_dict["scenes"]: stat = name_stat["stats"] if G.result_stats: G.result_stats += "\n" if stat["result"] == "OK": G.result_stats += "{}: {}".format(name_stat['name'], util.humanReadableTimeDifference( stat["total_render_time"])) else: G.result_stats += "{}: {}".format(name_stat['name'], stat["result"]) else: G.result_stats = "" # TOGO(sergey): Use some more nice picture for the final slide. G.background_image_path = "" # Tag for nice redraw with G.progress_lock: G.progress_status = '' G.current_progress = 0.0 context.area.tag_redraw() self.tmpdir = None def modal(self, context, event): if event.type == 'TIMER': if self.thread.is_alive(): self.update_status(context) return {'PASS_THROUGH'} else: self.done(context) return {'FINISHED'} elif event.type == 'ESC': self.cancel_request(context) return {'PASS_THROUGH'} def invoke(self, context, event): G.cancel = False G.result_platform = "" G.progress_status = "Initializing..." context.area.tag_redraw() compute_device = context.scene.compute_device device_type, device_name, compute_units, device_index = compute_device.split(":") self.tmpdir = tempfile.TemporaryDirectory(prefix="blender_benchmark_") # Before doing anything, make sure we have all sinks set up, so we do # not miss any progress report. self.setup_sink() wm = context.window_manager ctx = benchmark_context.Context() ctx.blender = blender_executable_get() ctx.configure_script = configure_script_get() if self.run_type == 'QUICK': ctx.scenes = QUICK_SCENES else: ctx.scenes = COMPLETE_SCENES for scene in ctx.scenes: G.scene_status[scene] = "Queued" ctx.scenes_dir = scenes_dir_get() ctx.device_type = device_type ctx.device_name = device_name ctx.single_compute_scene = True ctx.image_output_dir = self.tmpdir.name # Set this to True when having multiple GPUs of same name and only # one of the mis to be enabled. Or when requesting GPU render without # specifying GPU name. ctx.device_single = True ctx.device_index = device_index # ctx.image_output_dir = "/tmp/" self.benchmark_context = ctx # Create thread for the actual benchmark. self.thread = Thread(target=benchmark_thread, args=(self.benchmark_context,)) self.thread.start() # Create timer to query thread status self.timer = wm.event_timer_add(0.1, context.window) # Register self as modal. context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def cancel_request(self, context): with G.progress_lock: G.cancel = True context.area.tag_redraw() if self.progress_provider.process: if platform.system() == "Windows": self.progress_provider.process.kill() else: import signal self.progress_provider.process.send_signal(signal.SIGINT) def cancel(self, context): self.cancel_request(context) if self.timer: wm = context.window_manager wm.event_timer_remove(self.timer) if self.thread: self.thread.join() class BENCHMARK_OT_run_quick(BENCHMARK_OT_run_base): "Run quick Blender benchmark" bl_label = "Run Benchmark" bl_idname = "benchmark.run_quick" run_type = 'QUICK' class BENCHMARK_OT_run_complete(BENCHMARK_OT_run_base): "Run complete Blender benchmark (might take 1.5 hours to finish and 4GiB of GPU memory)" bl_label = "Run Benchmark" bl_idname = "benchmark.run_complete" run_type = 'COMPLETE' class BENCHMARK_OT_save(bpy.types.Operator): bl_idname = "benchmark.save" bl_label = "Save Benchmark Result" filepath: bpy.props.StringProperty( subtype='FILE_PATH', options={'SKIP_SAVE'}, ) def execute(self, context): with open(self.filepath, "w") as f: f.write(json.dumps(G.result_dict, sort_keys=True, indent=2)) make_buttons_green() return {'FINISHED'} def invoke(self, context, event): import os make_buttons_default() if not self.filepath: self.filepath = os.path.join( os.path.expanduser("~"), "benchmark-result.txt") wm = context.window_manager wm.fileselect_add(self) return {'RUNNING_MODAL'} def cancel(self, context): make_buttons_green() class BENCHMARK_OT_share(bpy.types.Operator): bl_idname = "benchmark.share" bl_label = "Share Benchmark Result" def execute(self, context): from benchmark import submission make_buttons_default() print('Submitting benchmark') try: submission.submit_benchmark(G.result_dict) except Exception as ex: self.report({'ERROR'}, f'Error submitting results:\n{str(ex)[:100]}') return {'CANCELLED'} print('Submission done') make_buttons_green() G.results_submitted = True return {'FINISHED'} class BENCHMARK_OT_opendata_link(bpy.types.Operator): bl_idname = "benchmark.opendata_link" bl_label = "opendata.blender.org" def invoke(self, context, event): bpy.ops.wm.url_open('INVOKE_DEFAULT', url="https://opendata.blender.org") return {'FINISHED'} ################################################################################ # Restart benchmark. class BENCHMARK_OT_restart(bpy.types.Operator): bl_idname = "benchmark.restart" bl_label = "Go to a home screen and choose another benchmark to run" def invoke(self, context, event): G.reset() return {'FINISHED'} ################################################################################ # Configuration. def cl_query_executable_get(): benchmark_data_dir = blender_benchmark_data_dir_get() system = platform.system() if system == "Linux": return os.path.join(benchmark_data_dir, "bin", "cl_query") elif system == "Windows": return os.path.join(benchmark_data_dir, "bin", "cl_query.exe") elif system == "Darwin": return os.path.join(benchmark_data_dir, "bin", "cl_query") else: raise Exception("Needs implementation") def query_opencl_compute_units(): binary = cl_query_executable_get() output = subprocess.run([binary], stdout=subprocess.PIPE).stdout lines = output.splitlines() compute_units = [] for line in lines: (name, max_compute_units) = line.rsplit(b':', 1) compute_units.append((name.decode(), max_compute_units.decode())) return compute_units def find_first_device_index(compute_units, device_name): if not compute_units: return -1 for index, value in enumerate(compute_units): if value[0] == device_name: return index return -1 def compute_device_list_get(self, context): if G.cached_compute_devices: return G.cached_compute_devices compute_devices = [('CPU:::', "CPU", "")] if not G.cached_system_info: ctx = benchmark_context.Context() ctx.blender = blender_executable_get() ctx.configure_script = configure_script_get() G.cached_system_info = system_info_get(ctx) compute_units = query_opencl_compute_units() device_indices = {} for device in G.cached_system_info["devices"]: raw_device_name = device["name"] device_type = device["type"] if raw_device_name in device_indices: device_indices[raw_device_name] += 1 device_index = device_indices[raw_device_name] else: device_indices[raw_device_name] = 0 device_index = 0 if device_type == "CPU": continue elif device_type == "OPENCL": device_name = correct_device_name(device["name"]) index = find_first_device_index(compute_units, device['name']) device_compute_units = "" if index != -1: if device["name"] == "Radeon RX Vega": device_name += " " + compute_units[index][1] device_compute_units = str(compute_units[index][1]) del compute_units[index] device_id = "{}:{}:{}:{}".format(device_type, device["name"], device_compute_units, device_index) compute_devices.append((device_id, device_name, "")) elif device_type == "CUDA": device_name = correct_device_name(device["name"]) device_id = "{}:{}::{}".format(device_type, device["name"], device_index) compute_devices.append((device_id, device_name, "")) G.cached_compute_devices = compute_devices return compute_devices ################################################################################ # Tweak User Preferences default_wcol_tool_inner = None default_wcol_tool_inner_sel = None default_wcol_tool_outline = None def backup_buttons_colors(): global default_wcol_tool_inner global default_wcol_tool_inner_sel global default_wcol_tool_outline userpref = bpy.context.user_preferences theme = userpref.themes[0] default_wcol_tool_inner = theme.user_interface.wcol_tool.inner[:] default_wcol_tool_inner_sel = theme.user_interface.wcol_tool.inner_sel[:] default_wcol_tool_outline = theme.user_interface.wcol_tool.outline[:] def make_buttons_green(): userpref = bpy.context.user_preferences theme = userpref.themes[0] theme.user_interface.wcol_tool.inner = [0.408, 0.590, 0.129, 1.0] theme.user_interface.wcol_tool.inner_sel = [0.308, 0.490, 0.029, 1.0] theme.user_interface.wcol_tool.outline = [0.408, 0.590, 0.129] def make_buttons_default(): userpref = bpy.context.user_preferences theme = userpref.themes[0] theme.user_interface.wcol_tool.inner = default_wcol_tool_inner theme.user_interface.wcol_tool.inner_sel = default_wcol_tool_inner_sel theme.user_interface.wcol_tool.outline = default_wcol_tool_outline userpref = bpy.context.user_preferences theme = userpref.themes[0] userpref.view.use_quit_dialog = False theme.benchmark.space.back = [0.26, 0.26, 0.26] backup_buttons_colors() make_buttons_green() style = userpref.ui_styles[0] style.widget.points = 12 ################################################################################ # Registration classes = ( BENCHMARK_PT_main, BENCHMARK_OT_restart, BENCHMARK_OT_run_quick, BENCHMARK_OT_run_complete, BENCHMARK_OT_save, BENCHMARK_OT_share, BENCHMARK_OT_opendata_link, ) Scene.compute_device = EnumProperty( items=compute_device_list_get, name="Compute Device", description="Compute device to run benchmark on")