rna data path names which are more likely to break animations.
[blender-staging.git] / release / scripts / io / netrender / client.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
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.
7 #
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.
12 #
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.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 import bpy
20 import sys, os, re
21 import http, http.client, http.server, urllib
22 import subprocess, shutil, time, hashlib
23
24 import netrender
25 import netrender.model
26 import netrender.slave as slave
27 import netrender.master as master
28 from netrender.utils import *
29
30 def addFluidFiles(job, path):
31     if os.path.exists(path):
32         pattern = re.compile("fluidsurface_(final|preview)_([0-9]+)\.(bobj|bvel)\.gz")
33
34         for fluid_file in sorted(os.listdir(path)):
35             match = pattern.match(fluid_file)
36
37             if match:
38                 # fluid frames starts at 0, which explains the +1
39                 # This is stupid
40                 current_frame = int(match.groups()[1]) + 1
41                 job.addFile(path + fluid_file, current_frame, current_frame)
42
43 def addPointCache(job, ob, point_cache, default_path):
44     if not point_cache.use_disk_cache:
45         return
46
47
48     name = point_cache.name
49     if name == "":
50         name = "".join(["%02X" % ord(c) for c in ob.name])
51
52     cache_path = bpy.path.abspath(point_cache.filepath) if point_cache.use_external else default_path
53
54     index = "%02i" % point_cache.index
55
56     if os.path.exists(cache_path):
57         pattern = re.compile(name + "_([0-9]+)_" + index + "\.bphys")
58
59         cache_files = []
60
61         for cache_file in sorted(os.listdir(cache_path)):
62             match = pattern.match(cache_file)
63
64             if match:
65                 cache_frame = int(match.groups()[0])
66                 cache_files.append((cache_frame, cache_file))
67
68         cache_files.sort()
69
70         if len(cache_files) == 1:
71             cache_frame, cache_file = cache_files[0]
72             job.addFile(cache_path + cache_file, cache_frame, cache_frame)
73         else:
74             for i in range(len(cache_files)):
75                 current_item = cache_files[i]
76                 next_item = cache_files[i+1] if i + 1 < len(cache_files) else None
77                 previous_item = cache_files[i - 1] if i > 0 else None
78
79                 current_frame, current_file = current_item
80
81                 if  not next_item and not previous_item:
82                     job.addFile(cache_path + current_file, current_frame, current_frame)
83                 elif next_item and not previous_item:
84                     next_frame = next_item[0]
85                     job.addFile(cache_path + current_file, current_frame, next_frame - 1)
86                 elif not next_item and previous_item:
87                     previous_frame = previous_item[0]
88                     job.addFile(cache_path + current_file, previous_frame + 1, current_frame)
89                 else:
90                     next_frame = next_item[0]
91                     previous_frame = previous_item[0]
92                     job.addFile(cache_path + current_file, previous_frame + 1, next_frame - 1)
93
94 def clientSendJob(conn, scene, anim = False):
95     netsettings = scene.network_render
96     job = netrender.model.RenderJob()
97
98     if anim:
99         for f in range(scene.frame_start, scene.frame_end + 1):
100             job.addFrame(f)
101     else:
102         job.addFrame(scene.frame_current)
103
104     filename = bpy.data.filepath
105     job.addFile(filename)
106
107     job_name = netsettings.job_name
108     path, name = os.path.split(filename)
109     if job_name == "[default]":
110         job_name = name
111
112     ###########################
113     # LIBRARIES
114     ###########################
115     for lib in bpy.data.libraries:
116         file_path = bpy.path.abspath(lib.filepath)
117         if os.path.exists(file_path):
118             job.addFile(file_path)
119
120     ###########################
121     # IMAGES
122     ###########################
123     for image in bpy.data.images:
124         if image.source == "FILE" and not image.packed_file:
125             file_path = bpy.path.abspath(image.filepath)
126             if os.path.exists(file_path):
127                 job.addFile(file_path)
128                 
129                 tex_path = os.path.splitext(file_path)[0] + ".tex"
130                 if os.path.exists(tex_path):
131                     job.addFile(tex_path)
132
133     ###########################
134     # FLUID + POINT CACHE
135     ###########################
136     root, ext = os.path.splitext(name)
137     default_path = path + os.sep + "blendcache_" + root + os.sep # need an API call for that
138
139     for object in bpy.data.objects:
140         for modifier in object.modifiers:
141             if modifier.type == 'FLUID_SIMULATION' and modifier.settings.type == "DOMAIN":
142                 addFluidFiles(job, bpy.path.abspath(modifier.settings.path))
143             elif modifier.type == "CLOTH":
144                 addPointCache(job, object, modifier.point_cache, default_path)
145             elif modifier.type == "SOFT_BODY":
146                 addPointCache(job, object, modifier.point_cache, default_path)
147             elif modifier.type == "SMOKE" and modifier.smoke_type == "TYPE_DOMAIN":
148                 addPointCache(job, object, modifier.domain_settings.point_cache_low, default_path)
149                 if modifier.domain_settings.use_high_resolution:
150                     addPointCache(job, object, modifier.domain_settings.point_cache_high, default_path)
151             elif modifier.type == "MULTIRES" and modifier.is_external:
152                 file_path = bpy.path.abspath(modifier.filepath)
153                 job.addFile(file_path)
154
155         # particles modifier are stupid and don't contain data
156         # we have to go through the object property
157         for psys in object.particle_systems:
158             addPointCache(job, object, psys.point_cache, default_path)
159
160     #print(job.files)
161
162     job.name = job_name
163     job.category = netsettings.job_category
164
165     for slave in netrender.blacklist:
166         job.blacklist.append(slave.id)
167
168     job.chunks = netsettings.chunks
169     job.priority = netsettings.priority
170
171     # try to send path first
172     conn.request("POST", "/job", repr(job.serialize()))
173     response = conn.getresponse()
174     response.read()
175
176     job_id = response.getheader("job-id")
177
178     # if not ACCEPTED (but not processed), send files
179     if response.status == http.client.ACCEPTED:
180         for rfile in job.files:
181             f = open(rfile.filepath, "rb")
182             conn.request("PUT", fileURL(job_id, rfile.index), f)
183             f.close()
184             response = conn.getresponse()
185             response.read()
186
187     # server will reply with ACCEPTED until all files are found
188
189     return job_id
190
191 def requestResult(conn, job_id, frame):
192     conn.request("GET", renderURL(job_id, frame))
193
194 class NetworkRenderEngine(bpy.types.RenderEngine):
195     bl_idname = 'NET_RENDER'
196     bl_label = "Network Render"
197     bl_postprocess = False
198     def render(self, scene):
199         if scene.network_render.mode == "RENDER_CLIENT":
200             self.render_client(scene)
201         elif scene.network_render.mode == "RENDER_SLAVE":
202             self.render_slave(scene)
203         elif scene.network_render.mode == "RENDER_MASTER":
204             self.render_master(scene)
205         else:
206             print("UNKNOWN OPERATION MODE")
207
208     def render_master(self, scene):
209         netsettings = scene.network_render
210
211         address = "" if netsettings.server_address == "[default]" else netsettings.server_address
212
213         master.runMaster((address, netsettings.server_port), netsettings.master_broadcast, netsettings.use_master_clear, netsettings.path, self.update_stats, self.test_break)
214
215
216     def render_slave(self, scene):
217         slave.render_slave(self, scene.network_render, scene.render.threads)
218
219     def render_client(self, scene):
220         netsettings = scene.network_render
221         self.update_stats("", "Network render client initiation")
222
223
224         conn = clientConnection(netsettings.server_address, netsettings.server_port)
225
226         if conn:
227             # Sending file
228
229             self.update_stats("", "Network render exporting")
230
231             new_job = False
232
233             job_id = netsettings.job_id
234
235             # reading back result
236
237             self.update_stats("", "Network render waiting for results")
238
239             requestResult(conn, job_id, scene.frame_current)
240             response = conn.getresponse()
241             response.read()
242
243             if response.status == http.client.NO_CONTENT:
244                 new_job = True
245                 netsettings.job_id = clientSendJob(conn, scene)
246                 job_id = netsettings.job_id
247
248                 requestResult(conn, job_id, scene.frame_current)
249                 response = conn.getresponse()
250                 response.read()
251
252             while response.status == http.client.ACCEPTED and not self.test_break():
253                 time.sleep(1)
254                 requestResult(conn, job_id, scene.frame_current)
255                 response = conn.getresponse()
256                 response.read()
257
258             # cancel new jobs (animate on network) on break
259             if self.test_break() and new_job:
260                 conn.request("POST", cancelURL(job_id))
261                 response = conn.getresponse()
262                 response.read()
263                 print( response.status, response.reason )
264                 netsettings.job_id = 0
265
266             if response.status != http.client.OK:
267                 conn.close()
268                 return
269
270             r = scene.render
271             x= int(r.resolution_x*r.resolution_percentage*0.01)
272             y= int(r.resolution_y*r.resolution_percentage*0.01)
273
274             f = open(os.path.join(netsettings.path, "output.exr"), "wb")
275             buf = response.read(1024)
276
277             while buf:
278                 f.write(buf)
279                 buf = response.read(1024)
280
281             f.close()
282
283             result = self.begin_result(0, 0, x, y)
284             result.load_from_file(os.path.join(netsettings.path, "output.exr"))
285             self.end_result(result)
286
287             conn.close()
288
289 def compatible(module):
290     module = __import__(module)
291     for subclass in module.__dict__.values():
292         try:            subclass.COMPAT_ENGINES.add('NET_RENDER')
293         except: pass
294     del module
295
296 #compatible("properties_render")
297 compatible("properties_world")
298 compatible("properties_material")
299 compatible("properties_data_mesh")
300 compatible("properties_data_camera")
301 compatible("properties_texture")