Pose Library: update for rename of asset_library to asset_library_ref
[blender-addons.git] / render_auto_tile_size.py
blob078513c6ee7ea9f3a58ecd7614f1c337b84c6535
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, 3),
24 "blender": (2, 80, 0),
25 "location": "Render Settings > Performance",
26 "warning": "",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/render/auto_tile_size.html",
28 "category": "Render",
32 import bpy
33 from bpy.types import (
34 Operator,
35 PropertyGroup,
37 from bpy.props import (
38 BoolProperty,
39 EnumProperty,
40 FloatVectorProperty,
41 IntProperty,
42 IntVectorProperty,
43 StringProperty,
44 PointerProperty,
46 from bpy.app.handlers import persistent
47 from math import (
48 ceil, floor,
49 sqrt,
53 SUPPORTED_RENDER_ENGINES = {'CYCLES', 'BLENDER_RENDER'}
54 TILE_SIZES = (
55 ('16', "16", "16 x 16"),
56 ('32', "32", "32 x 32"),
57 ('64', "64", "64 x 64"),
58 ('128', "128", "128 x 128"),
59 ('256', "256", "256 x 256"),
60 ('512', "512", "512 x 512"),
61 ('1024', "1024", "1024 x 1024"),
65 def _update_tile_size(self, context):
66 do_set_tile_size(context)
69 class AutoTileSizeSettings(PropertyGroup):
70 gpu_choice: EnumProperty(
71 name="Target GPU Tile Size",
72 items=TILE_SIZES,
73 default='256',
74 description="Square dimensions of tiles for GPU rendering",
75 update=_update_tile_size
77 cpu_choice: EnumProperty(
78 name="Target CPU Tile Size",
79 items=TILE_SIZES,
80 default='32',
81 description="Square dimensions of tiles for CPU rendering",
82 update=_update_tile_size
84 bi_choice: EnumProperty(
85 name="Target CPU Tile Size",
86 items=TILE_SIZES,
87 default='64',
88 description="Square dimensions of tiles",
89 update=_update_tile_size
91 gpu_custom: IntProperty(
92 name="Target Size",
93 default=256,
94 min=8, # same as blender's own limits
95 max=65536,
96 description="Custom target tile size for GPU rendering",
97 update=_update_tile_size
99 cpu_custom: IntProperty(
100 name="Target Size",
101 default=32,
102 min=8, # same as blender's own limits
103 max=65536,
104 description="Custom target tile size for CPU rendering",
105 update=_update_tile_size
107 bi_custom: IntProperty(
108 name="Target Size",
109 default=64,
110 min=8, # same as blender's own limits
111 max=65536,
112 description="Custom target tile size",
113 update=_update_tile_size
115 target_type: EnumProperty(
116 name="Target tile size",
117 items=(
118 ('po2', "Po2", "A choice between powers of 2 (16, 32, 64...)"),
119 ('custom', "Custom", "Choose any number as the tile size target")),
120 default='po2',
121 description="Method of choosing the target tile size",
122 update=_update_tile_size
124 use_optimal: BoolProperty(
125 name="Optimal Tiles",
126 default=True,
127 description="Try to find a similar tile size for best performance, "
128 "instead of using exact selected one",
129 update=_update_tile_size
131 is_enabled: BoolProperty(
132 name="Auto Tile Size",
133 default=True,
134 description="Calculate the best tile size based on factors of the "
135 "render size and the chosen target",
136 update=_update_tile_size
138 use_advanced_ui: BoolProperty(
139 name="Advanced Settings",
140 default=False,
141 description="Show extra options for more control over the calculated tile size"
143 thread_error_correct: BoolProperty(
144 name="Fix",
145 default=True,
146 description="Reduce the tile size so that all your available threads are used",
147 update=_update_tile_size
150 # Internally used props (not for GUI)
151 first_run: BoolProperty(
152 default=True,
153 options={'HIDDEN'}
155 threads_error: BoolProperty(
156 options={'HIDDEN'}
158 num_tiles: IntVectorProperty(
159 default=(0, 0),
160 size=2,
161 options={'HIDDEN'}
163 prev_choice: StringProperty(
164 default='',
165 options={'HIDDEN'}
167 prev_engine: StringProperty(
168 default='',
169 options={'HIDDEN'}
171 prev_device: StringProperty(
172 default='',
173 options={'HIDDEN'}
175 prev_res: IntVectorProperty(
176 default=(0, 0),
177 size=2,
178 options={'HIDDEN'}
180 prev_border: BoolProperty(
181 default=False,
182 options={'HIDDEN'}
184 prev_border_res: FloatVectorProperty(
185 default=(0, 0, 0, 0),
186 size=4,
187 options={'HIDDEN'}
189 prev_actual_tile_size: IntVectorProperty(
190 default=(0, 0),
191 size=2,
192 options={'HIDDEN'}
194 prev_threads: IntProperty(
195 default=0,
196 options={'HIDDEN'}
200 def ats_poll(context):
201 scene = context.scene
202 if scene.render.engine not in SUPPORTED_RENDER_ENGINES or not scene.ats_settings.is_enabled:
203 return False
204 return True
207 def engine_is_gpu(engine, device, userpref):
208 if engine == 'CYCLES' and device == 'GPU':
209 return userpref.addons['cycles'].preferences.has_active_device()
210 return False
213 def get_tilesize_prop(engine, device, userpref):
214 target_type = "_choice" if bpy.context.scene.ats_settings.target_type == 'po2' else "_custom"
215 if engine_is_gpu(engine, device, userpref):
216 return ("gpu" + target_type)
217 elif engine == 'CYCLES':
218 return ("cpu" + target_type)
219 return ("bi" + target_type)
222 @persistent
223 def on_scene_update(scene):
224 context = bpy.context
226 if not ats_poll(context):
227 return
229 userpref = context.preferences
231 settings = scene.ats_settings
232 render = scene.render
233 engine = render.engine
235 # scene.cycles might not always exist (Cycles is an addon)...
236 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
237 border = render.use_border
238 threads = get_threads(context, device)
240 choice = getattr(settings, get_tilesize_prop(engine, device, userpref))
242 res = get_actual_res(render)
243 actual_ts = (render.tile_x, render.tile_y)
244 border_res = (render.border_min_x, render.border_min_y, render.border_max_x, render.border_max_y)
246 # detect relevant changes in scene
247 do_change = (engine != settings.prev_engine or
248 device != settings.prev_device or
249 border != settings.prev_border or
250 threads != settings.prev_threads or
251 str(choice) != settings.prev_choice or
252 res != settings.prev_res[:] or
253 border_res != settings.prev_border_res[:] or
254 actual_ts != settings.prev_actual_tile_size[:])
255 if do_change:
256 do_set_tile_size(context)
259 def get_actual_res(render):
260 rend_percent = render.resolution_percentage * 0.01
261 # floor is implicitly done by int conversion...
262 return (int(render.resolution_x * rend_percent), int(render.resolution_y * rend_percent))
265 def get_threads(context, device):
266 render = context.scene.render
267 engine = render.engine
268 userpref = context.preferences
270 if engine_is_gpu(engine, device, userpref):
271 threads = userpref.addons['cycles'].preferences.get_num_gpu_devices()
272 else:
273 threads = render.threads
275 return threads
278 def max_tile_size(threads, xres, yres):
279 ''' Give the largest tile size that will still use all threads '''
281 render_area = xres * yres
282 tile_area = render_area / threads
283 tile_length = sqrt(tile_area)
285 # lists: num x tiles, num y tiles, squareness, total tiles
286 perfect_attempts = [] # attempts with correct number of tiles
287 attempts = [] # all attempts, even if incorrect number of tiles
289 axes = [xres, yres]
290 funcs = [floor, ceil]
292 for axis in axes:
293 sec_axis = yres if axis == xres else xres
294 for func in funcs:
295 primary = func(axis / tile_length)
296 if primary > 0:
297 secondary = threads / primary
298 ts_p = axis / primary
299 ts_s = sec_axis / secondary
300 squareness = max(ts_p, ts_s) - min(ts_p, ts_s)
301 attempt = [primary if axis == xres else secondary, primary if
302 axis != xres else secondary, squareness, primary * secondary]
303 if attempt not in attempts:
304 attempts.append(attempt)
305 # will only be an integer if there are the right number of tiles
306 if secondary.is_integer():
307 perfect_attempts.append(attempt)
309 if perfect_attempts: # prefer to use attempt that has exactly the right number of tiles
310 attempts = perfect_attempts
312 attempt = sorted(attempts, key=lambda k: k[2])[0] # pick set with most square tiles
313 numtiles_x = round(attempt[0])
314 numtiles_y = round(attempt[1])
315 tile_x = ceil(xres / numtiles_x)
316 tile_y = ceil(yres / numtiles_y)
318 return (tile_x, tile_y)
321 def do_set_tile_size(context):
322 if not ats_poll(context):
323 return False
325 scene = context.scene
326 userpref = context.preferences
328 settings = scene.ats_settings
329 render = scene.render
330 engine = render.engine
331 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
332 border = render.use_border
334 realxres, realyres = xres, yres = res = get_actual_res(scene.render)
336 if border:
337 xres = round(xres * (render.border_max_x - render.border_min_x))
338 yres = round(yres * (render.border_max_y - render.border_min_y))
340 choice = getattr(settings, get_tilesize_prop(engine, device, userpref))
341 target = int(choice)
343 numtiles_x = ceil(xres / target)
344 numtiles_y = ceil(yres / target)
345 settings.num_tiles = (numtiles_x, numtiles_y)
346 if settings.use_optimal:
347 tile_x = ceil(xres / numtiles_x)
348 tile_y = ceil(yres / numtiles_y)
349 else:
350 tile_x = target
351 tile_y = target
353 # Print tile size (for debug purposes)
354 # print("Tile size: %dx%d (%dx%d tiles)" % (tile_x, tile_y, ceil(xres / tile_x), ceil(yres / tile_y)))
356 # Detect if there are fewer tiles than available threads
357 threads = get_threads(context, device)
358 if ((numtiles_x * numtiles_y) < threads):
359 settings.threads_error = True
360 if settings.thread_error_correct:
361 tile_x, tile_y = max_tile_size(threads, xres, yres)
362 settings.num_tiles = (ceil(xres / tile_x), ceil(yres / tile_y))
363 else:
364 settings.threads_error = False
366 # Make sure tile sizes are within the internal limit
367 tile_x = max(8, tile_x)
368 tile_y = max(8, tile_y)
369 tile_x = min(65536, tile_x)
370 tile_y = min(65536, tile_y)
372 render.tile_x = tile_x
373 render.tile_y = tile_y
375 settings.prev_engine = engine
376 settings.prev_device = device
377 settings.prev_border = border
378 settings.prev_threads = threads
379 settings.prev_choice = str(choice)
380 settings.prev_res = res
381 settings.prev_border_res = (render.border_min_x, render.border_min_y,
382 render.border_max_x, render.border_max_y)
383 settings.prev_actual_tile_size = (tile_x, tile_y)
384 settings.first_run = False
386 return True
389 class SetTileSize(Operator):
390 bl_idname = "render.autotilesize_set"
391 bl_label = "Set"
392 bl_description = "The first render may not obey the tile-size set here"
394 @classmethod
395 def poll(clss, context):
396 return ats_poll(context)
398 def execute(self, context):
399 if do_set_tile_size(context):
400 return {'FINISHED'}
401 return {'CANCELLED'}
404 # ##### INTERFACE #####
406 def ui_layout(engine, layout, context):
407 scene = context.scene
408 userpref = context.preferences
410 settings = scene.ats_settings
411 render = scene.render
412 engine = render.engine
413 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
415 col = layout.column(align=True)
416 sub = col.column(align=True)
417 row = sub.row(align=True)
418 row.prop(settings, "is_enabled", toggle=True)
419 row.prop(settings, "use_advanced_ui", toggle=True, text="", icon='PREFERENCES')
421 sub = col.column(align=False)
422 sub.enabled = settings.is_enabled
424 if settings.use_advanced_ui:
425 row = sub.row(align=True)
426 row.label(text="Target tile size:")
427 row.separator()
428 row.prop(settings, "target_type", expand=True)
430 row = sub.row(align=True)
431 row.prop(settings, get_tilesize_prop(engine, device, userpref), expand=True)
432 sub.prop(settings, "use_optimal", text="Calculate Optimal Size")
434 sub.label(text="Number of tiles: %s x %s (Total: %s)" %
435 (settings.num_tiles[0], settings.num_tiles[1],
436 settings.num_tiles[0] * settings.num_tiles[1])
439 if settings.first_run:
440 sub = layout.column(align=True)
441 sub.operator("render.autotilesize_set", text="First-render fix", icon='ERROR')
442 elif settings.prev_device != device:
443 sub = layout.column(align=True)
444 sub.operator("render.autotilesize_set", text="Device changed - fix", icon='ERROR')
446 # if not very square tile
447 if (render.tile_x / render.tile_y > 2) or (render.tile_x / render.tile_y < 0.5):
448 sub.label(text="Warning: Tile size is not very square", icon='ERROR')
449 sub.label(text=" Try a slightly different resolution")
451 if settings.threads_error:
452 row = sub.row(align=True)
453 row.alignment = 'CENTER'
454 row.label(text="Warning: Fewer tiles than threads", icon='ERROR')
455 row.prop(settings, 'thread_error_correct')
458 def menu_func_cycles(self, context):
459 ui_layout('CYCLES', self.layout, context)
462 # ##### REGISTRATION #####
464 classes = (
465 AutoTileSizeSettings,
466 SetTileSize
469 def register():
470 for cls in classes:
471 bpy.utils.register_class(cls)
473 bpy.types.Scene.ats_settings = PointerProperty(
474 type=AutoTileSizeSettings
477 # Note, the Cycles addon must be registered first, otherwise
478 # this panel doesn't exist - better be safe here!
479 cycles_panel = getattr(bpy.types, "CYCLES_RENDER_PT_performance", None)
480 if cycles_panel is not None:
481 cycles_panel.append(menu_func_cycles)
483 bpy.app.handlers.depsgraph_update_post.append(on_scene_update)
486 def unregister():
487 bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
489 cycles_panel = getattr(bpy.types, "CYCLES_RENDER_PT_performance", None)
490 if cycles_panel is not None:
491 cycles_panel.remove(menu_func_cycles)
493 del bpy.types.Scene.ats_settings
495 for cls in reversed(classes):
496 bpy.utils.unregister_class(cls)
499 if __name__ == "__main__":
500 register()