Skip to content
Snippets Groups Projects
render_auto_tile_size.py 10.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • # 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()