import datetime import json import os import platform import sys import tempfile from threading import Thread, Lock import blf import bpy from bpy.types import Panel, Scene from bpy.props import EnumProperty, IntProperty, FloatProperty from ..foundation import (benchrunner, buildbot, config, logger, progress, system_info, util) from ..foundation import context as benchmark_context ################################################################################ # Global state. QUICK_SCENES = ["bmw27", "classroom"] COMPLETE_SCENES = ["barbershop_interior", "bmw27", "classroom", "fishy_cat", "koro", "pavillon_barcelona"] global_result_platform = None global_progress_status = None global_result_stats = None global_result_dict = None global_background_image_path = "" global_scene_status = {} global_cancel = False global_cached_system_info = None global_cached_compute_devices = None images = {} current_progress = 0.0 progress_lock = Lock() WELCOME_TEXT = "Run the Quick Benchmark to get a fast measurement of\n" \ "your hardware's performance. Requires approximately 10\n" \ "minutes" BLURB_TEXT = "Blurb on sharing the data on the opendata.blender.org\n" \ "you can manage your privacy and everything will be\n" \ "safe and open source" def reset_global_state(): global global_result_platform global global_progress_status global global_result_stats global global_result_dict global global_background_image_path global global_scene_status global_result_platform = None global_progress_status = None global_result_stats = None global_result_dict = None global_background_image_path = "" global_scene_status = {} ################################################################################ # Draw Utilities. font_id = 0 def viewport_size(): import bgl viewport = bgl.Buffer(bgl.GL_INT, 4) bgl.glGetIntegerv(bgl.GL_VIEWPORT, viewport) return viewport[2], viewport[3] def draw_text_center(text, x, y, shadow=False): dim = blf.dimensions(font_id, text) cx = x - int(dim[0]/2) cy = y - int(dim[1]/2) if shadow: delta = 1 blf.color(font_id, 0.2, 0.2, 0.2, 1.0) blf.position(font_id, cx + delta, cy - delta, 0) blf.draw(font_id, text) blf.color(font_id, 1.0, 1.0, 1.0, 1.0) blf.position(font_id, cx, cy, 0) blf.draw(font_id, text) def draw_text_multiline(text, x, y, shadow=False): ui_scale = bpy.context.user_preferences.system.ui_scale height = int(blf.dimensions(font_id, "Dummy Text")[1]) space = int(8 * ui_scale) for line in text.split('\n'): if shadow: delta = 1 blf.color(font_id, 0.2, 0.2, 0.2, 1.0) blf.position(font_id, x + delta, y - height - delta, 0) blf.draw(font_id, line) blf.color(font_id, 1.0, 1.0, 1.0, 1.0) blf.position(font_id, x, y - height, 0) blf.draw(font_id, line) y -= height + space def draw_rect(x, y, w, h, color): import gpu gpu.draw.rect(x, y, x + w, y + h, color[0], color[1], color[2], color[3]) def draw_image(filepath, x, y, w, h): global images if filepath not in images: ima = bpy.data.images.load(filepath) images[filepath] = ima import gpu gpu.draw.image(images[filepath], x, y, x + w, y + h) ################################################################################ # Draw. def benchmark_draw_post_pixel(arg1, arg2): global progress_lock progress_lock.acquire() progress_status = global_progress_status result_platform = global_result_platform result_stats = global_result_stats result_dict = global_result_dict progress_lock.release() ui_scale = bpy.context.user_preferences.system.ui_scale blf.color(font_id, 1.0, 1.0, 1.0, 1.0) window_width, window_height = viewport_size() # Image image_h = 370 * ui_scale image_y = window_height - image_h if global_background_image_path: draw_image(global_background_image_path, 0, image_y, window_width, image_h) else: splash_dir = os.path.dirname(os.path.abspath(__file__)) splash_filepath = os.path.join(splash_dir, 'splash.png') draw_image(splash_filepath, 0, image_y, window_width, image_h) if result_dict: x = 0.5 * window_width y = 0.70 * window_height score = 0 for stat in global_result_dict["stats"]: if stat["result"] == "OK": score += stat["total_render_time"] else: score = -1 if score >= 0: blf.size(font_id, int(32 * ui_scale), 72) draw_text_center("Your time {}" . format( util.humanReadableTimeDifference(score)), x, y, shadow=True) else: blf.size(font_id, int(18 * ui_scale), 72) draw_text_center("Unfortunately, crash happened :(", x, y, shadow=True) blf.size(font_id, int(24 * ui_scale), 72) draw_text_center("You can still share data of succeeded scenes!", x, y - 26 * ui_scale, shadow=True) x = 50.0 * ui_scale y = image_y - (image_y - 52 * ui_scale - 18 * 3 * ui_scale) * 0.5 blf.size(font_id, int(12 * ui_scale), 72) draw_text_multiline(BLURB_TEXT, x, y) elif result_stats or result_platform or progress_status: blf.size(font_id, int(12 * ui_scale), 72) x = 50.0 * ui_scale y = image_y - 20 * ui_scale # Stats if result_platform: draw_text_multiline(result_platform, 0.5 * window_width + x, y) if result_stats: draw_text_multiline(result_stats, x, y) # Progress progress_x = 0.0 progress_y = image_y + 1 progress_w = window_width * current_progress progress_h = 15.0 * ui_scale progress_color = [0.8, 1.0, 1.0, 0.2] draw_rect(progress_x, progress_y, progress_w, progress_h, progress_color) # Current status if global_progress_status: blf.size(font_id, int(18 * ui_scale), 72) draw_text_multiline(global_progress_status, progress_x + 8.0 * ui_scale, progress_y + progress_h + int(22 * ui_scale), shadow=True) else: # Title x = 0.5 * window_width y = 0.70 * window_height blf.size(font_id, int(32 * ui_scale), 72) draw_text_center("Blender Benchmark 1.0", x, y, shadow=True) y -= 32 * ui_scale blf.size(font_id, int(12 * ui_scale), 72) draw_text_center("Explore the results on opendata.blender.org", x, y, shadow=True) x = 50.0 * ui_scale y = image_y - (image_y - 52 * ui_scale - 18 * 3 * ui_scale) * 0.5 blf.size(font_id, int(12 * ui_scale), 72) draw_text_multiline(WELCOME_TEXT, x, y) # Bottom bar bottom_x = 0 bottom_y = 0 bottom_w = window_width bottom_h = 52 * ui_scale bottom_color = [0.2, 0.2, 0.2, 1.0] draw_rect(bottom_x, bottom_y, bottom_w, bottom_h, bottom_color) # Logo logo_width_unscaled = 326 logo_height_unscaled = 104 logo_dir = os.path.dirname(os.path.abspath(__file__)) logo_filepath = os.path.join(logo_dir, 'blender.png') logo_scale_factor = 1.0 while logo_height_unscaled * logo_scale_factor > bottom_h: logo_scale_factor *= 0.5 logo_width = logo_width_unscaled * logo_scale_factor logo_height = logo_height_unscaled * logo_scale_factor logo_padding = (bottom_h - logo_height) * 0.5 draw_image(logo_filepath, logo_padding, logo_padding, logo_width, logo_height) handle_draw = bpy.types.SpaceBenchmark.draw_handler_add( 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=""): progress_lock.acquire() if total != 0: self.current_progress = float(count) / float(total) else: self.current_progress = 0.0 progress_lock.release() def clear(self): progress_lock.acquire() self.current_progress = 0 progress_lock.release() def step(self, step_name): progress_lock.acquire() if self.current_step != step_name: self.current_step = step_name self.current_progress = 0 progress_lock.release() def scene(self, scene_name): progress_lock.acquire() self.current_scene = scene_name if scene_name: global global_scene_status global_scene_status[scene_name] = "Rendering..." progress_lock.release() def scene_stats(self, scene_name, stats): progress_lock.acquire() global global_scene_status if stats: global_scene_status[scene_name] = util.humanReadableTimeDifference( stats.total_render_time) else: global_scene_status[scene_name] = "Crashed :(" progress_lock.release() def render_process(self, process): self.process = process def is_canceled(self): global global_cancel return global_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 construct_gpu_string(system_info): gpu_names = [] for device in system_info["devices"]: if device["type"] == "CPU": continue gpu_names.append(correct_device_name(device["name"])) return ", " . join(gpu_names) def construct_platform_string(system_info): """ Construct human readable platform string to show in the interface. """ result = "" result += "Operation System: {} {} bit" . format(system_info["system"], system_info["bitness"]) result += "\nCPU: {}" . format( string_strip_trademark(system_info["cpu_brand"])) gpus = construct_gpu_string(system_info) if gpus: result += "\nGPU(s): {}" . format(gpus) 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['stats'] json_results['stats'] = [] 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'} stat["scene_name"] = scene json_results['stats'] .append(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 benchmark_thread(ctx): global progress_lock, global_result_platform, global_progress_status global global_cancel progress_lock.acquire() global_progress_status = "Collecting system information." if global_cancel: progress_lock.release() return progress_lock.release() 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 progress_lock.acquire() global_result_platform = construct_platform_string(blender_system_info) if global_cancel: progress_lock.release() return global_progress_status = "Prepating render." progress_lock.release() 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": blender_system_info, "device_info": blender_device_info, "stats": all_stats if all_stats else {} }) progress_lock.acquire() if global_cancel: progress_lock.release() return progress_lock.release() global global_result_dict global_result_dict = result ################################################################################ # Panels 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 layout.separator() layout.separator() 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 = 64.0 sub.separator() compute_device_list_get(None, context) if len(global_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 = 64.0 sub.separator() sub = col.row() sub.scale_y = 2.25 sub.operator("benchmark.share", text="SHARE ONLINE") sub = col.row() sub.emboss = 'LINK' sub.scale_y = 1.5 sub.operator("benchmark.save", text="SAVE LOCALLY") sub = col.row() sub.emboss = 'LINK' sub.scale_y = 1.5 sub.operator("benchmark.restart", text="START AGAIN") split.label() def draw(self, context): screen_index = 0 global progress_lock progress_lock.acquire() if global_result_dict: screen_index = 2 elif global_result_stats or global_progress_status: screen_index = 1 progress_lock.release() 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): global global_progress_status, global_background_image_path global global_cancel progress_lock.acquire() step = self.progress_provider.current_step if global_cancel: global_progress_status = "Cancelling." elif step == 'WARM_UP': global_progress_status = "Rendering warm-up pass." elif step == 'RUN': global current_progress current_progress = self.progress_provider.current_progress global_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: global_background_image_path = os.path.join( self.benchmark_context.scenes_dir, current_scene, current_scene + ".png") else: global_background_image_path = "" # Update per-scene status string global global_result_stats global_result_stats = "" for scene in global_scene_status: global_result_stats += "{}: {}\n" . format( scene, global_scene_status[scene]) progress_lock.release() def done(self, context): global global_progress_status, global_result_stats, current_progress global global_result_dict 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 global_cancel: global_result_dict = None reset_global_state() else: global_result_stats = "" for stat in global_result_dict["stats"]: if global_result_stats: global_result_stats += "\n" if stat["result"] == "OK": global_result_stats += "{}: {}" . format(stat["scene_name"], util.humanReadableTimeDifference( stat["total_render_time"])) else: global_result_stats += "{}: {}" . format(stat["scene_name"], stat["result"]) # TOGO(sergey): Use some more nice picture for the final slide. global global_background_image_path global_background_image_path = "" # Tag for nice redraw global_progress_status = None 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): global global_result_platform, global_progress_status global global_scene_status, global_cancel global_cancel = False global_result_platform = "" global_progress_status = "Initializing..." context.area.tag_redraw() compute_device = context.scene.compute_device device_type, device_name = 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: global_scene_status[scene] = "N/A" 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.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): global global_cancel progress_lock.acquire() global_cancel = True context.area.tag_redraw() progress_lock.release() 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): global global_cancel 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 hour to finish)" 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(global_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 invoke(self, context, event): 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): reset_global_state() return {'FINISHED'} ################################################################################ # Configuration. def compute_device_list_get(self, context): global global_cached_system_info global global_cached_compute_devices if global_cached_compute_devices: return global_cached_compute_devices compute_devices = [('CPU:', "CPU", "")] if not global_cached_system_info: ctx = benchmark_context.Context() ctx.blender = blender_executable_get() ctx.configure_script = configure_script_get() global_cached_system_info = system_info_get(ctx) for device in global_cached_system_info["devices"]: device_type = device["type"] if device_type == "CPU": continue elif device_type in ("OPENCL", "CUDA"): device_name = correct_device_name(device["name"]) device_id = "{}:{}" . format(device_type, device["name"]) compute_devices.append((device_id, device_name, "")) global_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.690, 0.129, 1.0] theme.user_interface.wcol_tool.inner_sel = [0.308, 0.590, 0.029, 1.0] theme.user_interface.wcol_tool.outline = [0.408, 0.690, 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")