Commit 462773b5 authored by Christopher Gearhart's avatar Christopher Gearhart

Missing frames now displayed and frame range (also fixed a few minor bugs)

parent a60587f9
...@@ -6,22 +6,19 @@ Scripts and associated files for rendering from Blender files on CSE remote serv ...@@ -6,22 +6,19 @@ Scripts and associated files for rendering from Blender files on CSE remote serv
* Features: * Features:
* Clean UI for sending frames to servers and viewing them within Blender * Clean UI for sending frames to servers and viewing them within Blender
* Mid-render previews/status updates available with 'SHIFT + P' * Mid-render previews/status updates available with 'SHIFT + P'
* Abort render ('ESC') * Abort render with 'ESC'
* NOTE: Files are auto-packed into the .blend file with each render process * NOTE: Files are auto-packed into the .blend file with each render process
* Required packages: * Required packages:
* Local: rsync * Local: rsync
* Host Server: rsync, python * Host Server: rsync, python
* Client Servers: blender * Client Servers: blender
* Future improvements: * Future improvements:
* Remove restriction from using spaces in project name
* Handle known errors * Handle known errors
* Detect when SSH keys have not been set up
* Detect when required packages have not been installed on servers (see 'which' command) * Detect when required packages have not been installed on servers (see 'which' command)
* Detect if you've run out of disk space * Detect if you've run out of disk space
* Don't pack files into the blend file? * Don't pack files into the blend file?
* 'blender_task' module * 'blender_task' module
* Integrate max server load functionality to set cap on how many frames will be rendered * Integrate max server load functionality to set cap on how many frames will be rendered
* If servers available, re-render current jobs until one is finished, then kill the rest
* Re-render failed frames automatically * Re-render failed frames automatically
* Handle known errors (see 'Handle known errors' list above) * Handle known errors (ssh keys, necessary packages not installed, run out of disk space, etc.)
* Send tiles to the various servers based on computer speed * Send tiles to the various servers based on computer speed
...@@ -5,11 +5,12 @@ import subprocess ...@@ -5,11 +5,12 @@ import subprocess
import os import os
import sys import sys
import fnmatch import fnmatch
import itertools
import operator
from .setupServers import * from .setupServers import *
def getFrames(projectName, archiveFiles=False, frameRange=False): def getFrames(projectName, archiveFiles=False, frameRange=False):
""" rsync rendered frames from host server to local machine """ """ rsync rendered frames from host server to local machine """
scn = bpy.context.scene scn = bpy.context.scene
basePath = bpy.path.abspath("//") basePath = bpy.path.abspath("//")
dumpLocation = getRenderDumpFolder() dumpLocation = getRenderDumpFolder()
...@@ -40,7 +41,6 @@ def getFrames(projectName, archiveFiles=False, frameRange=False): ...@@ -40,7 +41,6 @@ def getFrames(projectName, archiveFiles=False, frameRange=False):
def buildFrameRangesString(frameRanges): def buildFrameRangesString(frameRanges):
""" builds frame range list of lists/ints from user-entered frameRanges string """ """ builds frame range list of lists/ints from user-entered frameRanges string """
frameRangeList = frameRanges.replace(" ", "").split(",") frameRangeList = frameRanges.replace(" ", "").split(",")
newFrameRangeList = [] newFrameRangeList = []
invalidDict = {"valid":False, "string":None} invalidDict = {"valid":False, "string":None}
...@@ -68,7 +68,6 @@ def buildFrameRangesString(frameRanges): ...@@ -68,7 +68,6 @@ def buildFrameRangesString(frameRanges):
def copyProjectFile(projectName, compress): def copyProjectFile(projectName, compress):
""" copies project file from local machine to host server """ """ copies project file from local machine to host server """
scn = bpy.context.scene scn = bpy.context.scene
bpy.ops.file.pack_all() bpy.ops.file.pack_all()
saveToPath = "{tempLocalDir}{projectName}.blend".format(tempLocalDir=scn.tempLocalDir, projectName=projectName) saveToPath = "{tempLocalDir}{projectName}.blend".format(tempLocalDir=scn.tempLocalDir, projectName=projectName)
...@@ -96,7 +95,6 @@ def copyFiles(): ...@@ -96,7 +95,6 @@ def copyFiles():
def renderFrames(frameRange, projectName, jobsPerFrame=False): def renderFrames(frameRange, projectName, jobsPerFrame=False):
""" calls 'blender_task' on host server """ """ calls 'blender_task' on host server """
scn = bpy.context.scene scn = bpy.context.scene
# defines the name of the output files generated by 'blender_task' # defines the name of the output files generated by 'blender_task'
extraFlags = " -O {nameOutputFiles}".format(nameOutputFiles=getNameOutputFiles()) extraFlags = " -O {nameOutputFiles}".format(nameOutputFiles=getNameOutputFiles())
...@@ -130,7 +128,6 @@ def removeViewable(typeOfRender): ...@@ -130,7 +128,6 @@ def removeViewable(typeOfRender):
def expandFrames(frame_range): def expandFrames(frame_range):
""" Helper function takes frame range string and returns list with frame ranges expanded """ """ Helper function takes frame range string and returns list with frame ranges expanded """
frames = [] frames = []
for i in frame_range: for i in frame_range:
if type(i) == list: if type(i) == list:
...@@ -142,18 +139,29 @@ def expandFrames(frame_range): ...@@ -142,18 +139,29 @@ def expandFrames(frame_range):
return list(set(frames)) return list(set(frames))
def intsToFrameRanges(intsList):
""" turns list of ints to list of frame ranges """
frameRangesS = ""
i = 0
while i < len(intsList) - 1:
s = intsList[i] # start index
e = s # end index
while i < len(intsList) - 1 and intsList[i + 1] - intsList[i] == 1:
e += 1
i += 1
frameRangesS += "{s},".format(s=s) if s == e else "{s}-{e},".format(s=s, e=e)
i += 1
return frameRangesS[:-1]
def listMissingFiles(filename, frameRange): def listMissingFiles(filename, frameRange):
""" lists all missing files from local render dump directory """ """ lists all missing files from local render dump directory """
dumpFolder = getRenderDumpFolder() dumpFolder = getRenderDumpFolder()
compList = expandFrames(json.loads(frameRange)) compList = expandFrames(json.loads(frameRange))
if not os.path.exists(dumpFolder): if not os.path.exists(dumpFolder):
errorMsg = "The folder does not exist: {dumpFolder}/".format(dumpFolder=dumpFolder) errorMsg = "The folder does not exist: {dumpFolder}/".format(dumpFolder=dumpFolder)
sys.stderr.write(errorMsg) sys.stderr.write(errorMsg)
print(errorMsg) print(errorMsg)
return str(compList)[1:-1] return str(compList)[1:-1]
try: try:
allFiles = os.listdir(dumpFolder) allFiles = os.listdir(dumpFolder)
except: except:
...@@ -165,16 +173,15 @@ def listMissingFiles(filename, frameRange): ...@@ -165,16 +173,15 @@ def listMissingFiles(filename, frameRange):
for f in allFiles: for f in allFiles:
if "_average." not in f and not fnmatch.fnmatch(f, "*_seed-*_????.???") and f[:len(filename)] == filename: if "_average." not in f and not fnmatch.fnmatch(f, "*_seed-*_????.???") and f[:len(filename)] == filename:
imList.append(int(f[len(filename)+1:len(filename)+5])) imList.append(int(f[len(filename)+1:len(filename)+5]))
# compare lists to determine which frames are missing from imlist # compare lists to determine which frames are missing from imlist
missingF = [i for i in compList if i not in imList] missingF = [i for i in compList if i not in imList]
# convert list of ints to string with frame ranges
missingFR = intsToFrameRanges(missingF)
# return the list of missing frames as string, omitting the open and close brackets # return the list of missing frames as string, omitting the open and close brackets
return str(missingF)[1:-1] return missingFR
def handleError(classObject, errorSource, i="Not Provided"): def handleError(classObject, errorSource, i="Not Provided"):
errorMessage = False errorMessage = False
# if error message available, print in Info window and define errorMessage string # if error message available, print in Info window and define errorMessage string
if i == "Not Provided": if i == "Not Provided":
if classObject.process.stderr != None: if classObject.process.stderr != None:
...@@ -210,7 +217,6 @@ def handleBTError(classObject, i="Not Provided"): ...@@ -210,7 +217,6 @@ def handleBTError(classObject, i="Not Provided"):
def setFrameRangesDict(classObject): def setFrameRangesDict(classObject):
scn = bpy.context.scene scn = bpy.context.scene
if scn.frameRanges == "": if scn.frameRanges == "":
classObject.frameRangesDict = {"string":"[[{frameStart},{frameEnd}]]".format(frameStart=str(scn.frame_start), frameEnd=str(scn.frame_end))} classObject.frameRangesDict = {"string":"[[{frameStart},{frameEnd}]]".format(frameStart=str(scn.frame_start), frameEnd=str(scn.frame_end))}
else: else:
...@@ -222,7 +228,6 @@ def setFrameRangesDict(classObject): ...@@ -222,7 +228,6 @@ def setFrameRangesDict(classObject):
def getRenderDumpFolder(): def getRenderDumpFolder():
dumpLoc = bpy.context.scene.renderDumpLoc dumpLoc = bpy.context.scene.renderDumpLoc
# setup the render dump folder based on user input # setup the render dump folder based on user input
if dumpLoc.startswith("//"): if dumpLoc.startswith("//"):
dumpLoc = os.path.join(bpy.path.abspath("//"), dumpLoc[2:]) dumpLoc = os.path.join(bpy.path.abspath("//"), dumpLoc[2:])
...@@ -231,11 +236,9 @@ def getRenderDumpFolder(): ...@@ -231,11 +236,9 @@ def getRenderDumpFolder():
# if no user input, use default render location # if no user input, use default render location
else: else:
dumpLoc = os.path.join(bpy.path.abspath("//"), "render-dump") dumpLoc = os.path.join(bpy.path.abspath("//"), "render-dump")
# check to make sure dumpLoc exists on local machine # check to make sure dumpLoc exists on local machine
if not os.path.exists(dumpLoc): if not os.path.exists(dumpLoc):
os.mkdir(dumpLoc) os.mkdir(dumpLoc)
return dumpLoc return dumpLoc
def getRunningStatuses(): def getRunningStatuses():
...@@ -269,7 +272,6 @@ def getNumRenderedFiles(jobType, frameRange=None, fileName=None): ...@@ -269,7 +272,6 @@ def getNumRenderedFiles(jobType, frameRange=None, fileName=None):
def cleanupCancelledRender(classObject, context, killPython=True): def cleanupCancelledRender(classObject, context, killPython=True):
""" Kills running processes when render job cancelled """ """ Kills running processes when render job cancelled """
wm = context.window_manager wm = context.window_manager
wm.event_timer_remove(classObject._timer) wm.event_timer_remove(classObject._timer)
for j in range(len(classObject.processes)): for j in range(len(classObject.processes)):
...@@ -304,7 +306,7 @@ def updateServerPrefs(): ...@@ -304,7 +306,7 @@ def updateServerPrefs():
if bpy.props.serverPrefs != oldServerPrefs: if bpy.props.serverPrefs != oldServerPrefs:
# verify host server login, built from user entries, correspond to a responsive server # verify host server login, built from user entries, correspond to a responsive server
try: try:
subprocess.call("ssh -T -oStrictHostKeyChecking=no -x {login} 'echo hi'".format(login=bpy.props.serverPrefs["login"]), shell=True) subprocess.call("ssh -T -oBatchMode=yes -oStrictHostKeyChecking=no -x {login} 'echo hi'".format(login=bpy.props.serverPrefs["login"]), shell=True)
except: except:
return {"valid":False, "errorMessage":"ssh to '{login}' failed. Check your settings and ensure ssh keys are setup".format(login=bpy.props.serverPrefs["login"])} return {"valid":False, "errorMessage":"ssh to '{login}' failed. Check your settings and ensure ssh keys are setup".format(login=bpy.props.serverPrefs["login"])}
......
...@@ -59,11 +59,6 @@ def setupServerPrefs(): ...@@ -59,11 +59,6 @@ def setupServerPrefs():
extension = readFileFor(serverFile, "EXTENSION").replace("\"", "") extension = readFileFor(serverFile, "EXTENSION").replace("\"", "")
except: except:
return {"valid":False, "errorMessage":invalidEntry("EXTENSION")} return {"valid":False, "errorMessage":invalidEntry("EXTENSION")}
try:
email = readFileFor(serverFile, "EMAIL ADDRESS").replace("\"", "")
except:
return {"valid":False, "errorMessage":invalidEntry("EMAIL ADDRESS")}
# build SSH login information # build SSH login information
login = "{username}@{hostServer}{extension}".format(username=username, hostServer=hostServer, extension=extension) login = "{username}@{hostServer}{extension}".format(username=username, hostServer=hostServer, extension=extension)
...@@ -92,7 +87,7 @@ def setupServerPrefs(): ...@@ -92,7 +87,7 @@ def setupServerPrefs():
except: except:
return {"valid":False, "errorMessage":"Could not load dictionary. Please make sure you've entered a valid dictionary and check for syntax errors"} return {"valid":False, "errorMessage":"Could not load dictionary. Please make sure you've entered a valid dictionary and check for syntax errors"}
return {"valid":True, "servers":servers, "login":login, "path":path, "hostConnection":hostConnection, "email":email} return {"valid":True, "servers":servers, "login":login, "path":path, "hostConnection":hostConnection}
def writeServersFile(serverDict, serverGroups): def writeServersFile(serverDict, serverGroups):
f = open(os.path.join(getLibraryPath(), "to_host_server", "servers.txt"), "w") f = open(os.path.join(getLibraryPath(), "to_host_server", "servers.txt"), "w")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment