# 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": (3, 1, 3), "blender": (2, 80, 0), "location": "Render Settings > Performance", "warning": "", "doc_url": "{BLENDER_MANUAL_URL}/addons/render/auto_tile_size.html", "category": "Render", } import bpy from bpy.types import ( Operator, PropertyGroup, ) from bpy.props import ( BoolProperty, EnumProperty, FloatVectorProperty, IntProperty, IntVectorProperty, StringProperty, PointerProperty, ) from bpy.app.handlers import persistent from math import ( ceil, floor, sqrt, ) 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(PropertyGroup): gpu_choice: EnumProperty( name="Target GPU Tile Size", items=TILE_SIZES, default='256', description="Square dimensions of tiles for GPU rendering", update=_update_tile_size ) cpu_choice: EnumProperty( name="Target CPU Tile Size", items=TILE_SIZES, default='32', description="Square dimensions of tiles for CPU rendering", update=_update_tile_size ) bi_choice: EnumProperty( name="Target CPU Tile Size", items=TILE_SIZES, default='64', description="Square dimensions of tiles", update=_update_tile_size ) gpu_custom: IntProperty( name="Target Size", default=256, min=8, # same as blender's own limits max=65536, description="Custom target tile size for GPU rendering", update=_update_tile_size ) cpu_custom: IntProperty( name="Target Size", default=32, min=8, # same as blender's own limits max=65536, description="Custom target tile size for CPU rendering", update=_update_tile_size ) bi_custom: IntProperty( name="Target Size", default=64, min=8, # same as blender's own limits max=65536, description="Custom target tile size", update=_update_tile_size ) target_type: EnumProperty( name="Target tile size", items=( ('po2', "Po2", "A choice between powers of 2 (16, 32, 64...)"), ('custom', "Custom", "Choose any number as the tile size target")), default='po2', description="Method of choosing the target tile size", update=_update_tile_size ) use_optimal: 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: 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: BoolProperty( name="Advanced Settings", default=False, description="Show extra options for more control over the calculated tile size" ) thread_error_correct: BoolProperty( name="Fix", default=True, description="Reduce the tile size so that all your available threads are used", update=_update_tile_size ) # Internally used props (not for GUI) first_run: BoolProperty( default=True, options={'HIDDEN'} ) threads_error: BoolProperty( options={'HIDDEN'} ) num_tiles: IntVectorProperty( default=(0, 0), size=2, options={'HIDDEN'} ) prev_choice: StringProperty( default='', options={'HIDDEN'} ) prev_engine: StringProperty( default='', options={'HIDDEN'} ) prev_device: StringProperty( default='', options={'HIDDEN'} ) prev_res: IntVectorProperty( default=(0, 0), size=2, options={'HIDDEN'} ) prev_border: BoolProperty( default=False, options={'HIDDEN'} ) prev_border_res: FloatVectorProperty( default=(0, 0, 0, 0), size=4, options={'HIDDEN'} ) prev_actual_tile_size: IntVectorProperty( default=(0, 0), size=2, options={'HIDDEN'} ) prev_threads: 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 engine_is_gpu(engine, device, userpref): if engine == 'CYCLES' and device == 'GPU': return userpref.addons['cycles'].preferences.has_active_device() return False def get_tilesize_prop(engine, device, userpref): target_type = "_choice" if bpy.context.scene.ats_settings.target_type == 'po2' else "_custom" if engine_is_gpu(engine, device, userpref): return ("gpu" + target_type) elif engine == 'CYCLES': return ("cpu" + target_type) return ("bi" + target_type) @persistent def on_scene_update(scene): context = bpy.context if not ats_poll(context): return userpref = context.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 = get_threads(context, device) choice = getattr(settings, 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 str(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 get_threads(context, device): render = context.scene.render engine = render.engine userpref = context.preferences if engine_is_gpu(engine, device, userpref): threads = userpref.addons['cycles'].preferences.get_num_gpu_devices() else: threads = render.threads return threads def max_tile_size(threads, xres, yres): ''' Give the largest tile size that will still use all threads ''' render_area = xres * yres tile_area = render_area / threads tile_length = sqrt(tile_area) # lists: num x tiles, num y tiles, squareness, total tiles perfect_attempts = [] # attempts with correct number of tiles attempts = [] # all attempts, even if incorrect number of tiles axes = [xres, yres] funcs = [floor, ceil] for axis in axes: sec_axis = yres if axis == xres else xres for func in funcs: primary = func(axis / tile_length) if primary > 0: secondary = threads / primary ts_p = axis / primary ts_s = sec_axis / secondary squareness = max(ts_p, ts_s) - min(ts_p, ts_s) attempt = [primary if axis == xres else secondary, primary if axis != xres else secondary, squareness, primary * secondary] if attempt not in attempts: attempts.append(attempt) # will only be an integer if there are the right number of tiles if secondary.is_integer(): perfect_attempts.append(attempt) if perfect_attempts: # prefer to use attempt that has exactly the right number of tiles attempts = perfect_attempts attempt = sorted(attempts, key=lambda k: k[2])[0] # pick set with most square tiles numtiles_x = round(attempt[0]) numtiles_y = round(attempt[1]) tile_x = ceil(xres / numtiles_x) tile_y = ceil(yres / numtiles_y) return (tile_x, tile_y) def do_set_tile_size(context): if not ats_poll(context): return False scene = context.scene userpref = context.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 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, get_tilesize_prop(engine, device, userpref)) target = int(choice) numtiles_x = ceil(xres / target) numtiles_y = ceil(yres / target) settings.num_tiles = (numtiles_x, numtiles_y) 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 (for debug purposes) # print("Tile size: %dx%d (%dx%d tiles)" % (tile_x, tile_y, ceil(xres / tile_x), ceil(yres / tile_y))) # Detect if there are fewer tiles than available threads threads = get_threads(context, device) if ((numtiles_x * numtiles_y) < threads): settings.threads_error = True if settings.thread_error_correct: tile_x, tile_y = max_tile_size(threads, xres, yres) settings.num_tiles = (ceil(xres / tile_x), ceil(yres / tile_y)) else: settings.threads_error = False # Make sure tile sizes are within the internal limit tile_x = max(8, tile_x) tile_y = max(8, tile_y) tile_x = min(65536, tile_x) tile_y = min(65536, tile_y) render.tile_x = tile_x render.tile_y = tile_y settings.prev_engine = engine settings.prev_device = device settings.prev_border = border settings.prev_threads = threads settings.prev_choice = str(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(Operator): bl_idname = "render.autotilesize_set" bl_label = "Set" bl_description = "The first render may not obey the tile-size set here" @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.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=False) sub.enabled = settings.is_enabled if settings.use_advanced_ui: row = sub.row(align=True) row.label(text="Target tile size:") row.separator() row.prop(settings, "target_type", expand=True) row = sub.row(align=True) row.prop(settings, get_tilesize_prop(engine, device, userpref), expand=True) sub.prop(settings, "use_optimal", text="Calculate Optimal Size") sub.label(text="Number of tiles: %s x %s (Total: %s)" % (settings.num_tiles[0], settings.num_tiles[1], settings.num_tiles[0] * settings.num_tiles[1]) ) 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 not very square tile if (render.tile_x / render.tile_y > 2) or (render.tile_x / render.tile_y < 0.5): sub.label(text="Warning: Tile size is not very square", icon='ERROR') sub.label(text=" Try a slightly different resolution") if settings.threads_error: row = sub.row(align=True) row.alignment = 'CENTER' row.label(text="Warning: Fewer tiles than threads", icon='ERROR') row.prop(settings, 'thread_error_correct') def menu_func_cycles(self, context): ui_layout('CYCLES', self.layout, context) # ##### REGISTRATION ##### classes = ( AutoTileSizeSettings, SetTileSize ) def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.ats_settings = 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, "CYCLES_RENDER_PT_performance", None) if cycles_panel is not None: cycles_panel.append(menu_func_cycles) bpy.app.handlers.depsgraph_update_post.append(on_scene_update) def unregister(): bpy.app.handlers.depsgraph_update_post.remove(on_scene_update) cycles_panel = getattr(bpy.types, "CYCLES_RENDER_PT_performance", None) if cycles_panel is not None: cycles_panel.remove(menu_func_cycles) del bpy.types.Scene.ats_settings for cls in reversed(classes): bpy.utils.unregister_class(cls) if __name__ == "__main__": register()