diff --git a/network/during_render.py b/network/during_render.py index 865b379..d6bbc74 100644 --- a/network/during_render.py +++ b/network/during_render.py @@ -12,6 +12,8 @@ import os import sys import json +import time +import signal import socket import datetime from subprocess import * @@ -53,11 +55,11 @@ def output(string): print(string) - for i in range(500): - cs1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - cs1.sendto(bytes(string, 'utf-8'), ('127.0.0.1', 54545)) + # for i in range(500): + # cs1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # cs1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + # cs1.sendto(bytes(string, 'utf-8'), ('127.0.0.1', 54545)) # Now to make it all work the script that will lanuch this script will give it # an agrument of a project location. Which we might need to use for rendering @@ -131,6 +133,27 @@ def read_settings(filename): return data +def get_runtime_data(project): + + # This will read a specific .json file from the system. + + template = {"to_render":True, + "current_progress":"Fra: 69 | Mem: 420 |Sample: 69/420"} + try: + with open(project+"/render_runtime.json") as json_file: + data = json.load(json_file) + except: + data = template.copy() + + return data + +def save_runtime_data(data, project): + with open(project+"/render_runtime.json", 'w') as fp: + json.dump(data, fp, indent=4) + + +runtime = get_runtime_data(project) + # Now let's start the main loop. I'm pretty sure that there will be tons of bugs # at this stage. So please look at the following code carefully. @@ -236,30 +259,55 @@ while True: while line: output("VCStudio : RENDERING : "+render+" : "+line) + # Now at this stage i want it to also listen to a # command to stop. I might want my CPU back at any # moment. So let's do this. - UDP_IP = "127.0.0.1" - UDP_PORT = 54545 + # UDP_IP = "127.0.0.1" + # UDP_PORT = 54545 - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((UDP_IP, UDP_PORT)) - sock.settimeout(0.05) - input_line, addr = sock.recvfrom(1024) - input_line = input_line.decode('utf8') - sock.close() + # try: + # sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # sock.bind((UDP_IP, UDP_PORT)) + # sock.settimeout(0.05) + # input_line, addr = sock.recvfrom(1024) + # input_line = input_line.decode('utf8') + # sock.close() - if input_line == "VCStudio : RENDER STOP": - progress.kill() + # if input_line == "VCStudio : RENDER STOP": + # progress.kill() + # output("VCStudio : CLOSED") + # to_break2 = True + # to_break = True + # exit() + # except Exception as e: + # pass + + runtime = get_runtime_data(project) + + if not runtime.get("to_render"): + try: + #progress.kill() + output("VCStudio : CLOSED") to_break2 = True to_break = True + + os.killpg(os.getpgid(progress.pid), signal.SIGKILL) + exit() - except Exception as e: - pass + except Exception as e: + runtime["error"] = str(e) + save_runtime_data(runtime, project) + + runtime["current_progress"] = line + runtime["current_file"] = render + runtime["running"] = int(time.time()) + runtime["blender_id"] = int(progress.pid) + + save_runtime_data(runtime, project) line = progress.stdout.readline()[:-1] diff --git a/network/network_renders.py b/network/network_renders.py index fb338b7..52999aa 100644 --- a/network/network_renders.py +++ b/network/network_renders.py @@ -53,8 +53,27 @@ import os import json +import time import socket +def get_runtime_data(project): + + # This will read a specific .json file from the system. + + template = {"to_render":True, + "current_progress":"Fra: 69 | Mem: 420 |Sample: 69/420"} + try: + with open(project+"/render_runtime.json") as json_file: + data = json.load(json_file) + except: + data = template.copy() + + return data + +def save_runtime_data(data, project): + with open(project+"/render_runtime.json", 'w') as fp: + json.dump(data, fp, indent=4) + def read_renders(win): # This function will listen for 127.0.0.1 port 54545 for data about current @@ -136,119 +155,119 @@ def read_renders(win): string_read = "" - try: - UDP_IP = "127.0.0.1" - UDP_PORT = 54545 + # try: + # UDP_IP = "127.0.0.1" + # UDP_PORT = 54545 - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((UDP_IP, UDP_PORT)) + # sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # sock.bind((UDP_IP, UDP_PORT)) - # Now usually it will wait for a message untill one appears. But render - # could be finished. Or some error might accur. Or it could be that no - # render what so ever. So I don't want the UI to freez. So I'm going to - # put a timeout. Very short timeout. + # # Now usually it will wait for a message untill one appears. But render + # # could be finished. Or some error might accur. Or it could be that no + # # render what so ever. So I don't want the UI to freez. So I'm going to + # # put a timeout. Very short timeout. - sock.settimeout(0.005) + # sock.settimeout(0.005) - # This comes with it's own problems. Mainly I tested I need to send - # about 500 messages at ones for it to be recognized at all. But it's - # not a big deal what so ever. Just need to keep in mind. 500 messages. + # # This comes with it's own problems. Mainly I tested I need to send + # # about 500 messages at ones for it to be recognized at all. But it's + # # not a big deal what so ever. Just need to keep in mind. 500 messages. - data, addr = sock.recvfrom(1024) - data = data.decode('utf8') + # data, addr = sock.recvfrom(1024) + # data = data.decode('utf8') - sock.close() + # sock.close() - string_read = data + # string_read = data - except: - pass - - # Now let's do something with this data we read from the stream. - # We gonna complitelly ignore anything unrelated. Such as not from - # the project. - - if "network_render_timeout_frame" not in win.current: - win.current["network_render_timeout_frame"] = win.current["frame"] - - if string_read.startswith("VCStudio : "): - if ": RENDERING :" in string_read: - - blend, blend_string = string_read.split(" : ")[2], string_read.split(" : ")[3] - - # So we've got 2 peaces of data from the renderer. Blender. Which - # is a path to the blend file. Similar to the one in - # active_renders.data file that we read previously. - - # And we've got a raw string that blender outputs during render. - # Example: - # Fra:70 Mem:93.09M (Peak 97.78M) | Time:00:01.83 | Rendering 26 / 64 samples - - # You can find a string like this in the top banner inside the blender. - - # From this string we can find out a bunch of things. But not everything is - # that simple. Cycles and Eevee for exmple output different strings. Any - # other, wild render engine will output it's own string. - - # Now I'd like to actually load the data from the JSON file at every step - # like this. Because our rendering script will be recording and saving - # render times of each frame into the file. I need it for analytics. - - if blend in win.renders: - - folder = blend[:blend.rfind("/")]+"/extra" - savefile = folder+blend[blend.rfind("/"):]+".json" - - # It might not exits so we are going to do this with try. - - try: - with open(win.project+savefile) as json_file: - data = json.load(json_file) - - win.renders[blend] = data - except: - pass - - win.renders[blend]["rendering"] = blend_string - - # Next is let's try finding out the current frame rendering. It's - # probably not that hard. Every string usually starts with Fra: - - # So let's try doing it. - frame = 0 - try: - frame = int(blend_string[4:blend_string.find(" ")]) - except: - pass - - win.renders[blend]["current_frame"] = frame - - # The rest of it I will do in the UI. - - win.current["network_render_timeout_frame"] = win.current["frame"] - - - # Now sometimes for no particular reason the whole rendering thing could crash - # or in other words. If there is no data sent. We want to clean the renderings - # and make it so the user sees that renders DO NOT render. For this there should - # be a timeout system. Basically if in a given amount of frames there is no message - # from the renderer script. We going to say that it's not rendering. - - if win.current["frame"] - win.current["network_render_timeout_frame"] > 100: + # except: + # pass + + runtime = get_runtime_data(win.project) + + if int(time.time()) > runtime.get("running", 0) + 100 or not runtime.get("to_render"): + #print("Should be off") for blend in win.renders: win.renders[blend]["rendering"] = False + + else: + #print("Should be on...") + + blend, blend_string = runtime.get("current_file", ""), runtime.get("current_progress", "") + + # So we've got 2 peaces of data from the renderer. Blender. Which + # is a path to the blend file. Similar to the one in + # active_renders.data file that we read previously. + + # And we've got a raw string that blender outputs during render. + # Example: + # Fra:70 Mem:93.09M (Peak 97.78M) | Time:00:01.83 | Rendering 26 / 64 samples + + # You can find a string like this in the top banner inside the blender. + + # From this string we can find out a bunch of things. But not everything is + # that simple. Cycles and Eevee for exmple output different strings. Any + # other, wild render engine will output it's own string. + + # Now I'd like to actually load the data from the JSON file at every step + # like this. Because our rendering script will be recording and saving + # render times of each frame into the file. I need it for analytics. + + if blend in win.renders: + + folder = blend[:blend.rfind("/")]+"/extra" + savefile = folder+blend[blend.rfind("/"):]+".json" + + # It might not exits so we are going to do this with try. + + try: + with open(win.project+savefile) as json_file: + data = json.load(json_file) + + win.renders[blend] = data + except: + pass + + win.renders[blend]["rendering"] = blend_string + + # Next is let's try finding out the current frame rendering. It's + # probably not that hard. Every string usually starts with Fra: + + # So let's try doing it. + frame = 0 + try: + frame = int(blend_string[4:blend_string.find(" ")]) + except: + pass + + win.renders[blend]["current_frame"] = frame + + # The rest of it I will do in the UI. + + + + + def stop_render(win): # This function will stop the rendering. It will bombard the renderer with # stop messages. - for i in range(5000): - cs1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - cs1.sendto(bytes("VCStudio : RENDER STOP", 'utf-8'), ('127.0.0.1', 54545)) + # for i in range(5000): + # cs1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # cs1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + # cs1.sendto(bytes("VCStudio : RENDER STOP", 'utf-8'), ('127.0.0.1', 54545)) + + runtime = get_runtime_data(win.project) + runtime["to_render"] = False + save_runtime_data(runtime, win.project) + + for blend in win.renders: + win.renders[blend]["rendering"] = False + diff --git a/network/render_viewer.py b/network/render_viewer.py new file mode 100644 index 0000000..9954352 --- /dev/null +++ b/network/render_viewer.py @@ -0,0 +1,355 @@ +# Part of VCStudio +# GPLv3 or later + +# This file will visualize the rendering process + +import os +import sys +import json +import time + +if len(sys.argv) == 1: + project = input("Project: ") +else: + project = sys.argv[1] + +def get_runtime_data(project): + + # This will read a specific .json file from the system. + + template = {"to_render":True, + "current_progress":"Fra: 69 | Mem: 420 |Sample: 69/420"} + try: + with open(project+"/render_runtime.json") as json_file: + data = json.load(json_file) + except: + data = template.copy() + + return data + +def save_runtime_data(data, project): + with open(project+"/render_runtime.json", 'w') as fp: + json.dump(data, fp, indent=4) + + +# Colors are used to make the +clr = { + "norm":"\033[00m", # Reset to normal + "bold":"\033[01m", # Bold Text + "ital":"\033[03m", # Italic Text + "undr":"\033[04m", # Underlined + "blnk":"\033[05m", # Blinking + + # Text + "tdbl":"\033[30m", # Dark Black + "tdrd":"\033[31m", # Dark Red + "tdgr":"\033[32m", # Dark Green + "tdyl":"\033[33m", # Dark Yellow + "tdbu":"\033[34m", # Dark Blue + "tdma":"\033[35m", # Dark Magenta + "tdcy":"\033[36m", # Dark Cyan + "tdwh":"\033[37m", # Dark White + + "tbbl":"\033[90m", # Bright Black + "tbrd":"\033[91m", # Bright Red + "tbgr":"\033[92m", # Bright Green + "tbyl":"\033[93m", # Bright Yellow + "tbbu":"\033[94m", # Bright Blue + "tbma":"\033[95m", # Bright Magenta + "tbcy":"\033[96m", # Bright Cyan + "tbwh":"\033[97m", # Bright White + # Background + "bdbl":"\033[40m", # Dark Black + "bdrd":"\033[41m", # Dark Red + "bdgr":"\033[42m", # Dark Green + "bdyl":"\033[43m", # Dark Yellow + "bdbu":"\033[44m", # Dark Blue + "bdma":"\033[45m", # Dark Magenta + "bdcy":"\033[46m", # Dark Cyan + "bdwh":"\033[47m", # Dark White + + "bbbl":"\033[100m", # Bright Black + "bbrd":"\033[101m", # Bright Red + "bbgr":"\033[102m", # Bright Green + "bbyl":"\033[103m", # Bright Yellow + "bbbu":"\033[104m", # Bright Blue + "bbma":"\033[105m", # Bright Magenta + "bbcy":"\033[106m", # Bright Cyan + "bbwh":"\033[108m" # Bright White +} + +# A function that insures a specific width of the printed part +def wdth(x, n): + + # Convert Data to String + mode = "normal" + if type(x) == bool and x == True: + x = "V" + mode = "bdgr" + elif type(x) == bool and x == False: + x = "X" + mode = "bdrd" + else: + x = str(x) + + # Turn emogis + #x = emote(x) + + # Some characters are too wide. They do not obey the + # monospace of the terminal, thus making it not pretty. + + # This is the string of characters which are checked to + good = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'’()*+,-./:;<=>?@[\]^_`{|}~ йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮёЁ" + + # let's filter the string + y = x + + + x = "" + for i in y: + if i in good: + x = x + i + else: + x = x + "▓" + + # Now let's print what we've got. + if len(y) < n: + fac = n-len(y) + fac1 = int(round(fac/2)) + fac2 = fac1 + while fac1 + fac2 > fac: + fac2 -=1 + while fac1 + fac2 < fac: + fac2 +=1 + x = (" "*fac1)+x+(" "*fac2) + elif len(y) > n: + if n > 10: + x = x[:n-3]+"..." + else: + x = x[:n] + if mode == "normal": + return x + else: + return clr[mode]+clr["bold"]+x+clr["norm"] + +def tsize(): + + # This funtion will get the size of the terminal and + # return it to the variables provided width, height + + # On some systems this may not work. So there is a + # try function. + + try: + # Getting the size of the terminal + import os + w, h = os.get_terminal_size() + + # Sometimes when the terminal width is either + # even or odd. It breaks code for some other + # thing written differenly. For example: + + # You may have an even width ( like 84 ) when + # writing a function. And then it works on different + # widths as well like 62 and 80 and 48. All of them + # are still even. Then you scale the terminal to be + # something off like 63 and the function breaks. You + # have one character too much or one character too little. + + # This is why I do not want to have a difference. And + # force width to be one less, if it's not divisible by 2. + + if not w % 2: + w = w - 1 + + return w, h + + except: + + # If, by any reason the terminal can't get it's size. + # We want to return some size regardless. + + w = 60 + h = 20 + + return w, h + + +def table(data, number=True): + + # This function will present data in a pretty table thing. + + # So let's first of all get the size of the terminal + w, h = tsize() + + if number: + w = w - 4 + + # Then let's draw the categories for this we need to extract + # it's sizes. If there is no 'size' variable the sizes of + # each category will be spread equally. + + size = [] # Here the size will go as pure character value. + + if "size" in data: + for i in data["size"]: + size.append(int(( w - 10 ) / sum(data["size"]) * i)) + + while sum(size) < w - 10: + size[-1] += 1 + + # printing categories + nb = "" + if number: + nb = " " + s = " "+clr["bdma"]+" "+clr["tbwh"]+nb + + for n, item in enumerate(data["categories"]): + s = s + wdth(item.upper(), size[n]) + print(s+clr["bdma"]+" "+clr["norm"]) + + size[-1] += 1 + + # printing items + for b, i in enumerate(data["data"]): + + # dark bright sequence thingy + if b % 2: + d = "b" + else: + d = "d" + nb = "" + if number: + nb = clr["tbwh"]+wdth(b,4) + s = " "+clr["bdma"]+" "+nb+clr["norm"]+clr["b"+d+"bu"]#+clr["tbwh"] + for n, item in enumerate(i): + s = s +clr["b"+d+"bu"]+ clr["tbwh"]+wdth(item, size[n]-1)+clr["bdma"]+" " + print(s+clr["norm"]) + +def center(line, c="bdma", blink=False): + + # This funtiocn will bring a given string of text + # in the center of the terminal with a nice backgroud + # around it. + + + w, h = tsize() + + if blink: + blink = clr["blnk"] + else: + blink = "" + + if len(line) % 2: + line = line + " " + + if len(line) < w - 11: + print(" "+clr[c], + wdth(" ", int((w-10)/2 - (len(line)/2))), + clr["bold"]+clr["tbwh"]+blink+line, + wdth(" ", int((w-10)/2 - (len(line)/2))-1), + clr["norm"]) + else: + print(" "+clr[c], + clr["bold"]+clr["tbwh"]+blink+wdth(line,w-10), + clr["norm"]) + +running_for = 0 +lastframe = 0 +maxtime = 1000000 + +while True: + + running_for = running_for + 1 + + runtime = get_runtime_data(project) + if not runtime.get("to_render"): + break + + x,y = tsize() + + if y > 8: + print("\n\n") + + process = runtime.get("current_progress", "").split(" | ") + try: + del runtime["current_progress"] + except: + pass + + data_print = {"categories":list(runtime.keys()), + "size":[1]*(len(runtime)), + "data":[list(runtime.values())]} + + table(data_print, False) + + data_print = {"categories":[""]*(len(process)), + "size":[1]*(len(process)), + "data":[process]} + + + + + table(data_print, False) + center("") + + + # Latest frames data + + filename = runtime.get("current_file", "") + folder = filename[:filename.rfind("/")]+"/extra" + savefile = folder+filename[filename.rfind("/"):]+".json" + + try: + with open(project+savefile) as json_file: + data = json.load(json_file) + except: + data = {} + + listoftimesx = data.get("analytics", {}).get(data.get("save_folder", ""), []) + listoftimes = {} + minframe = data.get("start_frame", 0) + for i in listoftimesx: + try: + listoftimes[int(i)] = listoftimesx[i] + except: + pass + try: + maxframe = max(list(listoftimes.keys())) + + + + if maxframe > lastframe: + lastframe = maxframe + running_for = 0 + maxtime = max(list(listoftimes.values())) + maxtime = max(maxtime, running_for*1000000) + + print() + + + for i in range(max(minframe,maxframe-y+16), maxframe+1): + + amount = int((x-14)/(maxtime-minframe)*(listoftimes[i]-minframe)) + + + print(" ",wdth(i, 4), "▒"*amount) + except Exception as e: + #print(e) + maxframe = 0 + #maxtime = 1 + lastframe = maxframe + running_for = 0 + + amount = int((x-14)/maxtime*running_for*1000000) + + print(" " , wdth(maxframe+1, 4), "█"*amount) + + + amount = int((x-14)/(data.get("end_frame", maxframe+1)-minframe)*(maxframe+1-minframe)) + + print() + print(" "+clr["tbma"]+wdth(data.get("end_frame", maxframe+1), 4)+" "+("█"*amount)+clr["norm"]) + + time.sleep(1) + os.system("clear")