diff --git a/render_auto_tile_size.py b/render_auto_tile_size.py new file mode 100644 index 0000000000000000000000000000000000000000..8ece451d2a01c34350780b35dd121bbc99796eb3 --- /dev/null +++ b/render_auto_tile_size.py @@ -0,0 +1,318 @@ +# BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# END GPL LICENSE BLOCK ##### + +bl_info = { + "name": "Auto Tile Size", + "description": "Estimate and set the tile size that will render the fastest", + "author": "Greg Zaal", + "version": (2, 7), + "blender": (2, 72, 0), + "location": "Render Settings > Performance", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php?title=Extensions:2.6/Py/Scripts/Render/Auto_Tile_Size", + "category": "Render", +} + + +import bpy +from bpy.app.handlers import persistent +from math import ceil, floor + + +SUPPORTED_RENDER_ENGINES = {'CYCLES', 'BLENDER_RENDER'} +TILE_SIZES = ( + ('16', "16", "16 x 16"), + ('32', "32", "32 x 32"), + ('64', "64", "64 x 64"), + ('128', "128", "128 x 128"), + ('256', "256", "256 x 256"), + ('512', "512", "512 x 512"), + ('1024', "1024", "1024 x 1024"), +) + + +def _update_tile_size(self, context): + do_set_tile_size(context) + + +class AutoTileSizeSettings(bpy.types.PropertyGroup): + gpu_choice = bpy.props.EnumProperty( + name="Target GPU Tile Size", + items=TILE_SIZES, + default='256', + description="Square dimentions of tiles", + update=_update_tile_size) + + cpu_choice = bpy.props.EnumProperty( + name="Target CPU Tile Size", + items=TILE_SIZES, + default='32', + description="Square dimentions of tiles", + update=_update_tile_size) + + bi_choice = bpy.props.EnumProperty( + name="Target CPU Tile Size", + items=TILE_SIZES, + default='64', + description="Square dimentions of tiles", + update=_update_tile_size) + + use_optimal = bpy.props.BoolProperty( + name="Optimal Tiles", + default=True, + description="Try to find a similar tile size for best performance, instead of using exact selected one", + update=_update_tile_size) + + is_enabled = bpy.props.BoolProperty( + name="Auto Tile Size", + default=True, + description="Calculate the best tile size based on factors of the render size and the chosen target", + update=_update_tile_size) + + use_advanced_ui = bpy.props.BoolProperty( + name="Advanced Settings", + default=False, + description="Show extra options for more control over the calculated tile size") + + # Internally used props (not for GUI) + first_run = bpy.props.BoolProperty(default=True, options={'HIDDEN'}) + threads_error = bpy.props.BoolProperty(options={'HIDDEN'}) + prev_choice = bpy.props.StringProperty(default='', options={'HIDDEN'}) + prev_engine = bpy.props.StringProperty(default='', options={'HIDDEN'}) + prev_device = bpy.props.StringProperty(default='', options={'HIDDEN'}) + prev_res = bpy.props.IntVectorProperty(default=(0, 0), size=2, options={'HIDDEN'}) + prev_border = bpy.props.BoolProperty(default=False, options={'HIDDEN'}) + prev_border_res = bpy.props.FloatVectorProperty(default=(0, 0, 0, 0), size=4, options={'HIDDEN'}) + prev_actual_tile_size = bpy.props.IntVectorProperty(default=(0, 0), size=2, options={'HIDDEN'}) + prev_threads = bpy.props.IntProperty(default=0, options={'HIDDEN'}) + + +def ats_poll(context): + scene = context.scene + if scene.render.engine not in SUPPORTED_RENDER_ENGINES or not scene.ats_settings.is_enabled: + return False + return True + + +def ats_get_engine_is_gpu(engine, device, userpref): + return engine == 'CYCLES' and device == 'GPU' and userpref.system.compute_device_type != 'NONE' + + +def ats_get_tilesize_prop(engine, device, userpref): + if ats_get_engine_is_gpu(engine, device, userpref): + return "gpu_choice" + elif engine == 'CYCLES': + return "cpu_choice" + return "bi_choice" + + +@persistent +def on_scene_update(scene): + context = bpy.context + + if not ats_poll(context): + return + + userpref = context.user_preferences + + settings = scene.ats_settings + render = scene.render + engine = render.engine + + # scene.cycles might not always exist (Cycles is an addon)... + device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device + border = render.use_border + threads = render.threads + + choice = getattr(settings, ats_get_tilesize_prop(engine, device, userpref)) + + res = get_actual_res(render) + actual_ts = (render.tile_x, render.tile_y) + border_res = (render.border_min_x, render.border_min_y, render.border_max_x, render.border_max_y) + + # detect relevant changes in scene + do_change = (engine != settings.prev_engine or + device != settings.prev_device or + border != settings.prev_border or + threads != settings.prev_threads or + choice != settings.prev_choice or + res != settings.prev_res[:] or + border_res != settings.prev_border_res[:] or + actual_ts != settings.prev_actual_tile_size[:]) + if do_change: + do_set_tile_size(context) + + +def get_actual_res(render): + rend_percent = render.resolution_percentage * 0.01 + # floor is implicitly done by int conversion... + return (int(render.resolution_x * rend_percent), int(render.resolution_y * rend_percent)) + + +def do_set_tile_size(context): + if not ats_poll(context): + return False + + scene = context.scene + userpref = context.user_preferences + + settings = scene.ats_settings + render = scene.render + engine = render.engine + device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device + border = render.use_border + threads = render.threads + + realxres, realyres = xres, yres = res = get_actual_res(scene.render) + + if border: + xres = round(xres * (render.border_max_x - render.border_min_x)) + yres = round(yres * (render.border_max_y - render.border_min_y)) + + choice = getattr(settings, ats_get_tilesize_prop(engine, device, userpref)) + target = int(choice) + + numtiles_x = ceil(xres / target) + numtiles_y = ceil(yres / target) + if settings.use_optimal: + tile_x = ceil(xres / numtiles_x) + tile_y = ceil(yres / numtiles_y) + else: + tile_x = target + tile_y = target + + print("Tile size: %dx%d (%dx%d tiles)" % (tile_x, tile_y, ceil(xres / tile_x), ceil(yres / tile_y))) + + render.tile_x = tile_x + render.tile_y = tile_y + + # Detect if there are fewer tiles than available threads + if ((numtiles_x * numtiles_y) < threads) and not ats_get_engine_is_gpu(engine, device, userpref): + settings.threads_error = True + else: + settings.threads_error = False + + settings.prev_engine = engine + settings.prev_device = device + settings.prev_border = border + settings.prev_threads = threads + settings.prev_choice = choice + settings.prev_res = res + settings.prev_border_res = (render.border_min_x, render.border_min_y, render.border_max_x, render.border_max_y) + settings.prev_actual_tile_size = (tile_x, tile_y) + settings.first_run = False + + return True + + +class SetTileSize(bpy.types.Operator): + """The first render may not obey the tile-size set here""" + bl_idname = "render.autotilesize_set" + bl_label = "Set" + + @classmethod + def poll(clss, context): + return ats_poll(context) + + def execute(self, context): + if do_set_tile_size(context): + return {'FINISHED'} + return {'CANCELLED'} + + +# ##### INTERFACE ##### + +def ui_layout(engine, layout, context): + scene = context.scene + userpref = context.user_preferences + + settings = scene.ats_settings + render = scene.render + engine = render.engine + device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device + + col = layout.column(align=True) + sub = col.column(align=True) + row = sub.row(align=True) + row.prop(settings, "is_enabled", toggle=True) + row.prop(settings, "use_advanced_ui", toggle=True, text="", icon='PREFERENCES') + + sub = col.column(align=True) + sub.enabled = settings.is_enabled + + if settings.use_advanced_ui: + sub.label("Target tile size:") + + row = sub.row(align=True) + row.prop(settings, ats_get_tilesize_prop(engine, device, userpref), expand=True) + sub.prop(settings, "use_optimal", text="Calculate Optimal Size") + + if settings.first_run: + sub = layout.column(align=True) + sub.operator("render.autotilesize_set", text="First-render fix", icon='ERROR') + elif settings.prev_device != device: + sub = layout.column(align=True) + sub.operator("render.autotilesize_set", text="Device changed - fix", icon='ERROR') + + if (render.tile_x / render.tile_y > 2) or (render.tile_x / render.tile_y < 0.5): # if not very square tile + sub.label(text="Warning: Tile size is not very square", icon='ERROR') + sub.label(text=" Try a slightly different resolution") + sub.label(text=" or choose \"Exact\" above") + if settings.threads_error: + sub.label(text="Warning: Fewer tiles than render threads", icon='ERROR') + + +def menu_func_cycles(self, context): + ui_layout('CYCLES', self.layout, context) + + +def menu_func_bi(self, context): + ui_layout('BLENDER_RENDER', self.layout, context) + + +# ##### REGISTRATION ##### + +def register(): + bpy.utils.register_module(__name__) + + bpy.types.Scene.ats_settings = bpy.props.PointerProperty(type=AutoTileSizeSettings) + + # Note, the Cycles addon must be registered first, otherwise this panel doesn't exist - better be safe here! + cycles_panel = getattr(bpy.types, "CyclesRender_PT_performance", None) + if cycles_panel is not None: + cycles_panel.append(menu_func_cycles) + + bpy.types.RENDER_PT_performance.append(menu_func_bi) + bpy.app.handlers.scene_update_post.append(on_scene_update) + + +def unregister(): + bpy.app.handlers.scene_update_post.remove(on_scene_update) + bpy.types.RENDER_PT_performance.remove(menu_func_bi) + + cycles_panel = getattr(bpy.types, "CyclesRender_PT_performance", None) + if cycles_panel is not None: + cycles_panel.remove(menu_func_cycles) + + del bpy.types.Scene.ats_settings + + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register()