Blender GPU / CPU Render (#81)

* Add script to get GPU information from Blender

* Change run_python_script to allow it to run without a project file

* Simplify run_python_script code

* Fix mistake

* Add system_info to engine classes and api_server. /api/renderer_info now supports standard and full response modes.

* Get full renderer_info response for add job UI

* Enable setting specific Blender render_device using args

* Add Blender render device options to UI
This commit is contained in:
2024-08-03 18:26:56 -05:00
committed by GitHub
parent 9bc490acae
commit ef4fc0e42e
8 changed files with 107 additions and 42 deletions

View File

@@ -363,6 +363,8 @@ def status():
@server.get('/api/renderer_info') @server.get('/api/renderer_info')
def renderer_info(): def renderer_info():
response_type = request.args.get('response_type', 'standard')
def process_engine(engine): def process_engine(engine):
try: try:
# Get all installed versions of the engine # Get all installed versions of the engine
@@ -373,14 +375,27 @@ def renderer_info():
install_path = system_installed_versions[0]['path'] if system_installed_versions else \ install_path = system_installed_versions[0]['path'] if system_installed_versions else \
installed_versions[0]['path'] installed_versions[0]['path']
return { en = engine(install_path)
engine.name(): {
'is_available': RenderQueue.is_available_for_job(engine.name()), if response_type == 'full': # Full dataset - Can be slow
'versions': installed_versions, return {
'supported_extensions': engine.supported_extensions(), en.name(): {
'supported_export_formats': engine(install_path).get_output_formats() 'is_available': RenderQueue.is_available_for_job(en.name()),
'versions': installed_versions,
'supported_extensions': engine.supported_extensions(),
'supported_export_formats': en.get_output_formats(),
'system_info': en.system_info()
}
} }
} elif response_type == 'standard': # Simpler dataset to reduce response times
return {
en.name(): {
'is_available': RenderQueue.is_available_for_job(en.name()),
'versions': installed_versions,
}
}
else:
raise AttributeError(f"Invalid response_type: {response_type}")
except Exception as e: except Exception as e:
logger.error(f'Error fetching details for {engine.name()} renderer: {e}') logger.error(f'Error fetching details for {engine.name()} renderer: {e}')
return {} return {}

View File

@@ -248,17 +248,18 @@ class RenderServerProxy:
# --- Renderer --- # # --- Renderer --- #
def get_renderer_info(self, timeout=5): def get_renderer_info(self, response_type='standard', timeout=5):
""" """
Fetches renderer information from the server. Fetches renderer information from the server.
Args: Args:
response_type (str, optional): Returns standard or full version of renderer info
timeout (int, optional): The number of seconds to wait for a response from the server. Defaults to 5. timeout (int, optional): The number of seconds to wait for a response from the server. Defaults to 5.
Returns: Returns:
dict: A dictionary containing the renderer information. dict: A dictionary containing the renderer information.
""" """
all_data = self.request_data(f'renderer_info', timeout=timeout) all_data = self.request_data(f"renderer_info?response_type={response_type}", timeout=timeout)
return all_data return all_data
def delete_engine(self, engine, version, system_cpu=None): def delete_engine(self, engine, version, system_cpu=None):

View File

@@ -56,25 +56,27 @@ class Blender(BaseRenderEngine):
else: else:
raise FileNotFoundError(f'Project file not found: {project_path}') raise FileNotFoundError(f'Project file not found: {project_path}')
def run_python_script(self, project_path, script_path, timeout=None): def run_python_script(self, script_path, project_path=None, timeout=None):
if os.path.exists(project_path) and os.path.exists(script_path):
try: if project_path and not os.path.exists(project_path):
return subprocess.run([self.renderer_path(), '-b', project_path, '--python', script_path],
capture_output=True, timeout=timeout)
except Exception as e:
logger.warning(f"Error running python script in blender: {e}")
pass
elif not os.path.exists(project_path):
raise FileNotFoundError(f'Project file not found: {project_path}') raise FileNotFoundError(f'Project file not found: {project_path}')
elif not os.path.exists(script_path): elif not os.path.exists(script_path):
raise FileNotFoundError(f'Python script not found: {script_path}') raise FileNotFoundError(f'Python script not found: {script_path}')
raise Exception("Uncaught exception")
try:
command = [self.renderer_path(), '-b', '--python', script_path]
if project_path:
command.insert(2, project_path)
return subprocess.run(command, capture_output=True, timeout=timeout)
except Exception as e:
logger.exception(f"Error running python script in blender: {e}")
def get_project_info(self, project_path, timeout=10): def get_project_info(self, project_path, timeout=10):
scene_info = {} scene_info = {}
try: try:
script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_file_info.py') script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_file_info.py')
results = self.run_python_script(project_path, system_safe_path(script_path), timeout=timeout) results = self.run_python_script(project_path=project_path, script_path=system_safe_path(script_path),
timeout=timeout)
result_text = results.stdout.decode() result_text = results.stdout.decode()
for line in result_text.splitlines(): for line in result_text.splitlines():
if line.startswith('SCENE_DATA:'): if line.startswith('SCENE_DATA:'):
@@ -92,7 +94,8 @@ class Blender(BaseRenderEngine):
try: try:
logger.info(f"Starting to pack Blender file: {project_path}") logger.info(f"Starting to pack Blender file: {project_path}")
script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'pack_project.py') script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'pack_project.py')
results = self.run_python_script(project_path, system_safe_path(script_path), timeout=timeout) results = self.run_python_script(project_path=project_path, script_path=system_safe_path(script_path),
timeout=timeout)
result_text = results.stdout.decode() result_text = results.stdout.decode()
dir_name = os.path.dirname(project_path) dir_name = os.path.dirname(project_path)
@@ -144,12 +147,20 @@ class Blender(BaseRenderEngine):
return options return options
def get_detected_gpus(self): def system_info(self):
# no longer works on 4.0 return {'render_devices': self.get_render_devices()}
engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT,
capture_output=True).stdout.decode('utf-8') def get_render_devices(self):
gpu_names = re.findall(r"DETECTED GPU: (.+)", engine_output) script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_system_info.py')
return gpu_names results = self.run_python_script(script_path=script_path)
output = results.stdout.decode()
match = re.search(r"GPU DATA:(\[[\s\S]*\])", output)
if match:
gpu_data_json = match.group(1)
gpus_info = json.loads(gpu_data_json)
return gpus_info
else:
logger.error("GPU data not found in the output.")
def supported_render_engines(self): def supported_render_engines(self):
engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT, engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT,
@@ -163,5 +174,5 @@ class Blender(BaseRenderEngine):
if __name__ == "__main__": if __name__ == "__main__":
x = Blender.get_detected_gpus() x = Blender().get_render_devices()
print(x) print(x)

View File

@@ -4,5 +4,6 @@ class BlenderUI:
def get_options(instance): def get_options(instance):
options = [ options = [
{'name': 'engine', 'options': instance.supported_render_engines()}, {'name': 'engine', 'options': instance.supported_render_engines()},
{'name': 'render_device', 'options': ['Any', 'GPU', 'CPU']},
] ]
return options return options

View File

@@ -14,11 +14,6 @@ class BlenderRenderWorker(BaseRenderWorker):
def __init__(self, input_path, output_path, engine_path, args=None, parent=None, name=None): def __init__(self, input_path, output_path, engine_path, args=None, parent=None, name=None):
super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path, engine_path=engine_path, args=args, parent=parent, name=name) super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path, engine_path=engine_path, args=args, parent=parent, name=name)
# Args
self.blender_engine = self.args.get('engine', 'BLENDER_EEVEE').upper()
self.export_format = self.args.get('export_format', None) or 'JPEG'
self.camera = self.args.get('camera', None)
# Stats # Stats
self.__frame_percent_complete = 0.0 self.__frame_percent_complete = 0.0
@@ -36,16 +31,39 @@ class BlenderRenderWorker(BaseRenderWorker):
cmd.append('-b') cmd.append('-b')
cmd.append(self.input_path) cmd.append(self.input_path)
# Python expressions # Start Python expressions - # todo: investigate splitting into separate 'setup' script
cmd.append('--python-expr') cmd.append('--python-expr')
python_exp = 'import bpy; bpy.context.scene.render.use_overwrite = False;' python_exp = 'import bpy; bpy.context.scene.render.use_overwrite = False;'
if self.camera:
python_exp = python_exp + f"bpy.context.scene.camera = bpy.data.objects['{self.camera}'];" # Setup Custom Camera
# insert any other python exp checks here custom_camera = self.args.get('camera', None)
if custom_camera:
python_exp = python_exp + f"bpy.context.scene.camera = bpy.data.objects['{custom_camera}'];"
# Set Render Device (gpu/cpu/any)
blender_engine = self.args.get('engine', 'BLENDER_EEVEE').upper()
if blender_engine == 'CYCLES':
render_device = self.args.get('render_device', 'any').lower()
if render_device not in {'any', 'gpu', 'cpu'}:
raise AttributeError(f"Invalid Cycles render device: {render_device}")
use_gpu = render_device in {'any', 'gpu'}
use_cpu = render_device in {'any', 'cpu'}
python_exp = python_exp + ("exec(\"for device in bpy.context.preferences.addons["
f"'cycles'].preferences.devices: device.use = {use_cpu} if device.type == 'CPU'"
f" else {use_gpu}\")")
# -- insert any other python exp checks / generators here --
# End Python expressions here
cmd.append(python_exp) cmd.append(python_exp)
# Export format
export_format = self.args.get('export_format', None) or 'JPEG'
path_without_ext = os.path.splitext(self.output_path)[0] + "_" path_without_ext = os.path.splitext(self.output_path)[0] + "_"
cmd.extend(['-E', self.blender_engine, '-o', path_without_ext, '-F', self.export_format]) cmd.extend(['-E', blender_engine, '-o', path_without_ext, '-F', export_format])
# set frame range # set frame range
cmd.extend(['-s', self.start_frame, '-e', self.end_frame, '-a']) cmd.extend(['-s', self.start_frame, '-e', self.end_frame, '-a'])

View File

@@ -0,0 +1,17 @@
import bpy
import json
# Ensure Cycles is available
bpy.context.preferences.addons['cycles'].preferences.get_devices()
# Collect the devices information
devices_info = []
for device in bpy.context.preferences.addons['cycles'].preferences.devices:
devices_info.append({
"name": device.name,
"type": device.type,
"use": device.use
})
# Print the devices information in JSON format
print("GPU DATA:" + json.dumps(devices_info))

View File

@@ -69,8 +69,10 @@ class BaseRenderEngine(object):
def get_output_formats(cls): def get_output_formats(cls):
raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}") raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}")
@classmethod def get_arguments(self):
def get_arguments(cls): pass
def system_info(self):
pass pass
def perform_presubmission_tasks(self, project_path): def perform_presubmission_tasks(self, project_path):

View File

@@ -243,7 +243,7 @@ class NewRenderJobForm(QWidget):
def update_renderer_info(self): def update_renderer_info(self):
# get the renderer info and add them all to the ui # get the renderer info and add them all to the ui
self.renderer_info = self.server_proxy.get_renderer_info() self.renderer_info = self.server_proxy.get_renderer_info(response_type='full')
self.renderer_type.addItems(self.renderer_info.keys()) self.renderer_type.addItems(self.renderer_info.keys())
# select the best renderer for the file type # select the best renderer for the file type
engine = EngineManager.engine_for_project_path(self.project_path) engine = EngineManager.engine_for_project_path(self.project_path)
@@ -353,7 +353,7 @@ class NewRenderJobForm(QWidget):
self.current_engine_options = engine().ui_options() self.current_engine_options = engine().ui_options()
for option in self.current_engine_options: for option in self.current_engine_options:
h_layout = QHBoxLayout() h_layout = QHBoxLayout()
label = QLabel(option['name'].capitalize() + ':') label = QLabel(option['name'].replace('_', ' ').capitalize() + ':')
h_layout.addWidget(label) h_layout.addWidget(label)
if option.get('options'): if option.get('options'):
combo_box = QComboBox() combo_box = QComboBox()