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, 80, 0),
25 "location": "Render Settings > Performance",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/render/auto_tile_size.html",
33 from bpy
.types
import (
37 from bpy
.props
import (
46 from bpy
.app
.handlers
import persistent
53 SUPPORTED_RENDER_ENGINES
= {'CYCLES', 'BLENDER_RENDER'}
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",
74 description
="Square dimensions of tiles for GPU rendering",
75 update
=_update_tile_size
77 cpu_choice
: EnumProperty(
78 name
="Target CPU Tile Size",
81 description
="Square dimensions of tiles for CPU rendering",
82 update
=_update_tile_size
84 bi_choice
: EnumProperty(
85 name
="Target CPU Tile Size",
88 description
="Square dimensions of tiles",
89 update
=_update_tile_size
91 gpu_custom
: IntProperty(
94 min=8, # same as blender's own limits
96 description
="Custom target tile size for GPU rendering",
97 update
=_update_tile_size
99 cpu_custom
: IntProperty(
102 min=8, # same as blender's own limits
104 description
="Custom target tile size for CPU rendering",
105 update
=_update_tile_size
107 bi_custom
: IntProperty(
110 min=8, # same as blender's own limits
112 description
="Custom target tile size",
113 update
=_update_tile_size
115 target_type
: EnumProperty(
116 name
="Target tile size",
118 ('po2', "Po2", "A choice between powers of 2 (16, 32, 64...)"),
119 ('custom', "Custom", "Choose any number as the tile size target")),
121 description
="Method of choosing the target tile size",
122 update
=_update_tile_size
124 use_optimal
: BoolProperty(
125 name
="Optimal Tiles",
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",
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",
141 description
="Show extra options for more control over the calculated tile size"
143 thread_error_correct
: BoolProperty(
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(
155 threads_error
: BoolProperty(
158 num_tiles
: IntVectorProperty(
163 prev_choice
: StringProperty(
167 prev_engine
: StringProperty(
171 prev_device
: StringProperty(
175 prev_res
: IntVectorProperty(
180 prev_border
: BoolProperty(
184 prev_border_res
: FloatVectorProperty(
185 default
=(0, 0, 0, 0),
189 prev_actual_tile_size
: IntVectorProperty(
194 prev_threads
: IntProperty(
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
:
207 def engine_is_gpu(engine
, device
, userpref
):
208 if engine
== 'CYCLES' and device
== 'GPU':
209 return userpref
.addons
['cycles'].preferences
.has_active_device()
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
)
223 def on_scene_update(scene
):
224 context
= bpy
.context
226 if not ats_poll(context
):
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
[:])
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()
273 threads
= render
.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
290 funcs
= [floor
, ceil
]
293 sec_axis
= yres
if axis
== xres
else xres
295 primary
= func(axis
/ tile_length
)
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
):
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
)
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
))
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
)
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
))
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
389 class SetTileSize(Operator
):
390 bl_idname
= "render.autotilesize_set"
392 bl_description
= "The first render may not obey the tile-size set here"
395 def poll(clss
, context
):
396 return ats_poll(context
)
398 def execute(self
, context
):
399 if do_set_tile_size(context
):
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:")
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 #####
465 AutoTileSizeSettings
,
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
)
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__":