Newer
Older
Bastien Montagne
committed
# 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),
Bastien Montagne
committed
"location": "Render Settings > Performance",
"warning": "",
"doc_url": "{BLENDER_MANUAL_URL}/addons/render/auto_tile_size.html",
Bastien Montagne
committed
"category": "Render",
}
import bpy
from bpy.types import (
Operator,
PropertyGroup,
)
from bpy.props import (
BoolProperty,
EnumProperty,
FloatVectorProperty,
IntProperty,
IntVectorProperty,
StringProperty,
PointerProperty,
)
Bastien Montagne
committed
from bpy.app.handlers import persistent
Bastien Montagne
committed
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)
name="Target GPU Tile Size",
items=TILE_SIZES,
default='256',
description="Square dimensions of tiles for GPU rendering",
update=_update_tile_size
)
name="Target CPU Tile Size",
items=TILE_SIZES,
default='32',
description="Square dimensions of tiles for CPU rendering",
update=_update_tile_size
)
name="Target CPU Tile Size",
items=TILE_SIZES,
default='64',
description="Square dimensions of tiles",
update=_update_tile_size
)
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
)
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
)
name="Target Size",
default=64,
min=8, # same as blender's own limits
max=65536,
description="Custom target tile size",
update=_update_tile_size
)
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
)
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
)
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
)
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
)
Bastien Montagne
committed
# Internally used props (not for GUI)
default=(0, 0),
size=2,
options={'HIDDEN'}
)
default=(0, 0),
size=2,
options={'HIDDEN'}
)
prev_border_res: FloatVectorProperty(
default=(0, 0, 0, 0),
size=4,
options={'HIDDEN'}
)
default=(0, 0),
size=2,
options={'HIDDEN'}
)
Bastien Montagne
committed
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):
Brecht Van Lommel
committed
if engine == 'CYCLES' and device == 'GPU':
return userpref.addons['cycles'].preferences.has_active_device()
return False
Bastien Montagne
committed
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
Bastien Montagne
committed
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
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))
Bastien Montagne
committed
def get_threads(context, device):
render = context.scene.render
engine = render.engine
Bastien Montagne
committed
if engine_is_gpu(engine, device, userpref):
Brecht Van Lommel
committed
threads = userpref.addons['cycles'].preferences.get_num_gpu_devices()
Bastien Montagne
committed
else:
threads = render.threads
return threads
Bastien Montagne
committed
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
Bastien Montagne
committed
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
Bastien Montagne
committed
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]
Bastien Montagne
committed
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():
Bastien Montagne
committed
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)
Bastien Montagne
committed
def do_set_tile_size(context):
if not ats_poll(context):
return False
scene = context.scene
Bastien Montagne
committed
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
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))
Bastien Montagne
committed
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)
Bastien Montagne
committed
settings.prev_actual_tile_size = (tile_x, tile_y)
settings.first_run = False
return True
Bastien Montagne
committed
bl_idname = "render.autotilesize_set"
bl_label = "Set"
bl_description = "The first render may not obey the tile-size set here"
Bastien Montagne
committed
@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
Bastien Montagne
committed
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:")
Bastien Montagne
committed
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])
)
Bastien Montagne
committed
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):
Bastien Montagne
committed
sub.label(text="Warning: Tile size is not very square", icon='ERROR')
sub.label(text=" Try a slightly different resolution")
Bastien Montagne
committed
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
)
Bastien Montagne
committed
def register():
for cls in classes:
bpy.utils.register_class(cls)
Bastien Montagne
committed
bpy.types.Scene.ats_settings = PointerProperty(
type=AutoTileSizeSettings
)
Bastien Montagne
committed
# 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)
Bastien Montagne
committed
if cycles_panel is not None:
cycles_panel.append(menu_func_cycles)
bpy.app.handlers.depsgraph_update_post.append(on_scene_update)
Bastien Montagne
committed
def unregister():
bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
Bastien Montagne
committed
cycles_panel = getattr(bpy.types, "CYCLES_RENDER_PT_performance", None)
Bastien Montagne
committed
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)
Bastien Montagne
committed
if __name__ == "__main__":
register()