First commit draft for network rendering.

Docs are here: http://wiki.blender.org/index.php/User:Theeth/netrender

Should be easy to test if people want too, just follow the instructions on wiki

Code is still very much in flux, so I'd like if people would refrain from making changes (send patches directly to me if you must).

The UI side is very crap, it's basically there just to get things testable. See wiki for known bugs.
This commit is contained in:
Martin Poirier 2009-08-29 17:25:22 +00:00
parent e6f2f4db28
commit afee963155
8 changed files with 1547 additions and 0 deletions

@ -0,0 +1,9 @@
# This directory is a Python package.
import model
import operators
import client
import slave
import master
import utils
import ui

@ -0,0 +1,87 @@
import bpy
import sys, os
import http, http.client, http.server, urllib
import subprocess, shutil, time, hashlib
import netrender.slave as slave
import netrender.master as master
from netrender.utils import *
class NetworkRenderEngine(bpy.types.RenderEngine):
__idname__ = 'NET_RENDER'
__label__ = "Network Render"
def render(self, scene):
if scene.network_render.mode == "RENDER_CLIENT":
self.render_client(scene)
elif scene.network_render.mode == "RENDER_SLAVE":
self.render_slave(scene)
elif scene.network_render.mode == "RENDER_MASTER":
self.render_master(scene)
else:
print("UNKNOWN OPERATION MODE")
def render_master(self, scene):
server_address = (scene.network_render.server_address, scene.network_render.server_port)
httpd = master.RenderMasterServer(server_address, master.RenderHandler)
httpd.timeout = 1
httpd.stats = self.update_stats
while not self.test_break():
httpd.handle_request()
def render_slave(self, scene):
slave.render_slave(self, scene)
def render_client(self, scene):
self.update_stats("", "Network render client initiation")
conn = clientConnection(scene)
if conn:
# Sending file
self.update_stats("", "Network render exporting")
job_id = scene.network_render.job_id
# reading back result
self.update_stats("", "Network render waiting for results")
clientRequestResult(conn, scene, job_id)
response = conn.getresponse()
if response.status == http.client.NO_CONTENT:
scene.network_render.job_id = clientSendJob(conn, scene)
clientRequestResult(conn, scene, job_id)
while response.status == http.client.PROCESSING and not self.test_break():
print("waiting")
time.sleep(1)
clientRequestResult(conn, scene, job_id)
response = conn.getresponse()
if response.status != http.client.OK:
conn.close()
return
r = scene.render_data
x= int(r.resolution_x*r.resolution_percentage*0.01)
y= int(r.resolution_y*r.resolution_percentage*0.01)
f = open(PATH_PREFIX + "output.exr", "wb")
buf = response.read(1024)
while buf:
f.write(buf)
buf = response.read(1024)
f.close()
result = self.begin_result(0, 0, x, y)
result.load_from_file(PATH_PREFIX + "output.exr", 0, 0)
self.end_result(result)
conn.close()
bpy.types.register(NetworkRenderEngine)

@ -0,0 +1,546 @@
import sys, os
import http, http.client, http.server, urllib
import subprocess, shutil, time, hashlib
from netrender.utils import *
import netrender.model
class MRenderSlave(netrender.model.RenderSlave):
def __init__(self, name, adress, stats):
super().__init__()
self.id = hashlib.md5(bytes(repr(name) + repr(adress), encoding='utf8')).hexdigest()
self.name = name
self.adress = adress
self.stats = stats
self.last_seen = time.time()
self.job = None
self.frame = None
netrender.model.RenderSlave._slave_map[self.id] = self
def seen(self):
self.last_seen = time.time()
# sorting key for jobs
def groupKey(job):
return (job.framesLeft() > 0, job.priority, job.credits)
class MRenderJob(netrender.model.RenderJob):
def __init__(self, job_id, name, path, chunks = 1, priority = 1, credits = 100.0, blacklist = []):
super().__init__()
self.id = job_id
self.name = name
self.path = path
self.frames = []
self.chunks = chunks
self.priority = priority
self.credits = credits
self.blacklist = blacklist
self.last_dispatched = time.time()
def update(self):
self.credits -= 5 # cost of one frame
self.credits += (time.time() - self.last_dispatched) / 60
self.last_dispatched = time.time()
def addFrame(self, frame_number):
frame = MRenderFrame(frame_number)
self.frames.append(frame)
return frame
def framesLeft(self):
total = 0
for j in self.frames:
if j.status == QUEUED:
total += 1
return total
def reset(self, all):
for f in self.frames:
f.reset(all)
def getFrames(self):
frames = []
for f in self.frames:
if f.status == QUEUED:
self.update()
frames.append(f)
if len(frames) == self.chunks:
break
return frames
class MRenderFrame(netrender.model.RenderFrame):
def __init__(self, frame):
super().__init__()
self.number = frame
self.slave = None
self.time = 0
self.status = QUEUED
def reset(self, all):
if all or self.status == ERROR:
self.slave = None
self.time = 0
self.status = QUEUED
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
class RenderHandler(http.server.BaseHTTPRequestHandler):
def send_head(self, code = http.client.OK, headers = {}):
self.send_response(code)
self.send_header("Content-type", "application/octet-stream")
for key, value in headers.items():
self.send_header(key, value)
self.end_headers()
def do_HEAD(self):
print(self.path)
if self.path == "status":
job_id = self.headers.get('job-id', "")
job_frame = int(self.headers.get('job-frame', -1))
if job_id:
print("status:", job_id, "\n")
job = self.server.getJobByID(job_id)
if job:
if job_frame != -1:
frame = job[frame]
if not frame:
# no such frame
self.send_heat(http.client.NOT_FOUND)
return
else:
# no such job id
self.send_head(http.client.NOT_FOUND)
return
self.send_head()
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def do_GET(self):
print(self.path)
if self.path == "version":
self.send_head()
self.server.stats("", "New client connection")
self.wfile.write(VERSION)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "render":
job_id = self.headers['job-id']
job_frame = int(self.headers['job-frame'])
print("render:", job_id, job_frame)
job = self.server.getJobByID(job_id)
if job:
frame = job[job_frame]
if frame:
if frame.status in (QUEUED, DISPATCHED):
self.send_head(http.client.PROCESSING)
elif frame.status == DONE:
self.server.stats("", "Sending result back to client")
f = open(PATH_PREFIX + job_id + "%04d" % job_frame + ".exr", 'rb')
self.send_head()
shutil.copyfileobj(f, self.wfile)
f.close()
elif frame.status == ERROR:
self.send_head(http.client.NO_CONTENT)
else:
# no such frame
self.send_head(http.client.NOT_FOUND)
else:
# no such job id
self.send_head(http.client.NOT_FOUND)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "log":
job_id = self.headers['job-id']
job_frame = int(self.headers['job-frame'])
print("log:", job_id, job_frame)
job = self.server.getJobByID(job_id)
if job:
frame = job[job_frame]
if frame:
if frame.status in (QUEUED, DISPATCHED):
self.send_head(http.client.PROCESSING)
elif frame.status == DONE:
self.server.stats("", "Sending log back to client")
f = open(PATH_PREFIX + job_id + "%04d" % job_frame + ".log", 'rb')
self.send_head()
shutil.copyfileobj(f, self.wfile)
f.close()
elif frame.status == ERROR:
self.send_head(http.client.NO_CONTENT)
else:
# no such frame
self.send_head(http.client.NOT_FOUND)
else:
# no such job id
self.send_head(http.client.NOT_FOUND)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "status":
job_id = self.headers.get('job-id', "")
job_frame = int(self.headers.get('job-frame', -1))
if job_id:
print("status:", job_id, "\n")
job = self.server.getJobByID(job_id)
if job:
if job_frame != -1:
frame = job[frame]
if frame:
message = frame.serialize()
else:
# no such frame
self.send_heat(http.client.NOT_FOUND)
return
else:
message = job.serialize()
else:
# no such job id
self.send_head(http.client.NOT_FOUND)
return
else: # status of all jobs
message = []
for job in self.server:
results = job.status()
message.append(job.serialize())
self.send_head()
self.wfile.write(bytes(repr(message), encoding='utf8'))
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "job":
self.server.update()
slave_id = self.headers['slave-id']
print("slave-id", slave_id)
self.server.getSlave(slave_id)
slave = self.server.updateSlave(slave_id)
if slave: # only if slave id is valid
job, frames = self.server.getNewJob(slave_id)
if job and frames:
for f in frames:
f.status = DISPATCHED
f.slave = slave
self.send_head(headers={"job-id": job.id})
message = job.serialize(frames)
self.wfile.write(bytes(repr(message), encoding='utf8'))
self.server.stats("", "Sending job frame to render node")
else:
# no job available, return error code
self.send_head(http.client.NO_CONTENT)
else: # invalid slave id
self.send_head(http.client.NOT_FOUND)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "file":
job_id = self.headers['job-id']
print("file:", job_id, "\n")
job = self.server.getJobByID(job_id)
if job:
self.send_head(headers={"job-id": job.id})
self.server.stats("", "Sending file to render node")
f = open(PATH_PREFIX + job.id + ".blend", 'rb')
shutil.copyfileobj(f, self.wfile)
f.close()
else:
# no such job id
self.send_head(http.client.NOT_FOUND)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "slave":
message = []
for slave in self.server.slaves:
message.append(slave.serialize())
self.send_head()
self.wfile.write(bytes(repr(message), encoding='utf8'))
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def do_POST(self):
print(self.path)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
if self.path == "job":
print("posting job info")
self.server.stats("", "Receiving job")
length = int(self.headers['content-length'])
job_frame_string = self.headers['job-frame']
job_name = self.headers.get('job-name', "")
job_chunks = int(self.headers.get('job-chunks', "1"))
blacklist = self.headers.get('slave-blacklist', '').split()
print("blacklist", blacklist)
job_path = str(self.rfile.read(length), encoding='utf8')
if os.path.exists(job_path):
f = open(job_path, "rb")
buf = f.read()
f.close()
job_id = hashlib.md5(buf).hexdigest()
del buf
job = self.server.getJobByID(job_id)
if job == None:
job = MRenderJob(job_id, job_name, job_path, chunks = job_chunks, blacklist = blacklist)
self.server.addJob(job)
if ":" in job_frame_string:
frame_start, frame_end = [int(x) for x in job_frame_string.split(":")]
for job_frame in range(frame_start, frame_end + 1):
frame = job.addFrame(job_frame)
else:
job_frame = int(job_frame_string)
frame = job.addFrame(job_frame)
self.send_head(headers={"job-id": job_id})
else:
self.send_head(http.client.NOT_FOUND)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "cancel":
job_id = self.headers.get('job-id', "")
if job_id:
print("cancel:", job_id, "\n")
self.server.removeJob(job_id)
else: # cancel all jobs
self.server.clear()
self.send_head()
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "reset":
job_id = self.headers.get('job-id', "")
job_frame = int(self.headers.get('job-frame', "-1"))
all = bool(self.headers.get('reset-all', "False"))
job = self.server.getJobByID(job_id)
if job:
if job_frame != -1:
job[job_frame].reset(all)
else:
job.reset(all)
self.send_head()
else: # job not found
self.send_head(http.client.NOT_FOUND)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "slave":
length = int(self.headers['content-length'])
job_frame_string = self.headers['job-frame']
name, stats = eval(str(self.rfile.read(length), encoding='utf8'))
slave_id = self.server.addSlave(name, self.client_address, stats)
self.send_head(headers = {"slave-id": slave_id})
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def do_PUT(self):
print(self.path)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
if self.path == "file":
print("writing blend file")
self.server.stats("", "Receiving job")
length = int(self.headers['content-length'])
job_frame_string = self.headers['job-frame']
job_name = self.headers.get('job-name', "")
job_chunks = int(self.headers.get('job-chunks', "1"))
blacklist = self.headers.get('slave-blacklist', '').split()
buf = self.rfile.read(length)
job_id = hashlib.md5(buf).hexdigest()
job_path = job_id + ".blend"
f = open(PATH_PREFIX + job_path, "wb")
f.write(buf)
f.close()
del buf
job = self.server.getJobByID(job_id)
if job == None:
job = MRenderJob(job_id, job_name, job_path, chunks = job_chunks, blacklist = blacklist)
self.server.addJob(job)
if ":" in job_frame_string:
frame_start, frame_end = [int(x) for x in job_frame_string.split(":")]
for job_frame in range(frame_start, frame_end + 1):
frame = job.addFrame(job_frame)
else:
job_frame = int(job_frame_string)
frame = job.addFrame(job_frame)
self.send_head(headers={"job-id": job_id})
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "render":
print("writing result file")
self.server.stats("", "Receiving render result")
job_id = self.headers['job-id']
job_frame = int(self.headers['job-frame'])
job_result = int(self.headers['job-result'])
job_time = float(self.headers['job-time'])
if job_result == DONE:
length = int(self.headers['content-length'])
buf = self.rfile.read(length)
f = open(PATH_PREFIX + job_id + "%04d" % job_frame + ".exr", 'wb')
f.write(buf)
f.close()
del buf
job = self.server.getJobByID(job_id)
frame = job[job_frame]
frame.status = job_result
frame.time = job_time
self.server.updateSlave(self.headers['slave-id'])
self.send_head()
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
elif self.path == "log":
print("writing log file")
self.server.stats("", "Receiving log file")
length = int(self.headers['content-length'])
job_id = self.headers['job-id']
job_frame = int(self.headers['job-frame'])
print("log length:", length)
buf = self.rfile.read(length)
f = open(PATH_PREFIX + job_id + "%04d" % job_frame + ".log", 'wb')
f.write(buf)
f.close()
del buf
self.server.updateSlave(self.headers['slave-id'])
self.send_head()
class RenderMasterServer(http.server.HTTPServer):
def __init__(self, address, handler_class):
super().__init__(address, handler_class)
self.jobs = []
self.jobs_map = {}
self.slaves = []
self.slaves_map = {}
def addSlave(self, name, adress, stats):
slave = MRenderSlave(name, adress, stats)
self.slaves.append(slave)
self.slaves_map[slave.id] = slave
return slave.id
def getSlave(self, slave_id):
return self.slaves_map.get(slave_id, None)
def updateSlave(self, slave_id):
slave = self.getSlave(slave_id)
if slave:
slave.seen()
return slave
def clear(self):
self.jobs_map = {}
self.jobs = []
def update(self):
self.jobs.sort(key = groupKey)
def removeJob(self, id):
job = self.jobs_map.pop(id)
if job:
self.jobs.remove(job)
def addJob(self, job):
self.jobs.append(job)
self.jobs_map[job.id] = job
def getJobByID(self, id):
return self.jobs_map.get(id, None)
def __iter__(self):
for job in self.jobs:
yield job
def getNewJob(self, slave_id):
if self.jobs:
for job in reversed(self.jobs):
if job.framesLeft() > 0 and slave_id not in job.blacklist:
return job, job.getFrames()
return None, None

@ -0,0 +1,150 @@
import sys, os
import http, http.client, http.server, urllib
import subprocess, shutil, time, hashlib
from netrender.utils import *
class RenderSlave:
_slave_map = {}
def __init__(self):
self.id = ""
self.name = ""
self.adress = (0,0)
self.stats = ""
self.total_done = 0
self.total_error = 0
self.last_seen = 0.0
def serialize(self):
return {
"id": self.id,
"name": self.name,
"adress": self.adress,
"stats": self.stats,
"total_done": self.total_done,
"total_error": self.total_error,
"last_seen": self.last_seen
}
@staticmethod
def materialize(data):
if not data:
return None
slave_id = data["id"]
if slave_id in RenderSlave._slave_map:
return RenderSlave._slave_map[slave_id]
else:
slave = RenderSlave()
slave.id = slave_id
slave.name = data["name"]
slave.adress = data["adress"]
slave.stats = data["stats"]
slave.total_done = data["total_done"]
slave.total_error = data["total_error"]
slave.last_seen = data["last_seen"]
RenderSlave._slave_map[slave_id] = slave
return slave
class RenderJob:
def __init__(self):
self.id = ""
self.name = ""
self.path = ""
self.frames = []
self.chunks = 0
self.priority = 0
self.credits = 0
self.blacklist = []
self.last_dispatched = 0.0
def __len__(self):
return len(self.frames)
def status(self):
results = {
QUEUED: 0,
DISPATCHED: 0,
DONE: 0,
ERROR: 0
}
for frame in self.frames:
results[frame.status] += 1
return results
def __contains__(self, frame_number):
for f in self.frames:
if f.number == frame_number:
return True
else:
return False
def __getitem__(self, frame_number):
for f in self.frames:
if f.number == frame_number:
return f
else:
return None
def serialize(self, frames = None):
return {
"id": self.id,
"name": self.name,
"path": self.path,
"frames": [f.serialize() for f in self.frames if not frames or f in frames],
"priority": self.priority,
"credits": self.credits,
"blacklist": self.blacklist,
"last_dispatched": self.last_dispatched
}
@staticmethod
def materialize(data):
if not data:
return None
job = RenderJob()
job.id = data["id"]
job.name = data["name"]
job.path = data["path"]
job.frames = [RenderFrame.materialize(f) for f in data["frames"]]
job.priority = data["priority"]
job.credits = data["credits"]
job.blacklist = data["blacklist"]
job.last_dispatched = data["last_dispatched"]
return job
class RenderFrame:
def __init__(self):
self.number = 0
self.time = 0
self.status = QUEUED
self.slave = None
def serialize(self):
return {
"number": self.number,
"time": self.time,
"status": self.status,
"slave": None if not self.slave else self.slave.serialize()
}
@staticmethod
def materialize(data):
if not data:
return None
frame = RenderFrame()
frame.number = data["number"]
frame.time = data["time"]
frame.status = data["status"]
frame.slave = RenderSlave.materialize(data["slave"])
return frame

@ -0,0 +1,238 @@
import bpy
import sys, os
import http, http.client, http.server, urllib
from netrender.utils import *
import netrender.model
class RENDER_OT_netclientsend(bpy.types.Operator):
'''
Operator documentation text, will be used for the operator tooltip and python docs.
'''
__idname__ = "render.netclientsend"
__label__ = "Net Render Client Send"
# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
__props__ = []
def poll(self, context):
return True
def execute(self, context):
scene = context.scene
conn = clientConnection(scene)
if conn:
# Sending file
scene.network_render.job_id = clientSendJob(conn, scene, True)
return ('FINISHED',)
def invoke(self, context, event):
return self.execute(context)
class RENDER_OT_netclientstatus(bpy.types.Operator):
'''Operator documentation text, will be used for the operator tooltip and python docs.'''
__idname__ = "render.netclientstatus"
__label__ = "Net Render Client Status"
# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
__props__ = []
def poll(self, context):
return True
def execute(self, context):
netprops = context.scene.network_render
conn = clientConnection(context.scene)
if conn:
conn.request("GET", "status")
response = conn.getresponse()
print( response.status, response.reason )
jobs = (netrender.model.RenderJob.materialize(j) for j in eval(str(response.read(), encoding='utf8')))
while(len(netprops.jobs) > 0):
netprops.jobs.remove(0)
for j in jobs:
netprops.jobs.add()
job = netprops.jobs[-1]
job_results = j.status()
job.id = j.id
job.name = j.name
job.length = len(j)
job.done = job_results[DONE]
job.error = job_results[ERROR]
return ('FINISHED',)
def invoke(self, context, event):
return self.execute(context)
class RENDER_OT_netclientblacklistslave(bpy.types.Operator):
'''Operator documentation text, will be used for the operator tooltip and python docs.'''
__idname__ = "render.netclientblacklistslave"
__label__ = "Net Render Client Blacklist Slave"
# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
__props__ = []
def poll(self, context):
return True
def execute(self, context):
netprops = context.scene.network_render
if netprops.active_slave_index >= 0:
slave = netrender.slaves[netprops.active_slave_index]
netprops.slaves_blacklist.add()
netprops.slaves_blacklist[-1].id = slave.id
netprops.slaves_blacklist[-1].name = slave.name
netprops.slaves_blacklist[-1].adress = slave.adress
netprops.slaves_blacklist[-1].last_seen = slave.last_seen
netprops.slaves_blacklist[-1].stats = slave.stats
netprops.slaves.remove(netprops.active_slave_index)
netprops.active_slave_index = -1
return ('FINISHED',)
def invoke(self, context, event):
return self.execute(context)
class RENDER_OT_netclientwhitelistslave(bpy.types.Operator):
'''Operator documentation text, will be used for the operator tooltip and python docs.'''
__idname__ = "render.netclientwhitelistslave"
__label__ = "Net Render Client Whitelist Slave"
# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
__props__ = []
def poll(self, context):
return True
def execute(self, context):
netprops = context.scene.network_render
if netprops.active_blacklisted_slave_index >= 0:
slave = netprops.slaves_blacklist[netprops.active_blacklisted_slave_index]
netprops.slaves.add()
netprops.slaves[-1].id = slave.id
netprops.slaves[-1].name = slave.name
netprops.slaves[-1].adress = slave.adress
netprops.slaves[-1].last_seen = slave.last_seen
netprops.slaves[-1].stats = slave.stats
netprops.slaves_blacklist.remove(netprops.active_blacklisted_slave_index)
netprops.active_blacklisted_slave_index = -1
return ('FINISHED',)
def invoke(self, context, event):
return self.execute(context)
class RENDER_OT_netclientslaves(bpy.types.Operator):
'''Operator documentation text, will be used for the operator tooltip and python docs.'''
__idname__ = "render.netclientslaves"
__label__ = "Net Render Client Slaves"
# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
__props__ = []
def poll(self, context):
return True
def execute(self, context):
netprops = context.scene.network_render
conn = clientConnection(context.scene)
if conn:
conn.request("GET", "slave")
response = conn.getresponse()
print( response.status, response.reason )
slaves = (netrender.model.RenderSlave.materialize(s) for s in eval(str(response.read(), encoding='utf8')))
while(len(netprops.slaves) > 0):
netprops.slaves.remove(0)
for s in slaves:
for slave in netprops.slaves_blacklist:
if slave.id == s.id:
break
netprops.slaves.add()
slave = netprops.slaves[-1]
slave.id = s.id
slave.name = s.name
slave.stats = s.stats
slave.adress = s.adress[0]
slave.last_seen = time.ctime(s.last_seen)
return ('FINISHED',)
def invoke(self, context, event):
return self.execute(context)
class RENDER_OT_netclientcancel(bpy.types.Operator):
'''Operator documentation text, will be used for the operator tooltip and python docs.'''
__idname__ = "render.netclientcancel"
__label__ = "Net Render Client Cancel"
# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
__props__ = []
def poll(self, context):
netrender = scene.network_render
return netrender.active_job_index >= 0 and len(netrender.jobs) > 0
def execute(self, context):
netprops = context.scene.network_render
conn = clientConnection(context.scene)
if conn:
job = netprops.jobs[netrender.active_job_index]
conn.request("POST", "cancel", headers={"job-id":job.id})
response = conn.getresponse()
print( response.status, response.reason )
return ('FINISHED',)
def invoke(self, context, event):
return self.execute(context)
bpy.ops.add(RENDER_OT_netclientsend)
bpy.ops.add(RENDER_OT_netclientstatus)
bpy.ops.add(RENDER_OT_netclientslaves)
bpy.ops.add(RENDER_OT_netclientblacklistslave)
bpy.ops.add(RENDER_OT_netclientwhitelistslave)
bpy.ops.add(RENDER_OT_netclientcancel)

@ -0,0 +1,147 @@
import sys, os
import http, http.client, http.server, urllib
import subprocess, time
from netrender.utils import *
import netrender.model
CANCEL_POLL_SPEED = 2
MAX_TIMEOUT = 10
INCREMENT_TIMEOUT = 1
def slave_Info():
sysname, nodename, release, version, machine = os.uname()
return (nodename, sysname + " " + release + " " + machine)
def testCancel(conn, job_id):
conn.request("HEAD", "status", headers={"job-id":job_id})
response = conn.getresponse()
# cancelled if job isn't found anymore
if response.status == http.client.NOT_FOUND:
return True
else:
return False
def render_slave(engine, scene):
NODE_PREFIX = PATH_PREFIX + "node" + os.sep
timeout = 1
if not os.path.exists(NODE_PREFIX):
os.mkdir(NODE_PREFIX)
engine.update_stats("", "Network render node initiation")
conn = clientConnection(scene)
if conn:
conn.request("POST", "slave", repr(slave_Info()))
response = conn.getresponse()
slave_id = response.getheader("slave-id")
while not engine.test_break():
conn.request("GET", "job", headers={"slave-id":slave_id})
response = conn.getresponse()
if response.status == http.client.OK:
timeout = 1 # reset timeout on new job
job = netrender.model.RenderJob.materialize(eval(str(response.read(), encoding='utf8')))
print("File:", job.path)
engine.update_stats("", "Render File", job.path, "for job", job.id)
if os.path.isabs(job.path):
# if an absolute path, make sure path exists, if it doesn't, use relative local path
job_full_path = job.path
if not os.path.exists(job_full_path):
job_full_path = NODE_PREFIX + job.id + ".blend"
else:
job_full_path = NODE_PREFIX + job.path
if not os.path.exists(job_full_path):
conn.request("GET", "file", headers={"job-id": job.id, "slave-id":slave_id})
response = conn.getresponse()
if response.status != http.client.OK:
break # file for job not returned by server, need to return an error code to server
f = open(job_full_path, "wb")
buf = response.read(1024)
while buf:
f.write(buf)
buf = response.read(1024)
f.close()
frame_args = []
for frame in job.frames:
frame_args += ["-f", str(frame.number)]
start_t = time.time()
process = subprocess.Popen([sys.argv[0], "-b", job_full_path, "-o", NODE_PREFIX + job.id, "-E", "BLENDER_RENDER", "-F", "MULTILAYER"] + frame_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
cancelled = False
stdout = bytes()
run_t = time.time()
while process.poll() == None and not cancelled:
stdout += process.stdout.read(32)
current_t = time.time()
cancelled = engine.test_break()
if current_t - run_t > CANCEL_POLL_SPEED:
if testCancel(conn, job.id):
cancelled = True
else:
run_t = current_t
if cancelled:
continue # to next frame
total_t = time.time() - start_t
avg_t = total_t / len(job.frames)
status = process.returncode
print("status", status)
headers = {"job-id":job.id, "slave-id":slave_id, "job-time":str(avg_t)}
if status == 0: # non zero status is error
headers["job-result"] = str(DONE)
for frame in job.frames:
headers["job-frame"] = str(frame.number)
# send result back to server
f = open(NODE_PREFIX + job.id + "%04d" % frame.number + ".exr", 'rb')
conn.request("PUT", "render", f, headers=headers)
f.close()
response = conn.getresponse()
else:
headers["job-result"] = str(ERROR)
for frame in job.frames:
headers["job-frame"] = str(frame.number)
# send error result back to server
conn.request("PUT", "render", headers=headers)
response = conn.getresponse()
for frame in job.frames:
headers["job-frame"] = str(frame.number)
# send log in any case
conn.request("PUT", "log", stdout, headers=headers)
response = conn.getresponse()
else:
if timeout < MAX_TIMEOUT:
timeout += INCREMENT_TIMEOUT
for i in range(timeout):
time.sleep(1)
if engine.test_break():
conn.close()
return
conn.close()

290
release/io/netrender/ui.py Normal file

@ -0,0 +1,290 @@
import bpy
import sys, os
import http, http.client, http.server, urllib
import subprocess, shutil, time, hashlib
import netrender.slave as slave
import netrender.master as master
VERSION = b"0.3"
PATH_PREFIX = "/tmp/"
QUEUED = 0
DISPATCHED = 1
DONE = 2
ERROR = 3
class RenderButtonsPanel(bpy.types.Panel):
__space_type__ = "PROPERTIES"
__region_type__ = "WINDOW"
__context__ = "scene"
# COMPAT_ENGINES must be defined in each subclass, external engines can add themselves here
def poll(self, context):
rd = context.scene.render_data
return (rd.use_game_engine==False) and (rd.engine in self.COMPAT_ENGINES)
# Setting panel, use in the scene for now.
class SCENE_PT_network_settings(RenderButtonsPanel):
__label__ = "Network Settings"
COMPAT_ENGINES = set(['NET_RENDER'])
def draw_header(self, context):
layout = self.layout
scene = context.scene
def draw(self, context):
layout = self.layout
scene = context.scene
rd = scene.render_data
layout.active = True
split = layout.split()
col = split.column()
col.itemR(scene.network_render, "mode")
col.itemR(scene.network_render, "server_address")
col.itemR(scene.network_render, "server_port")
if scene.network_render.mode == "RENDER_CLIENT":
col.itemR(scene.network_render, "chunks")
col.itemR(scene.network_render, "job_name")
col.itemO("render.netclientsend", text="send job to server")
bpy.types.register(SCENE_PT_network_settings)
class SCENE_PT_network_slaves(RenderButtonsPanel):
__label__ = "Slaves Status"
COMPAT_ENGINES = set(['NET_RENDER'])
def poll(self, context):
scene = context.scene
return super().poll(context) and scene.network_render.mode == "RENDER_CLIENT"
def draw(self, context):
layout = self.layout
scene = context.scene
netrender = scene.network_render
row = layout.row()
row.template_list(netrender, "slaves", netrender, "active_slave_index", rows=2)
col = row.column()
subcol = col.column(align=True)
subcol.itemO("render.netclientslaves", icon="ICON_FILE_REFRESH", text="")
subcol.itemO("render.netclientblacklistslave", icon="ICON_ZOOMOUT", text="")
if netrender.active_slave_index >= 0 and len(netrender.slaves) > 0:
layout.itemS()
slave = netrender.slaves[netrender.active_slave_index]
layout.itemL(text="Name: " + slave.name)
layout.itemL(text="Adress: " + slave.adress)
layout.itemL(text="Seen: " + slave.last_seen)
layout.itemL(text="Stats: " + slave.stats)
bpy.types.register(SCENE_PT_network_slaves)
class SCENE_PT_network_slaves_blacklist(RenderButtonsPanel):
__label__ = "Slaves Blacklist"
COMPAT_ENGINES = set(['NET_RENDER'])
def poll(self, context):
scene = context.scene
return super().poll(context) and scene.network_render.mode == "RENDER_CLIENT"
def draw(self, context):
layout = self.layout
scene = context.scene
netrender = scene.network_render
row = layout.row()
row.template_list(netrender, "slaves_blacklist", netrender, "active_blacklisted_slave_index", rows=2)
col = row.column()
subcol = col.column(align=True)
subcol.itemO("render.netclientwhitelistslave", icon="ICON_ZOOMOUT", text="")
if netrender.active_blacklisted_slave_index >= 0 and len(netrender.slaves_blacklist) > 0:
layout.itemS()
slave = netrender.slaves_blacklist[netrender.active_blacklisted_slave_index]
layout.itemL(text="Name: " + slave.name)
layout.itemL(text="Adress: " + slave.adress)
layout.itemL(text="Seen: " + slave.last_seen)
layout.itemL(text="Stats: " + slave.stats)
bpy.types.register(SCENE_PT_network_slaves_blacklist)
class SCENE_PT_network_jobs(RenderButtonsPanel):
__label__ = "Jobs"
COMPAT_ENGINES = set(['NET_RENDER'])
def poll(self, context):
scene = context.scene
return super().poll(context) and scene.network_render.mode == "RENDER_CLIENT"
def draw(self, context):
layout = self.layout
scene = context.scene
netrender = scene.network_render
row = layout.row()
row.template_list(netrender, "jobs", netrender, "active_job_index", rows=2)
col = row.column()
subcol = col.column(align=True)
subcol.itemO("render.netclientstatus", icon="ICON_FILE_REFRESH", text="")
subcol.itemO("render.netclientcancel", icon="ICON_ZOOMOUT", text="")
if netrender.active_job_index >= 0 and len(netrender.jobs) > 0:
layout.itemS()
job = netrender.jobs[netrender.active_job_index]
layout.itemL(text="Name: %s" % job.name)
layout.itemL(text="Length: %04i" % job.length)
layout.itemL(text="Done: %04i" % job.done)
layout.itemL(text="Error: %04i" % job.error)
bpy.types.register(SCENE_PT_network_jobs)
class NetRenderSettings(bpy.types.IDPropertyGroup):
pass
class NetRenderSlave(bpy.types.IDPropertyGroup):
pass
class NetRenderJob(bpy.types.IDPropertyGroup):
pass
bpy.types.register(NetRenderSettings)
bpy.types.register(NetRenderSlave)
bpy.types.register(NetRenderJob)
bpy.types.Scene.PointerProperty(attr="network_render", type=NetRenderSettings, name="Network Render", description="Network Render Settings")
NetRenderSettings.StringProperty( attr="server_address",
name="Server address",
description="IP or name of the master render server",
maxlen = 128,
default = "127.0.0.1")
NetRenderSettings.IntProperty( attr="server_port",
name="Server port",
description="port of the master render server",
default = 8000,
min=1,
max=65535)
NetRenderSettings.StringProperty( attr="job_name",
name="Job name",
description="Name of the job",
maxlen = 128,
default = "[default]")
NetRenderSettings.IntProperty( attr="chunks",
name="Chunks",
description="Number of frame to dispatch to each slave in one chunk",
default = 5,
min=1,
max=65535)
NetRenderSettings.StringProperty( attr="job_id",
name="Network job id",
description="id of the last sent render job",
maxlen = 64,
default = "")
NetRenderSettings.IntProperty( attr="active_slave_index",
name="Index of the active slave",
description="",
default = -1,
min= -1,
max=65535)
NetRenderSettings.IntProperty( attr="active_blacklisted_slave_index",
name="Index of the active slave",
description="",
default = -1,
min= -1,
max=65535)
NetRenderSettings.IntProperty( attr="active_job_index",
name="Index of the active job",
description="",
default = -1,
min= -1,
max=65535)
NetRenderSettings.EnumProperty(attr="mode",
items=(
("RENDER_CLIENT", "Client", "Act as render client"),
("RENDER_MASTER", "Master", "Act as render master"),
("RENDER_SLAVE", "Slave", "Act as render slave"),
),
name="network mode",
description="mode of operation of this instance",
default="RENDER_CLIENT")
NetRenderSettings.CollectionProperty(attr="slaves", type=NetRenderSlave, name="Slaves", description="")
NetRenderSettings.CollectionProperty(attr="slaves_blacklist", type=NetRenderSlave, name="Slaves Blacklist", description="")
NetRenderSettings.CollectionProperty(attr="jobs", type=NetRenderJob, name="Job List", description="")
NetRenderSlave.StringProperty( attr="name",
name="Name of the slave",
description="",
maxlen = 64,
default = "")
NetRenderSlave.StringProperty( attr="adress",
name="Adress of the slave",
description="",
maxlen = 64,
default = "")
NetRenderJob.StringProperty( attr="id",
name="ID of the job",
description="",
maxlen = 64,
default = "")
NetRenderJob.StringProperty( attr="name",
name="Name of the job",
description="",
maxlen = 128,
default = "")
NetRenderJob.IntProperty( attr="length",
name="Number of frames",
description="",
default = 0,
min= 0,
max=65535)
NetRenderJob.IntProperty( attr="done",
name="Number of frames rendered",
description="",
default = 0,
min= 0,
max=65535)
NetRenderJob.IntProperty( attr="error",
name="Number of frames in error",
description="",
default = 0,
min= 0,
max=65535)

@ -0,0 +1,80 @@
import bpy
import sys, os
import http, http.client, http.server, urllib
import subprocess, shutil, time, hashlib
VERSION = b"0.3"
PATH_PREFIX = "/tmp/"
QUEUED = 0
DISPATCHED = 1
DONE = 2
ERROR = 3
def clientConnection(scene):
netrender = scene.network_render
conn = http.client.HTTPConnection(netrender.server_address, netrender.server_port)
if clientVerifyVersion(conn):
return conn
else:
conn.close()
return None
def clientVerifyVersion(conn):
conn.request("GET", "version")
response = conn.getresponse()
if response.status != http.client.OK:
conn.close()
return False
server_version = response.read()
if server_version != VERSION:
print("Incorrect server version!")
print("expected", VERSION, "received", server_version)
return False
return True
def clientSendJob(conn, scene, anim = False, chunks = 5):
if anim:
job_frame = "%i:%i" % (scene.start_frame, scene.end_frame)
else:
job_frame = "%i" % (scene.current_frame, )
blacklist = []
filename = bpy.data.filename
name = scene.network_render.job_name
if name == "[default]":
path, name = os.path.split(filename)
for slave in scene.network_render.slaves_blacklist:
blacklist.append(slave.id)
blacklist = " ".join(blacklist)
headers = {"job-frame":job_frame, "job-name":name, "job-chunks": str(chunks), "slave-blacklist": blacklist}
# try to send path first
conn.request("POST", "job", filename, headers=headers)
response = conn.getresponse()
# if not found, send whole file
if response.status == http.client.NOT_FOUND:
f = open(bpy.data.filename, "rb")
conn.request("PUT", "file", f, headers=headers)
f.close()
response = conn.getresponse()
return response.getheader("job-id")
def clientRequestResult(conn, scene, job_id):
conn.request("GET", "render", headers={"job-id": job_id, "job-frame":str(scene.current_frame)})