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 #####
20 "name": "Auto Tile Size",
21 "description": "Estimate and set the tile size that will render the fastest",
22 "author": "Greg Zaal",
24 "blender": (2, 74, 0),
25 "location": "Render Settings > Performance",
27 "wiki_url": "http://wiki.blender.org/index.php?title=Extensions:2.6/Py/Scripts/Render/Auto_Tile_Size",
33 from bpy
.app
.handlers
import persistent
34 from math
import ceil
, floor
, sqrt
37 SUPPORTED_RENDER_ENGINES
= {'CYCLES', 'BLENDER_RENDER'}
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",
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",
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",
70 description
="Square dimensions of tiles",
71 update
=_update_tile_size
)
73 gpu_custom
= bpy
.props
.IntProperty(
76 min=8, # same as blender's own limits
78 description
="Custom target tile size for GPU rendering",
79 update
=_update_tile_size
)
80 cpu_custom
= bpy
.props
.IntProperty(
83 min=8, # same as blender's own limits
85 description
="Custom target tile size for CPU rendering",
86 update
=_update_tile_size
)
87 bi_custom
= bpy
.props
.IntProperty(
90 min=8, # same as blender's own limits
92 description
="Custom target tile size",
93 update
=_update_tile_size
)
95 target_type
= bpy
.props
.EnumProperty(
96 name
="Target tile size",
98 ('po2', "Po2", "A choice between powers of 2 (16, 32, 64...)"),
99 ('custom', "Custom", "Choose any number as the tile size target")),
101 description
="Method of choosing the target tile size",
102 update
=_update_tile_size
)
104 use_optimal
= bpy
.props
.BoolProperty(
105 name
="Optimal Tiles",
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",
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",
119 description
="Show extra options for more control over the calculated tile size")
121 thread_error_correct
= bpy
.props
.BoolProperty(
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
:
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
)
162 def on_scene_update(scene
):
163 context
= bpy
.context
165 if not ats_poll(context
):
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
[:])
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])
215 threads
= render
.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
231 funcs
= [floor
, ceil
]
234 sec_axis
= yres
if axis
== xres
else xres
236 primary
= func(axis
/ tile_length
)
238 secondary
= threads
/ 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
):
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
)
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
))
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
)
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
))
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
326 class SetTileSize(bpy
.types
.Operator
):
327 """The first render may not obey the tile-size set here"""
328 bl_idname
= "render.autotilesize_set"
332 def poll(clss
, context
):
333 return ats_poll(context
)
335 def execute(self
, context
):
336 if do_set_tile_size(context
):
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:")
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 #####
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
)
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__":