Skip to content
Snippets Groups Projects
render_auto_tile_size.py 14.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
    # 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, 1),
        "blender": (2, 74, 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, 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(bpy.types.PropertyGroup):
        gpu_choice = bpy.props.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 = bpy.props.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 = bpy.props.EnumProperty(
            name="Target CPU Tile Size",
            items=TILE_SIZES,
            default='64',
            description="Square dimensions of tiles",
            update=_update_tile_size)
    
        gpu_custom = bpy.props.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 = bpy.props.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 = bpy.props.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 = bpy.props.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 = 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")
    
        thread_error_correct = bpy.props.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 = bpy.props.BoolProperty(default=True, options={'HIDDEN'})
        threads_error = bpy.props.BoolProperty(options={'HIDDEN'})
        num_tiles = bpy.props.IntVectorProperty(default=(0, 0), size=2, 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 engine_is_gpu(engine, device, userpref):
        return engine == 'CYCLES' and device == 'GPU' and userpref.system.compute_device_type != 'NONE'
    
    
    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.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 = 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.user_preferences
    
        if engine_is_gpu(engine, device, userpref):
            gpu_device_str = userpref.system.compute_device
            if 'MULTI' in gpu_device_str:
                threads = int(gpu_device_str.split('_')[-1])
            else:
                threads = 1
        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)
                        if secondary.is_integer():  # will only be an integer if there are the right number of tiles
                            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.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
    
        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(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=False)
        sub.enabled = settings.is_enabled
    
        if settings.use_advanced_ui:
            row = sub.row(align=True)
            row.label("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("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 (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")
        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)
    
    
    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()