diff options
author | Martin Poirier <theeth@yahoo.com> | 2009-08-29 21:25:22 +0400 |
---|---|---|
committer | Martin Poirier <theeth@yahoo.com> | 2009-08-29 21:25:22 +0400 |
commit | afee963155a2776066d377ed590e755217948d0f (patch) | |
tree | 7612d20af38df7bd4c97da9595e6bc8fde35d6f2 /release | |
parent | e6f2f4db285be1ccdc9c9871171afb1f9e2a4f6c (diff) |
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.
Diffstat (limited to 'release')
-rw-r--r-- | release/io/netrender/__init__.py | 9 | ||||
-rw-r--r-- | release/io/netrender/client.py | 87 | ||||
-rw-r--r-- | release/io/netrender/master.py | 546 | ||||
-rw-r--r-- | release/io/netrender/model.py | 150 | ||||
-rw-r--r-- | release/io/netrender/operators.py | 238 | ||||
-rw-r--r-- | release/io/netrender/slave.py | 147 | ||||
-rw-r--r-- | release/io/netrender/ui.py | 290 | ||||
-rw-r--r-- | release/io/netrender/utils.py | 80 |
8 files changed, 1547 insertions, 0 deletions
diff --git a/release/io/netrender/__init__.py b/release/io/netrender/__init__.py new file mode 100644 index 00000000000..e0de2726a55 --- /dev/null +++ b/release/io/netrender/__init__.py @@ -0,0 +1,9 @@ +# This directory is a Python package. + +import model +import operators +import client +import slave +import master +import utils +import ui diff --git a/release/io/netrender/client.py b/release/io/netrender/client.py new file mode 100644 index 00000000000..90039a3273a --- /dev/null +++ b/release/io/netrender/client.py @@ -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) + diff --git a/release/io/netrender/master.py b/release/io/netrender/master.py new file mode 100644 index 00000000000..5a62ef1a8bf --- /dev/null +++ b/release/io/netrender/master.py @@ -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 diff --git a/release/io/netrender/model.py b/release/io/netrender/model.py new file mode 100644 index 00000000000..6e68929d340 --- /dev/null +++ b/release/io/netrender/model.py @@ -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 diff --git a/release/io/netrender/operators.py b/release/io/netrender/operators.py new file mode 100644 index 00000000000..3355c3d1a0b --- /dev/null +++ b/release/io/netrender/operators.py @@ -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)
\ No newline at end of file diff --git a/release/io/netrender/slave.py b/release/io/netrender/slave.py new file mode 100644 index 00000000000..03c6803955e --- /dev/null +++ b/release/io/netrender/slave.py @@ -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() diff --git a/release/io/netrender/ui.py b/release/io/netrender/ui.py new file mode 100644 index 00000000000..2fd2b9a317f --- /dev/null +++ b/release/io/netrender/ui.py @@ -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) diff --git a/release/io/netrender/utils.py b/release/io/netrender/utils.py new file mode 100644 index 00000000000..c158ff115fa --- /dev/null +++ b/release/io/netrender/utils.py @@ -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)}) |