Fix for access of undefined ground object in UI code giving pointless
[blender-addons-contrib.git] / render_auto_tile_size.py
1 # BEGIN GPL LICENSE BLOCK #####\r
2 #\r
3 #  This program is free software; you can redistribute it and/or\r
4 #  modify it under the terms of the GNU General Public License\r
5 #  as published by the Free Software Foundation; either version 2\r
6 #  of the License, or (at your option) any later version.\r
7 #\r
8 #  This program is distributed in the hope that it will be useful,\r
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of\r
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
11 #  GNU General Public License for more details.\r
12 #\r
13 #  You should have received a copy of the GNU General Public License\r
14 #  along with this program; if not, write to the Free Software Foundation,\r
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
16 #\r
17 # END GPL LICENSE BLOCK #####\r
18 \r
19 bl_info = {\r
20     "name": "Auto Tile Size",\r
21     "description": "Estimate and set the tile size that will render the fastest",\r
22     "author": "Greg Zaal",\r
23     "version": (2, 7),\r
24     "blender": (2, 72, 0),\r
25     "location": "Render Settings > Performance",\r
26     "warning": "",\r
27     "wiki_url": "http://wiki.blender.org/index.php?title=Extensions:2.6/Py/Scripts/Render/Auto_Tile_Size",\r
28     "category": "Render",\r
29 }\r
30 \r
31 \r
32 import bpy\r
33 from bpy.app.handlers import persistent\r
34 from math import ceil, floor\r
35 \r
36 \r
37 SUPPORTED_RENDER_ENGINES = {'CYCLES', 'BLENDER_RENDER'}\r
38 TILE_SIZES = (\r
39     ('16', "16", "16 x 16"),\r
40     ('32', "32", "32 x 32"),\r
41     ('64', "64", "64 x 64"),\r
42     ('128', "128", "128 x 128"),\r
43     ('256', "256", "256 x 256"),\r
44     ('512', "512", "512 x 512"),\r
45     ('1024', "1024", "1024 x 1024"),\r
46 )\r
47 \r
48 \r
49 def _update_tile_size(self, context):\r
50     do_set_tile_size(context)\r
51 \r
52 \r
53 class AutoTileSizeSettings(bpy.types.PropertyGroup):\r
54     gpu_choice = bpy.props.EnumProperty(\r
55         name="Target GPU Tile Size",\r
56         items=TILE_SIZES,\r
57         default='256',\r
58         description="Square dimentions of tiles",\r
59         update=_update_tile_size)\r
60 \r
61     cpu_choice = bpy.props.EnumProperty(\r
62         name="Target CPU Tile Size",\r
63         items=TILE_SIZES,\r
64         default='32',\r
65         description="Square dimentions of tiles",\r
66         update=_update_tile_size)\r
67 \r
68     bi_choice = bpy.props.EnumProperty(\r
69         name="Target CPU Tile Size",\r
70         items=TILE_SIZES,\r
71         default='64',\r
72         description="Square dimentions of tiles",\r
73         update=_update_tile_size)\r
74 \r
75     # XXX There is no need for this to be an enum, a mere bool should be enough?\r
76     use_optimal = bpy.props.BoolProperty(\r
77         name="Optimal Tiles",\r
78         default=True,\r
79         description="Try to find a similar tile size for best performance, instead of using exact selected one",\r
80         update=_update_tile_size)\r
81 \r
82     is_enabled = bpy.props.BoolProperty(\r
83         name="Auto Tile Size",\r
84         default=True,\r
85         description="Calculate the best tile size based on factors of the render size and the chosen target",\r
86         update=_update_tile_size)\r
87 \r
88     use_advanced_ui = bpy.props.BoolProperty(\r
89         name="Advanced Settings",\r
90         default=False,\r
91         description="Show extra options for more control over the calculated tile size")\r
92 \r
93     # Internally used props (not for GUI)\r
94     first_run = bpy.props.BoolProperty(default=True, options={'HIDDEN'})\r
95     threads_error = bpy.props.BoolProperty(options={'HIDDEN'})\r
96     prev_choice = bpy.props.StringProperty(default='', options={'HIDDEN'})\r
97     prev_engine = bpy.props.StringProperty(default='', options={'HIDDEN'})\r
98     prev_device = bpy.props.StringProperty(default='', options={'HIDDEN'})\r
99     prev_res = bpy.props.IntVectorProperty(default=(0, 0), size=2, options={'HIDDEN'})\r
100     prev_border = bpy.props.BoolProperty(default=False, options={'HIDDEN'})\r
101     prev_border_res = bpy.props.FloatVectorProperty(default=(0, 0, 0, 0), size=4, options={'HIDDEN'})\r
102     prev_actual_tile_size = bpy.props.IntVectorProperty(default=(0, 0), size=2, options={'HIDDEN'})\r
103     prev_threads = bpy.props.IntProperty(default=0, options={'HIDDEN'})\r
104 \r
105 \r
106 def ats_poll(context):\r
107     scene = context.scene\r
108     if scene.render.engine not in SUPPORTED_RENDER_ENGINES or not scene.ats_settings.is_enabled:\r
109         return False\r
110     return True\r
111 \r
112 \r
113 def ats_get_engine_is_gpu(engine, device, userpref):\r
114     return engine == 'CYCLES' and device == 'GPU' and userpref.system.compute_device_type != 'NONE'\r
115 \r
116 \r
117 def ats_get_tilesize_prop(engine, device, userpref):\r
118     if ats_get_engine_is_gpu(engine, device, userpref):\r
119         return "gpu_choice"\r
120     elif engine == 'CYCLES':\r
121         return "cpu_choice"\r
122     return "bi_choice"\r
123 \r
124 \r
125 @persistent\r
126 def on_scene_update(scene):\r
127     context = bpy.context\r
128 \r
129     if not ats_poll(context):\r
130         return\r
131 \r
132     userpref = context.user_preferences\r
133 \r
134     settings = scene.ats_settings\r
135     render = scene.render\r
136     engine = render.engine\r
137 \r
138     # scene.cycles might not always exist (Cycles is an addon)...\r
139     device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device\r
140     border = render.use_border\r
141     threads = render.threads\r
142 \r
143     choice = getattr(settings, ats_get_tilesize_prop(engine, device, userpref))\r
144 \r
145     res = get_actual_res(render)\r
146     actual_ts = (render.tile_x, render.tile_y)\r
147     border_res = (render.border_min_x, render.border_min_y, render.border_max_x, render.border_max_y)\r
148 \r
149     # detect relevant changes in scene\r
150     do_change = (engine != settings.prev_engine or\r
151                  device != settings.prev_device or\r
152                  border != settings.prev_border or\r
153                  threads != settings.prev_threads or\r
154                  choice != settings.prev_choice or\r
155                  res != settings.prev_res[:] or\r
156                  border_res != settings.prev_border_res[:] or\r
157                  actual_ts != settings.prev_actual_tile_size[:])\r
158     if do_change:\r
159         do_set_tile_size(context)\r
160 \r
161 \r
162 def get_actual_res(render):\r
163     rend_percent = render.resolution_percentage * 0.01\r
164     # floor is implicitly done by int conversion...\r
165     return (int(render.resolution_x * rend_percent), int(render.resolution_y * rend_percent))\r
166 \r
167 \r
168 def do_set_tile_size(context):\r
169     if not ats_poll(context):\r
170         return False\r
171 \r
172     scene = context.scene\r
173     userpref = context.user_preferences\r
174 \r
175     settings = scene.ats_settings\r
176     render = scene.render\r
177     engine = render.engine\r
178     device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device\r
179     border = render.use_border\r
180     threads = render.threads\r
181 \r
182     realxres, realyres = xres, yres = res = get_actual_res(scene.render)\r
183 \r
184     if border:\r
185         xres = round(xres * (render.border_max_x - render.border_min_x))\r
186         yres = round(yres * (render.border_max_y - render.border_min_y))\r
187 \r
188     choice = getattr(settings, ats_get_tilesize_prop(engine, device, userpref))\r
189     target = int(choice)\r
190 \r
191     numtiles_x = ceil(xres / target)\r
192     numtiles_y = ceil(yres / target)\r
193     if settings.use_optimal:\r
194         tile_x = ceil(xres / numtiles_x)\r
195         tile_y = ceil(yres / numtiles_y)\r
196     else:\r
197         tile_x = target\r
198         tile_y = target\r
199 \r
200     print("Tile size: %dx%d (%dx%d tiles)" % (tile_x, tile_y, ceil(xres / tile_x), ceil(yres / tile_y)))\r
201 \r
202     render.tile_x = tile_x\r
203     render.tile_y = tile_y\r
204 \r
205     # Detect if there are fewer tiles than available threads\r
206     if ((numtiles_x * numtiles_y) < threads) and not ats_get_engine_is_gpu(engine, device, userpref):\r
207         settings.threads_error = True\r
208     else:\r
209         settings.threads_error = False\r
210 \r
211     settings.prev_engine = engine\r
212     settings.prev_device = device\r
213     settings.prev_border = border\r
214     settings.prev_threads = threads\r
215     settings.prev_choice = choice\r
216     settings.prev_res = res\r
217     settings.prev_border_res = (render.border_min_x, render.border_min_y, render.border_max_x, render.border_max_y)\r
218     settings.prev_actual_tile_size = (tile_x, tile_y)\r
219     settings.first_run = False\r
220 \r
221     return True\r
222 \r
223 \r
224 class SetTileSize(bpy.types.Operator):\r
225     """The first render may not obey the tile-size set here"""\r
226     bl_idname = "render.autotilesize_set"\r
227     bl_label = "Set"\r
228 \r
229     @classmethod\r
230     def poll(clss, context):\r
231         return ats_poll(context)\r
232 \r
233     def execute(self, context):\r
234         if do_set_tile_size(context):\r
235             return {'FINISHED'}\r
236         return {'CANCELLED'}\r
237 \r
238 \r
239 # ##### INTERFACE #####\r
240 \r
241 def ui_layout(engine, layout, context):\r
242     scene = context.scene\r
243     userpref = context.user_preferences\r
244 \r
245     settings = scene.ats_settings\r
246     render = scene.render\r
247     engine = render.engine\r
248     device = scene.cycles.device if engine == 'CYCLES' else settings.prev_device\r
249 \r
250     col = layout.column(align=True)\r
251     sub = col.column(align=True)\r
252     row = sub.row(align=True)\r
253     row.prop(settings, "is_enabled", toggle=True)\r
254     row.prop(settings, "use_advanced_ui", toggle=True, text="", icon='PREFERENCES')\r
255 \r
256     sub = col.column(align=True)\r
257     sub.enabled = settings.is_enabled\r
258 \r
259     if settings.use_advanced_ui:\r
260         sub.label("Target tile size:")\r
261 \r
262         row = sub.row(align=True)\r
263         row.prop(settings, ats_get_tilesize_prop(engine, device, userpref), expand=True)\r
264         sub.prop(settings, "use_optimal", text="Calculate Optimal Size")\r
265 \r
266     if settings.first_run:\r
267         sub = layout.column(align=True)\r
268         sub.operator("render.autotilesize_set", text="First-render fix", icon='ERROR')\r
269     elif settings.prev_device != device:\r
270         sub = layout.column(align=True)\r
271         sub.operator("render.autotilesize_set", text="Device changed - fix", icon='ERROR')\r
272 \r
273     if (render.tile_x / render.tile_y > 2) or (render.tile_x / render.tile_y < 0.5):  # if not very square tile\r
274         sub.label(text="Warning: Tile size is not very square", icon='ERROR')\r
275         sub.label(text="    Try a slightly different resolution")\r
276         sub.label(text="    or choose \"Exact\" above")\r
277     if settings.threads_error:\r
278         sub.label(text="Warning: Fewer tiles than render threads", icon='ERROR')\r
279 \r
280 \r
281 def menu_func_cycles(self, context):\r
282     ui_layout('CYCLES', self.layout, context)\r
283 \r
284 \r
285 def menu_func_bi(self, context):\r
286     ui_layout('BLENDER_RENDER', self.layout, context)\r
287 \r
288 \r
289 # ##### REGISTRATION #####\r
290 \r
291 def register():\r
292     bpy.utils.register_module(__name__)\r
293 \r
294     bpy.types.Scene.ats_settings = bpy.props.PointerProperty(type=AutoTileSizeSettings)\r
295 \r
296     # Note, the Cycles addon must be registered first, otherwise this panel doesn't exist - better be safe here!\r
297     cycles_panel = getattr(bpy.types, "CyclesRender_PT_performance", None)\r
298     if cycles_panel is not None:\r
299         cycles_panel.append(menu_func_cycles)\r
300 \r
301     bpy.types.RENDER_PT_performance.append(menu_func_bi)\r
302     bpy.app.handlers.scene_update_post.append(on_scene_update)\r
303 \r
304 \r
305 def unregister():\r
306     bpy.app.handlers.scene_update_post.remove(on_scene_update)\r
307     bpy.types.RENDER_PT_performance.remove(menu_func_bi)\r
308 \r
309     cycles_panel = getattr(bpy.types, "CyclesRender_PT_performance", None)\r
310     if cycles_panel is not None:\r
311         cycles_panel.remove(menu_func_cycles)\r
312 \r
313     del bpy.types.Scene.ats_settings\r
314 \r
315     bpy.utils.unregister_module(__name__)\r
316 \r
317 \r
318 if __name__ == "__main__":\r
319     register()\r