Merge branch 'master' into blender2.8
[blender-addons.git] / render_auto_tile_size.py
blobeb1076fc6136fb53b2d8a6d6e11220a3f2cac883
1 # BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Auto Tile Size",
21 "description": "Estimate and set the tile size that will render the fastest",
22 "author": "Greg Zaal",
23 "version": (3, 1, 2),
24 "blender": (2, 74, 0),
25 "location": "Render Settings > Performance",
26 "warning": "",
27 "wiki_url": "https://wiki.blender.org/index.php?title=Extensions:2.6/Py/"
28 "Scripts/Render/Auto_Tile_Size",
29 "category": "Render",
33 import bpy
34 from bpy.types import (
35 Operator,
36 PropertyGroup,
38 from bpy.props import (
39 BoolProperty,
40 EnumProperty,
41 FloatVectorProperty,
42 IntProperty,
43 IntVectorProperty,
44 StringProperty,
45 PointerProperty,
47 from bpy.app.handlers import persistent
48 from math import (
49 ceil, floor,
50 sqrt,
54 SUPPORTED_RENDER_ENGINES = {'CYCLES', 'BLENDER_RENDER'}
55 TILE_SIZES = (
56 ('16', "16", "16 x 16"),
57 ('32', "32", "32 x 32"),
58 ('64', "64", "64 x 64"),
59 ('128', "128", "128 x 128"),
60 ('256', "256", "256 x 256"),
61 ('512', "512", "512 x 512"),
62 ('1024', "1024", "1024 x 1024"),
66 def _update_tile_size(self, context):
67 do_set_tile_size(context)
70 class AutoTileSizeSettings(PropertyGroup):
71 gpu_choice = EnumProperty(
72 name="Target GPU Tile Size",
73 items=TILE_SIZES,
74 default='256',
75 description="Square dimensions of tiles for GPU rendering",
76 update=_update_tile_size
78 cpu_choice = EnumProperty(
79 name="Target CPU Tile Size",
80 items=TILE_SIZES,
81 default='32',
82 description="Square dimensions of tiles for CPU rendering",
83 update=_update_tile_size
85 bi_choice = EnumProperty(
86 name="Target CPU Tile Size",
87 items=TILE_SIZES,
88 default='64',
89 description="Square dimensions of tiles",
90 update=_update_tile_size
92 gpu_custom = IntProperty(
93 name="Target Size",
94 default=256,
95 min=8, # same as blender's own limits
96 max=65536,
97 description="Custom target tile size for GPU rendering",
98 update=_update_tile_size
100 cpu_custom = IntProperty(
101 name="Target Size",
102 default=32,
103 min=8, # same as blender's own limits
104 max=65536,
105 description="Custom target tile size for CPU rendering",
106 update=_update_tile_size
108 bi_custom = IntProperty(
109 name="Target Size",
110 default=64,
111 min=8, # same as blender's own limits
112 max=65536,
113 description="Custom target tile size",
114 update=_update_tile_size
116 target_type = EnumProperty(
117 name="Target tile size",
118 items=(
119 ('po2', "Po2", "A choice between powers of 2 (16, 32, 64...)"),
120 ('custom', "Custom", "Choose any number as the tile size target")),
121 default='po2',
122 description="Method of choosing the target tile size",
123 update=_update_tile_size
125 use_optimal = BoolProperty(
126 name="Optimal Tiles",
127 default=True,
128 description="Try to find a similar tile size for best performance, "
129 "instead of using exact selected one",
130 update=_update_tile_size
132 is_enabled = BoolProperty(
133 name="Auto Tile Size",
134 default=True,
135 description="Calculate the best tile size based on factors of the "
136 "render size and the chosen target",
137 update=_update_tile_size
139 use_advanced_ui = BoolProperty(
140 name="Advanced Settings",
141 default=False,
142 description="Show extra options for more control over the calculated tile size"
144 thread_error_correct = BoolProperty(
145 name="Fix",
146 default=True,
147 description="Reduce the tile size so that all your available threads are used",
148 update=_update_tile_size
151 # Internally used props (not for GUI)
152 first_run = BoolProperty(
153 default=True,
154 options={'HIDDEN'}
156 threads_error = BoolProperty(
157 options={'HIDDEN'}
159 num_tiles = IntVectorProperty(
160 default=(0, 0),
161 size=2,
162 options={'HIDDEN'}
164 prev_choice = StringProperty(
165 default='',
166 options={'HIDDEN'}
168 prev_engine = StringProperty(
169 default='',
170 options={'HIDDEN'}
172 prev_device = StringProperty(
173 default='',
174 options={'HIDDEN'}
176 prev_res = IntVectorProperty(
177 default=(0, 0),
178 size=2,
179 options={'HIDDEN'}
181 prev_border = BoolProperty(
182 default=False,
183 options={'HIDDEN'}
185 prev_border_res = FloatVectorProperty(
186 default=(0, 0, 0, 0),
187 size=4,
188 options={'HIDDEN'}
190 prev_actual_tile_size = IntVectorProperty(
191 default=(0, 0),
192 size=2,
193 options={'HIDDEN'}
195 prev_threads = IntProperty(
196 default=0,
197 options={'HIDDEN'}
201 def ats_poll(context):
202 scene = context.scene
203 if scene.render.engine not in SUPPORTED_RENDER_ENGINES or not scene.ats_settings.is_enabled:
204 return False
205 return True
208 def engine_is_gpu(engine, device, userpref):
209 if engine == 'CYCLES' and device == 'GPU':
210 return userpref.addons['cycles'].preferences.has_active_device()
211 return False
214 def get_tilesize_prop(engine, device, userpref):
215 target_type = "_choice" if bpy.context.scene.ats_settings.target_type == 'po2' else "_custom"
216 if engine_is_gpu(engine, device, userpref):
217 return ("gpu" + target_type)
218 elif engine == 'CYCLES':
219 return ("cpu" + target_type)
220 return ("bi" + target_type)
223 @persistent
224 def on_scene_update(scene):
225 context = bpy.context
227 if not ats_poll(context):
228 return
230 userpref = context.user_preferences
232 settings = scene.ats_settings
233 render = scene.render
234 engine = render.engine
236 # scene.cycles might not always exist (Cycles is an addon)...
237 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
238 border = render.use_border
239 threads = get_threads(context, device)
241 choice = getattr(settings, get_tilesize_prop(engine, device, userpref))
243 res = get_actual_res(render)
244 actual_ts = (render.tile_x, render.tile_y)
245 border_res = (render.border_min_x, render.border_min_y, render.border_max_x, render.border_max_y)
247 # detect relevant changes in scene
248 do_change = (engine != settings.prev_engine or
249 device != settings.prev_device or
250 border != settings.prev_border or
251 threads != settings.prev_threads or
252 str(choice) != settings.prev_choice or
253 res != settings.prev_res[:] or
254 border_res != settings.prev_border_res[:] or
255 actual_ts != settings.prev_actual_tile_size[:])
256 if do_change:
257 do_set_tile_size(context)
260 def get_actual_res(render):
261 rend_percent = render.resolution_percentage * 0.01
262 # floor is implicitly done by int conversion...
263 return (int(render.resolution_x * rend_percent), int(render.resolution_y * rend_percent))
266 def get_threads(context, device):
267 render = context.scene.render
268 engine = render.engine
269 userpref = context.user_preferences
271 if engine_is_gpu(engine, device, userpref):
272 threads = userpref.addons['cycles'].preferences.get_num_gpu_devices()
273 else:
274 threads = render.threads
276 return threads
279 def max_tile_size(threads, xres, yres):
280 ''' Give the largest tile size that will still use all threads '''
282 render_area = xres * yres
283 tile_area = render_area / threads
284 tile_length = sqrt(tile_area)
286 # lists: num x tiles, num y tiles, squareness, total tiles
287 perfect_attempts = [] # attempts with correct number of tiles
288 attempts = [] # all attempts, even if incorrect number of tiles
290 axes = [xres, yres]
291 funcs = [floor, ceil]
293 for axis in axes:
294 sec_axis = yres if axis == xres else xres
295 for func in funcs:
296 primary = func(axis / tile_length)
297 if primary > 0:
298 secondary = threads / primary
299 ts_p = axis / primary
300 ts_s = sec_axis / secondary
301 squareness = max(ts_p, ts_s) - min(ts_p, ts_s)
302 attempt = [primary if axis == xres else secondary, primary if
303 axis != xres else secondary, squareness, primary * secondary]
304 if attempt not in attempts:
305 attempts.append(attempt)
306 # will only be an integer if there are the right number of tiles
307 if secondary.is_integer():
308 perfect_attempts.append(attempt)
310 if perfect_attempts: # prefer to use attempt that has exactly the right number of tiles
311 attempts = perfect_attempts
313 attempt = sorted(attempts, key=lambda k: k[2])[0] # pick set with most square tiles
314 numtiles_x = round(attempt[0])
315 numtiles_y = round(attempt[1])
316 tile_x = ceil(xres / numtiles_x)
317 tile_y = ceil(yres / numtiles_y)
319 return (tile_x, tile_y)
322 def do_set_tile_size(context):
323 if not ats_poll(context):
324 return False
326 scene = context.scene
327 userpref = context.user_preferences
329 settings = scene.ats_settings
330 render = scene.render
331 engine = render.engine
332 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
333 border = render.use_border
335 realxres, realyres = xres, yres = res = get_actual_res(scene.render)
337 if border:
338 xres = round(xres * (render.border_max_x - render.border_min_x))
339 yres = round(yres * (render.border_max_y - render.border_min_y))
341 choice = getattr(settings, get_tilesize_prop(engine, device, userpref))
342 target = int(choice)
344 numtiles_x = ceil(xres / target)
345 numtiles_y = ceil(yres / target)
346 settings.num_tiles = (numtiles_x, numtiles_y)
347 if settings.use_optimal:
348 tile_x = ceil(xres / numtiles_x)
349 tile_y = ceil(yres / numtiles_y)
350 else:
351 tile_x = target
352 tile_y = target
354 # Print tile size (for debug purposes)
355 # print("Tile size: %dx%d (%dx%d tiles)" % (tile_x, tile_y, ceil(xres / tile_x), ceil(yres / tile_y)))
357 # Detect if there are fewer tiles than available threads
358 threads = get_threads(context, device)
359 if ((numtiles_x * numtiles_y) < threads):
360 settings.threads_error = True
361 if settings.thread_error_correct:
362 tile_x, tile_y = max_tile_size(threads, xres, yres)
363 settings.num_tiles = (ceil(xres / tile_x), ceil(yres / tile_y))
364 else:
365 settings.threads_error = False
367 # Make sure tile sizes are within the internal limit
368 tile_x = max(8, tile_x)
369 tile_y = max(8, tile_y)
370 tile_x = min(65536, tile_x)
371 tile_y = min(65536, tile_y)
373 render.tile_x = tile_x
374 render.tile_y = tile_y
376 settings.prev_engine = engine
377 settings.prev_device = device
378 settings.prev_border = border
379 settings.prev_threads = threads
380 settings.prev_choice = str(choice)
381 settings.prev_res = res
382 settings.prev_border_res = (render.border_min_x, render.border_min_y,
383 render.border_max_x, render.border_max_y)
384 settings.prev_actual_tile_size = (tile_x, tile_y)
385 settings.first_run = False
387 return True
390 class SetTileSize(Operator):
391 bl_idname = "render.autotilesize_set"
392 bl_label = "Set"
393 bl_description = "The first render may not obey the tile-size set here"
395 @classmethod
396 def poll(clss, context):
397 return ats_poll(context)
399 def execute(self, context):
400 if do_set_tile_size(context):
401 return {'FINISHED'}
402 return {'CANCELLED'}
405 # ##### INTERFACE #####
407 def ui_layout(engine, layout, context):
408 scene = context.scene
409 userpref = context.user_preferences
411 settings = scene.ats_settings
412 render = scene.render
413 engine = render.engine
414 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
416 col = layout.column(align=True)
417 sub = col.column(align=True)
418 row = sub.row(align=True)
419 row.prop(settings, "is_enabled", toggle=True)
420 row.prop(settings, "use_advanced_ui", toggle=True, text="", icon='PREFERENCES')
422 sub = col.column(align=False)
423 sub.enabled = settings.is_enabled
425 if settings.use_advanced_ui:
426 row = sub.row(align=True)
427 row.label("Target tile size:")
428 row.separator()
429 row.prop(settings, "target_type", expand=True)
431 row = sub.row(align=True)
432 row.prop(settings, get_tilesize_prop(engine, device, userpref), expand=True)
433 sub.prop(settings, "use_optimal", text="Calculate Optimal Size")
435 sub.label("Number of tiles: %s x %s (Total: %s)" %
436 (settings.num_tiles[0], settings.num_tiles[1],
437 settings.num_tiles[0] * settings.num_tiles[1])
440 if settings.first_run:
441 sub = layout.column(align=True)
442 sub.operator("render.autotilesize_set", text="First-render fix", icon='ERROR')
443 elif settings.prev_device != device:
444 sub = layout.column(align=True)
445 sub.operator("render.autotilesize_set", text="Device changed - fix", icon='ERROR')
447 # if not very square tile
448 if (render.tile_x / render.tile_y > 2) or (render.tile_x / render.tile_y < 0.5):
449 sub.label(text="Warning: Tile size is not very square", icon='ERROR')
450 sub.label(text=" Try a slightly different resolution")
452 if settings.threads_error:
453 row = sub.row(align=True)
454 row.alignment = 'CENTER'
455 row.label(text="Warning: Fewer tiles than threads", icon='ERROR')
456 row.prop(settings, 'thread_error_correct')
459 def menu_func_cycles(self, context):
460 ui_layout('CYCLES', self.layout, context)
463 def menu_func_bi(self, context):
464 ui_layout('BLENDER_RENDER', self.layout, context)
467 # ##### REGISTRATION #####
469 def register():
470 bpy.utils.register_module(__name__)
472 bpy.types.Scene.ats_settings = PointerProperty(
473 type=AutoTileSizeSettings
476 # Note, the Cycles addon must be registered first, otherwise
477 # this panel doesn't exist - better be safe here!
478 cycles_panel = getattr(bpy.types, "CYCLES_RENDER_PT_performance", None)
479 if cycles_panel is not None:
480 cycles_panel.append(menu_func_cycles)
482 bpy.types.RENDER_PT_performance.append(menu_func_bi)
483 bpy.app.handlers.scene_update_post.append(on_scene_update)
486 def unregister():
487 bpy.app.handlers.scene_update_post.remove(on_scene_update)
488 bpy.types.RENDER_PT_performance.remove(menu_func_bi)
490 cycles_panel = getattr(bpy.types, "CYCLES_RENDER_PT_performance", None)
491 if cycles_panel is not None:
492 cycles_panel.remove(menu_func_cycles)
494 del bpy.types.Scene.ats_settings
496 bpy.utils.unregister_module(__name__)
499 if __name__ == "__main__":
500 register()