Update 3d-print toolbox to only export selection
[blender-addons.git] / render_auto_tile_size.py
blob8aef71dcc21eefaf34347280d6483ed79554d790
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, 1),
24 "blender": (2, 74, 0),
25 "location": "Render Settings > Performance",
26 "warning": "",
27 "wiki_url": "http://wiki.blender.org/index.php?title=Extensions:2.6/Py/Scripts/Render/Auto_Tile_Size",
28 "category": "Render",
32 import bpy
33 from bpy.app.handlers import persistent
34 from math import ceil, floor, sqrt
37 SUPPORTED_RENDER_ENGINES = {'CYCLES', 'BLENDER_RENDER'}
38 TILE_SIZES = (
39 ('16', "16", "16 x 16"),
40 ('32', "32", "32 x 32"),
41 ('64', "64", "64 x 64"),
42 ('128', "128", "128 x 128"),
43 ('256', "256", "256 x 256"),
44 ('512', "512", "512 x 512"),
45 ('1024', "1024", "1024 x 1024"),
49 def _update_tile_size(self, context):
50 do_set_tile_size(context)
53 class AutoTileSizeSettings(bpy.types.PropertyGroup):
54 gpu_choice = bpy.props.EnumProperty(
55 name="Target GPU Tile Size",
56 items=TILE_SIZES,
57 default='256',
58 description="Square dimensions of tiles for GPU rendering",
59 update=_update_tile_size)
60 cpu_choice = bpy.props.EnumProperty(
61 name="Target CPU Tile Size",
62 items=TILE_SIZES,
63 default='32',
64 description="Square dimensions of tiles for CPU rendering",
65 update=_update_tile_size)
66 bi_choice = bpy.props.EnumProperty(
67 name="Target CPU Tile Size",
68 items=TILE_SIZES,
69 default='64',
70 description="Square dimensions of tiles",
71 update=_update_tile_size)
73 gpu_custom = bpy.props.IntProperty(
74 name="Target Size",
75 default=256,
76 min=8, # same as blender's own limits
77 max=65536,
78 description="Custom target tile size for GPU rendering",
79 update=_update_tile_size)
80 cpu_custom = bpy.props.IntProperty(
81 name="Target Size",
82 default=32,
83 min=8, # same as blender's own limits
84 max=65536,
85 description="Custom target tile size for CPU rendering",
86 update=_update_tile_size)
87 bi_custom = bpy.props.IntProperty(
88 name="Target Size",
89 default=64,
90 min=8, # same as blender's own limits
91 max=65536,
92 description="Custom target tile size",
93 update=_update_tile_size)
95 target_type = bpy.props.EnumProperty(
96 name="Target tile size",
97 items=(
98 ('po2', "Po2", "A choice between powers of 2 (16, 32, 64...)"),
99 ('custom', "Custom", "Choose any number as the tile size target")),
100 default='po2',
101 description="Method of choosing the target tile size",
102 update=_update_tile_size)
104 use_optimal = bpy.props.BoolProperty(
105 name="Optimal Tiles",
106 default=True,
107 description="Try to find a similar tile size for best performance, instead of using exact selected one",
108 update=_update_tile_size)
110 is_enabled = bpy.props.BoolProperty(
111 name="Auto Tile Size",
112 default=True,
113 description="Calculate the best tile size based on factors of the render size and the chosen target",
114 update=_update_tile_size)
116 use_advanced_ui = bpy.props.BoolProperty(
117 name="Advanced Settings",
118 default=False,
119 description="Show extra options for more control over the calculated tile size")
121 thread_error_correct = bpy.props.BoolProperty(
122 name="Fix",
123 default=True,
124 description="Reduce the tile size so that all your available threads are used",
125 update=_update_tile_size)
127 # Internally used props (not for GUI)
128 first_run = bpy.props.BoolProperty(default=True, options={'HIDDEN'})
129 threads_error = bpy.props.BoolProperty(options={'HIDDEN'})
130 num_tiles = bpy.props.IntVectorProperty(default=(0, 0), size=2, options={'HIDDEN'})
131 prev_choice = bpy.props.StringProperty(default='', options={'HIDDEN'})
132 prev_engine = bpy.props.StringProperty(default='', options={'HIDDEN'})
133 prev_device = bpy.props.StringProperty(default='', options={'HIDDEN'})
134 prev_res = bpy.props.IntVectorProperty(default=(0, 0), size=2, options={'HIDDEN'})
135 prev_border = bpy.props.BoolProperty(default=False, options={'HIDDEN'})
136 prev_border_res = bpy.props.FloatVectorProperty(default=(0, 0, 0, 0), size=4, options={'HIDDEN'})
137 prev_actual_tile_size = bpy.props.IntVectorProperty(default=(0, 0), size=2, options={'HIDDEN'})
138 prev_threads = bpy.props.IntProperty(default=0, options={'HIDDEN'})
141 def ats_poll(context):
142 scene = context.scene
143 if scene.render.engine not in SUPPORTED_RENDER_ENGINES or not scene.ats_settings.is_enabled:
144 return False
145 return True
148 def engine_is_gpu(engine, device, userpref):
149 return engine == 'CYCLES' and device == 'GPU' and userpref.system.compute_device_type != 'NONE'
152 def get_tilesize_prop(engine, device, userpref):
153 target_type = "_choice" if bpy.context.scene.ats_settings.target_type == 'po2' else "_custom"
154 if engine_is_gpu(engine, device, userpref):
155 return ("gpu" + target_type)
156 elif engine == 'CYCLES':
157 return ("cpu" + target_type)
158 return ("bi" + target_type)
161 @persistent
162 def on_scene_update(scene):
163 context = bpy.context
165 if not ats_poll(context):
166 return
168 userpref = context.user_preferences
170 settings = scene.ats_settings
171 render = scene.render
172 engine = render.engine
174 # scene.cycles might not always exist (Cycles is an addon)...
175 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
176 border = render.use_border
177 threads = get_threads(context, device)
179 choice = getattr(settings, get_tilesize_prop(engine, device, userpref))
181 res = get_actual_res(render)
182 actual_ts = (render.tile_x, render.tile_y)
183 border_res = (render.border_min_x, render.border_min_y, render.border_max_x, render.border_max_y)
185 # detect relevant changes in scene
186 do_change = (engine != settings.prev_engine or
187 device != settings.prev_device or
188 border != settings.prev_border or
189 threads != settings.prev_threads or
190 str(choice) != settings.prev_choice or
191 res != settings.prev_res[:] or
192 border_res != settings.prev_border_res[:] or
193 actual_ts != settings.prev_actual_tile_size[:])
194 if do_change:
195 do_set_tile_size(context)
198 def get_actual_res(render):
199 rend_percent = render.resolution_percentage * 0.01
200 # floor is implicitly done by int conversion...
201 return (int(render.resolution_x * rend_percent), int(render.resolution_y * rend_percent))
203 def get_threads(context, device):
204 render = context.scene.render
205 engine = render.engine
206 userpref = context.user_preferences
208 if engine_is_gpu(engine, device, userpref):
209 gpu_device_str = userpref.system.compute_device
210 if 'MULTI' in gpu_device_str:
211 threads = int(gpu_device_str.split('_')[-1])
212 else:
213 threads = 1
214 else:
215 threads = render.threads
217 return threads
219 def max_tile_size(threads, xres, yres):
220 ''' Give the largest tile size that will still use all threads '''
222 render_area = xres * yres
223 tile_area = render_area / threads
224 tile_length = sqrt(tile_area)
226 # lists: num x tiles, num y tiles, squareness, total tiles
227 perfect_attempts = [] # attempts with correct number of tiles
228 attempts = [] # all attempts, even if incorrect number of tiles
230 axes = [xres, yres]
231 funcs = [floor, ceil]
233 for axis in axes:
234 sec_axis = yres if axis == xres else xres
235 for func in funcs:
236 primary = func(axis / tile_length)
237 if primary > 0:
238 secondary = threads / primary
239 ts_p = axis/primary
240 ts_s = sec_axis/secondary
241 squareness = max(ts_p, ts_s) - min(ts_p, ts_s)
242 attempt = [primary if axis == xres else secondary, primary if axis != xres else secondary, squareness, primary * secondary]
243 if attempt not in attempts:
244 attempts.append(attempt)
245 if secondary.is_integer(): # will only be an integer if there are the right number of tiles
246 perfect_attempts.append(attempt)
248 if perfect_attempts: # prefer to use attempt that has exactly the right number of tiles
249 attempts = perfect_attempts
251 attempt = sorted(attempts, key=lambda k: k[2])[0] # pick set with most square tiles
252 numtiles_x = round(attempt[0])
253 numtiles_y = round(attempt[1])
254 tile_x = ceil(xres / numtiles_x)
255 tile_y = ceil(yres / numtiles_y)
257 return (tile_x, tile_y)
259 def do_set_tile_size(context):
260 if not ats_poll(context):
261 return False
263 scene = context.scene
264 userpref = context.user_preferences
266 settings = scene.ats_settings
267 render = scene.render
268 engine = render.engine
269 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
270 border = render.use_border
272 realxres, realyres = xres, yres = res = get_actual_res(scene.render)
274 if border:
275 xres = round(xres * (render.border_max_x - render.border_min_x))
276 yres = round(yres * (render.border_max_y - render.border_min_y))
278 choice = getattr(settings, get_tilesize_prop(engine, device, userpref))
279 target = int(choice)
281 numtiles_x = ceil(xres / target)
282 numtiles_y = ceil(yres / target)
283 settings.num_tiles = (numtiles_x, numtiles_y)
284 if settings.use_optimal:
285 tile_x = ceil(xres / numtiles_x)
286 tile_y = ceil(yres / numtiles_y)
287 else:
288 tile_x = target
289 tile_y = target
291 # Print tile size (for debug purposes)
292 # print("Tile size: %dx%d (%dx%d tiles)" % (tile_x, tile_y, ceil(xres / tile_x), ceil(yres / tile_y)))
294 # Detect if there are fewer tiles than available threads
295 threads = get_threads(context, device)
296 if ((numtiles_x * numtiles_y) < threads):
297 settings.threads_error = True
298 if settings.thread_error_correct:
299 tile_x, tile_y = max_tile_size(threads, xres, yres)
300 settings.num_tiles = (ceil(xres/tile_x), ceil(yres/tile_y))
301 else:
302 settings.threads_error = False
304 # Make sure tile sizes are within the internal limit
305 tile_x = max(8, tile_x)
306 tile_y = max(8, tile_y)
307 tile_x = min(65536, tile_x)
308 tile_y = min(65536, tile_y)
310 render.tile_x = tile_x
311 render.tile_y = tile_y
313 settings.prev_engine = engine
314 settings.prev_device = device
315 settings.prev_border = border
316 settings.prev_threads = threads
317 settings.prev_choice = str(choice)
318 settings.prev_res = res
319 settings.prev_border_res = (render.border_min_x, render.border_min_y, render.border_max_x, render.border_max_y)
320 settings.prev_actual_tile_size = (tile_x, tile_y)
321 settings.first_run = False
323 return True
326 class SetTileSize(bpy.types.Operator):
327 """The first render may not obey the tile-size set here"""
328 bl_idname = "render.autotilesize_set"
329 bl_label = "Set"
331 @classmethod
332 def poll(clss, context):
333 return ats_poll(context)
335 def execute(self, context):
336 if do_set_tile_size(context):
337 return {'FINISHED'}
338 return {'CANCELLED'}
341 # ##### INTERFACE #####
343 def ui_layout(engine, layout, context):
344 scene = context.scene
345 userpref = context.user_preferences
347 settings = scene.ats_settings
348 render = scene.render
349 engine = render.engine
350 device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device
352 col = layout.column(align=True)
353 sub = col.column(align=True)
354 row = sub.row(align=True)
355 row.prop(settings, "is_enabled", toggle=True)
356 row.prop(settings, "use_advanced_ui", toggle=True, text="", icon='PREFERENCES')
358 sub = col.column(align=False)
359 sub.enabled = settings.is_enabled
361 if settings.use_advanced_ui:
362 row = sub.row(align=True)
363 row.label("Target tile size:")
364 row.separator()
365 row.prop(settings, "target_type", expand=True)
367 row = sub.row(align=True)
368 row.prop(settings, get_tilesize_prop(engine, device, userpref), expand=True)
369 sub.prop(settings, "use_optimal", text="Calculate Optimal Size")
371 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]))
373 if settings.first_run:
374 sub = layout.column(align=True)
375 sub.operator("render.autotilesize_set", text="First-render fix", icon='ERROR')
376 elif settings.prev_device != device:
377 sub = layout.column(align=True)
378 sub.operator("render.autotilesize_set", text="Device changed - fix", icon='ERROR')
380 if (render.tile_x / render.tile_y > 2) or (render.tile_x / render.tile_y < 0.5): # if not very square tile
381 sub.label(text="Warning: Tile size is not very square", icon='ERROR')
382 sub.label(text=" Try a slightly different resolution")
383 if settings.threads_error:
384 row = sub.row(align=True)
385 row.alignment = 'CENTER'
386 row.label(text="Warning: Fewer tiles than threads", icon='ERROR')
387 row.prop(settings, 'thread_error_correct')
390 def menu_func_cycles(self, context):
391 ui_layout('CYCLES', self.layout, context)
394 def menu_func_bi(self, context):
395 ui_layout('BLENDER_RENDER', self.layout, context)
398 # ##### REGISTRATION #####
400 def register():
401 bpy.utils.register_module(__name__)
403 bpy.types.Scene.ats_settings = bpy.props.PointerProperty(type=AutoTileSizeSettings)
405 # Note, the Cycles addon must be registered first, otherwise this panel doesn't exist - better be safe here!
406 cycles_panel = getattr(bpy.types, "CyclesRender_PT_performance", None)
407 if cycles_panel is not None:
408 cycles_panel.append(menu_func_cycles)
410 bpy.types.RENDER_PT_performance.append(menu_func_bi)
411 bpy.app.handlers.scene_update_post.append(on_scene_update)
414 def unregister():
415 bpy.app.handlers.scene_update_post.remove(on_scene_update)
416 bpy.types.RENDER_PT_performance.remove(menu_func_bi)
418 cycles_panel = getattr(bpy.types, "CyclesRender_PT_performance", None)
419 if cycles_panel is not None:
420 cycles_panel.remove(menu_func_cycles)
422 del bpy.types.Scene.ats_settings
424 bpy.utils.unregister_module(__name__)
427 if __name__ == "__main__":
428 register()