Compare commits

..

36 Commits

Author SHA1 Message Date
6bce1ccf90 Bumped BAT requirement to 1.1 2019-03-25 17:48:28 +01:00
bbe524c099 Updated CHANGELOG 2019-03-25 17:44:56 +01:00
462da038ec Fixed Blender 2.79 incompatibility 2019-03-20 13:58:56 +01:00
8d7799655e Bumped BAT to 1.1.dev2 2019-03-20 13:58:47 +01:00
cb0393868e Flamenco: get JWT token from Flamenco Server when sending files to Shaman 2019-03-13 15:09:24 +01:00
5a61a7a6c4 Use exponential backoff in uncached_session 2019-03-13 15:08:56 +01:00
60d1fbff50 Blender changed use_quit_dialog into use_save_prompt 2019-03-13 10:07:23 +01:00
352fe239f2 Flamenco: Use DNA enum value for format setting
See https://developer.blender.org/D4502 and https://developer.blender.org/rF032423271d0417aed3b6053adb8b6db2774b0d36
for more info.
2019-03-12 15:27:27 +01:00
09c1bf67b4 Bumped BAT to 1.1-dev1 2019-03-06 13:41:49 +01:00
23235afe71 Updated CHANGELOG 2019-03-06 13:32:38 +01:00
ff9624d4f3 Blender Video Chunks: also allow .mp4 and .mov as container format 2019-03-06 13:31:30 +01:00
48c60f73d7 Bundle with BAT 1.1-dev0 for Shaman support
See https://gitlab.com/blender-institute/shaman for more info.
2019-03-01 14:37:44 +01:00
12eaaa5bae Set min job priority to 1
Previously the minimum was 0, but the server only accepts 1 and up.
2019-03-01 14:36:41 +01:00
f7396350db Add support for Shaman servers
See https://gitlab.com/blender-institute/shaman for more info
2019-02-28 12:53:29 +01:00
cc97288018 Create job first, then send files
This requires Flamenco Server 2.2 or newer.
2019-02-28 12:52:51 +01:00
26105add9c Updated BAT to 0.99 2019-02-26 16:48:39 +01:00
ea81cc5769 Flamenco: Name render jobs just 'thefile' instead of 'Render thefile.flamenco.blend'
This makes the job list on Flamenco Server cleaner.
2019-02-13 15:18:33 +01:00
25b6053836 Allow project selection, even when the current project is ''. 2019-02-13 14:29:36 +01:00
65a05403dc Bumped BAT to 0.9 2019-02-12 12:33:31 +01:00
770b0121fa Flamenco: Different label for 'frame chunk' depending on render job type
The frame chunk size has a slightly different meaning when rendering
progressively (Flamenco Server can choose to chunk more frames together
when rendering a low number of samples).
2019-02-06 09:32:24 +01:00
2b155eac45 Flamenco: show a warning when the frame dimensions are not divisible by 2
Any 'Create Video' Flamenco task that's part of the job will pad the video
with black pixels to make the dimensions even, and this warning notifies
the artist about this.
2019-02-04 11:39:14 +01:00
d36959e91b Flamenco: Fixed tiny layout bug 2019-02-04 11:37:04 +01:00
9028c38c68 Fixed "You are not logged in" message 2019-02-01 17:20:01 +01:00
f04f07eaf1 Bumped version to 1.12.0 2019-01-31 14:43:08 +01:00
6c38a432bc Flamenco: Added a hidden "Submit & Quit" button.
This button can be enabled in the add-on preferences and and then be
available on the Flamenco Render panel. Pressing the button will
silently close Blender after the job has been submitted to Flamenco (for
example to click, walk away, and free up memory for when the same
machine is part of the render farm).
2019-01-31 14:42:50 +01:00
53fa3e628a Flamenco: disable Cycles denoiser when progressive rendering
The denoiser data cannot be (easily) merged, so for now we just disable
the denoiser.
2019-01-30 16:10:09 +01:00
924fb45cb2 Flamenco: disallow progressive rendering unless Cycles is used 2019-01-30 16:06:39 +01:00
b5619757bc Flamenco: disallow progressive rendering on Blender < 2.80
Rendering ranges of sample chunks only works reliably for us after
Blender commit 7744203b7fde35a074faf232dda3595b78c5f14c (Tue Jan 29
18:08:12 2019 +0100).
2019-01-30 16:06:39 +01:00
ae41745743 Flamenco: easy button for setting max sample count for progressive rendering 2019-01-30 16:06:39 +01:00
ffab83f921 Flamenco: no longer use the word 'chunks' in the UI
It's a confusing word; 'Frames per Task' is clearer.
2019-01-30 16:06:39 +01:00
8bef2f48a5 Flamenco: Move job-type-specific options to a box below job type selector
This should make the relation between the job type and its options clearer.
2019-01-30 16:04:43 +01:00
74b46ff0db Flamenco: Progressive Rendering max sample count instead of chunk count
Flamenco Server changed from expecting a fixed number of sample chunks to
a compile-time determined number of nonuniform chunks. The artist can now
influence the size of each render task by setting a maximum number of
samples per render task.
2019-01-30 16:04:43 +01:00
e1934b20d9 Flamenco: nicer error reporting when creating a job fails 2019-01-30 13:05:09 +01:00
0caf761863 Prevent error when running Blender in background mode
We shouldn't call any `gpu` functions in background mode. Since the texture
browser will never run when Blender is in background mode anyway, we can
simply assign `None` instead.
2019-01-04 16:25:50 +01:00
bc864737ae Bumped version to 1.11.1 2019-01-04 13:42:12 +01:00
f454a99c4b Bundled missing Texture Browser icons in setup.py 2019-01-04 13:42:04 +01:00
11 changed files with 405 additions and 94 deletions

View File

@@ -1,5 +1,28 @@
# Blender Cloud changelog # Blender Cloud changelog
## Version 1.12 (2019-03-25)
- Flamenco: Change how progressive render tasks are created. Instead of the artist setting a fixed
number of sample chunks, they can now set a maximum number of samples for each render task.
Initial render tasks are created with a low number of samples, and subsequent tasks have an
increasing number of samples, up to the set maximum. The total number of samples of the final
render is still equal to the number of samples configured in the blend file.
Requires Flamenco Server 2.2 or newer.
- Flamenco: Added a hidden "Submit & Quit" button. This button can be enabled in the add-on
preferences and and then be available on the Flamenco Render panel. Pressing the button will
silently close Blender after the job has been submitted to Flamenco (for example to click,
walk away, and free up memory for when the same machine is part of the render farm).
- Flamenco: Name render jobs just 'thefile' instead of 'Render thefile.flamenco.blend'.
This makes the job overview on Flamenco Server cleaner.
- Flamenco: support Shaman servers. See https://www.flamenco.io/docs/user_manual/shaman/
for more info.
- Flamenco: The 'blender-video-chunks' job type now also allows MP4 and MOV video containers.
## Version 1.11.1 (2019-01-04)
- Bundled missing Texture Browser icons.
## Version 1.11.0 (2019-01-04) ## Version 1.11.0 (2019-01-04)

View File

@@ -21,7 +21,7 @@
bl_info = { bl_info = {
'name': 'Blender Cloud', 'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis", "author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 11, 0), 'version': (1, 12, 0),
'blender': (2, 80, 0), 'blender': (2, 80, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser', 'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon ' 'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '

View File

@@ -267,6 +267,13 @@ class BlenderCloudPreferences(AddonPreferences):
description='When enabled, Blender will open a webbrowser', description='When enabled, Blender will open a webbrowser',
default=True, default=True,
) )
flamenco_show_quit_after_submit_button = BoolProperty(
name='Show "Submit & Quit" button',
description='When enabled, next to the "Render on Flamenco" button there will be a button '
'"Submit & Quit" that silently quits Blender after submitting the render job '
'to Flamenco',
default=False,
)
def draw(self, context): def draw(self, context):
import textwrap import textwrap
@@ -403,7 +410,7 @@ class BlenderCloudPreferences(AddonPreferences):
projects = bcp.available_projects projects = bcp.available_projects
project = bcp.project project = bcp.project
if bcp.status in {'NONE', 'IDLE'}: if bcp.status in {'NONE', 'IDLE'}:
if not projects or not project: if not projects:
row_buttons.operator('pillar.projects', row_buttons.operator('pillar.projects',
text='Find project to load', text='Find project to load',
icon='FILE_REFRESH') icon='FILE_REFRESH')
@@ -489,6 +496,7 @@ class BlenderCloudPreferences(AddonPreferences):
flamenco_box.prop(self, 'flamenco_relative_only') flamenco_box.prop(self, 'flamenco_relative_only')
flamenco_box.prop(self, 'flamenco_open_browser_after_submit') flamenco_box.prop(self, 'flamenco_open_browser_after_submit')
flamenco_box.prop(self, 'flamenco_show_quit_after_submit_button')
class PillarCredentialsUpdate(pillar.PillarOperatorMixin, class PillarCredentialsUpdate(pillar.PillarOperatorMixin,

View File

@@ -69,6 +69,21 @@ VIDEO_CONTAINER_TO_EXTENSION = {
'FLASH': '.flv', 'FLASH': '.flv',
} }
SHAMAN_URL_SCHEMES = {'shaman://', 'shaman+http://', 'shaman+https://'}
def scene_sample_count(scene) -> int:
"""Determine nr of render samples for this scene."""
if scene.cycles.progressive == 'BRANCHED_PATH':
samples = scene.cycles.aa_samples
else:
samples = scene.cycles.samples
if scene.cycles.use_square_samples:
samples **= 2
return samples
@pyside_cache('manager') @pyside_cache('manager')
def available_managers(self, context): def available_managers(self, context):
@@ -105,6 +120,24 @@ def manager_updated(self: 'FlamencoManagerGroup', context):
pppm) pppm)
def silently_quit_blender():
"""Quit Blender without any confirmation popup."""
try:
prefs = bpy.context.preferences
except AttributeError:
# Backward compatibility with Blender < 2.80
prefs = bpy.context.user_preferences
try:
prefs.view.use_save_prompt = False
except AttributeError:
# Backward compatibility with Blender < 2.80
prefs.view.use_quit_dialog = False
bpy.ops.wm.quit_blender()
class FlamencoManagerGroup(PropertyGroup): class FlamencoManagerGroup(PropertyGroup):
manager = EnumProperty( manager = EnumProperty(
items=available_managers, items=available_managers,
@@ -209,6 +242,8 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
stop_upon_exception = True stop_upon_exception = True
log = logging.getLogger('%s.FLAMENCO_OT_render' % __name__) log = logging.getLogger('%s.FLAMENCO_OT_render' % __name__)
quit_after_submit = BoolProperty()
async def async_execute(self, context): async def async_execute(self, context):
# Refuse to start if the file hasn't been saved. It's okay if # Refuse to start if the file hasn't been saved. It's okay if
# it's dirty, but we do need a filename and a location. # it's dirty, but we do need a filename and a location.
@@ -268,17 +303,10 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
# Add extra settings specific to the job type # Add extra settings specific to the job type
if scene.flamenco_render_job_type == 'blender-render-progressive': if scene.flamenco_render_job_type == 'blender-render-progressive':
if scene.cycles.progressive == 'BRANCHED_PATH': samples = scene_sample_count(scene)
samples = scene.cycles.aa_samples settings['cycles_sample_cap'] = scene.flamenco_render_chunk_sample_cap
else:
samples = scene.cycles.samples
if scene.cycles.use_square_samples:
samples **= 2
settings['cycles_num_chunks'] = scene.flamenco_render_schunk_count
settings['cycles_sample_count'] = samples settings['cycles_sample_count'] = samples
settings['format'] = 'EXR' settings['format'] = 'OPEN_EXR'
# Let Flamenco Server know whether we'll output images or video. # Let Flamenco Server know whether we'll output images or video.
output_format = settings.get('format') or scene.render.image_settings.file_format output_format = settings.get('format') or scene.render.image_settings.file_format
@@ -302,31 +330,88 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
self.quit() self.quit()
return return
# BAT-pack the files to the destination directory.
outdir, outfile, missing_sources = await self.bat_pack(filepath)
if not outfile:
return
settings['filepath'] = manager.replace_path(outfile)
# Create the job at Flamenco Server. # Create the job at Flamenco Server.
context.window_manager.flamenco_status = 'COMMUNICATING' context.window_manager.flamenco_status = 'COMMUNICATING'
project_id = prefs.project.project project_id = prefs.project.project
job_name = self._make_job_name(filepath)
try: try:
job_info = await create_job(self.user_id, job_info = await create_job(self.user_id,
project_id, project_id,
manager_id, manager_id,
scene.flamenco_render_job_type, scene.flamenco_render_job_type,
settings, settings,
'Render %s' % filepath.name, job_name,
priority=scene.flamenco_render_job_priority, priority=scene.flamenco_render_job_priority,
start_paused=scene.flamenco_start_paused) start_paused=scene.flamenco_start_paused)
except Exception as ex: except Exception as ex:
self.report({'ERROR'}, 'Error creating Flamenco job: %s' % ex) message = str(ex)
if isinstance(ex, pillarsdk.exceptions.BadRequest):
payload = ex.response.json()
try:
message = payload['_error']['message']
except KeyError:
pass
self.log.exception('Error creating Flamenco job')
self.report({'ERROR'}, 'Error creating Flamenco job: %s' % message)
self.quit() self.quit()
return return
# Store the job ID in a file in the output dir. # BAT-pack the files to the destination directory.
job_id = job_info['_id']
outdir, outfile, missing_sources = await self.bat_pack(job_id, filepath)
if not outfile:
return
# Store the job ID in a file in the output dir, if we can.
# TODO: Make it possible to create this file first and then send it to BAT for packing.
if outdir is not None:
await self._create_jobinfo_json(
outdir, job_info, manager_id, project_id, missing_sources)
# Now that the files have been transfered, PATCH the job at the Manager
# to kick off the job compilation.
job_filepath = manager.replace_path(outfile)
self.log.info('Final file path: %s', job_filepath)
new_settings = {'filepath': job_filepath}
await self.compile_job(job_id, new_settings)
# We can now remove the local copy we made with bpy.ops.wm.save_as_mainfile().
# Strictly speaking we can already remove it after the BAT-pack, but it may come in
# handy in case of failures.
try:
self.log.info('Removing temporary file %s', filepath)
filepath.unlink()
except Exception as ex:
self.report({'ERROR'}, 'Unable to remove file: %s' % ex)
self.quit()
return
if prefs.flamenco_open_browser_after_submit:
import webbrowser
from urllib.parse import urljoin
from ..blender import PILLAR_WEB_SERVER_URL
url = urljoin(PILLAR_WEB_SERVER_URL, '/flamenco/jobs/%s/redir' % job_id)
webbrowser.open_new_tab(url)
# Do a final report.
if missing_sources:
names = (ms.name for ms in missing_sources)
self.report({'WARNING'}, 'Flamenco job created with missing files: %s' %
'; '.join(names))
else:
self.report({'INFO'}, 'Flamenco job created.')
if self.quit_after_submit:
silently_quit_blender()
self.quit()
async def _create_jobinfo_json(self, outdir: Path, job_info: dict,
manager_id: str, project_id: str,
missing_sources: typing.List[Path]):
from ..blender import preferences
prefs = preferences()
with open(str(outdir / 'jobinfo.json'), 'w', encoding='utf8') as outfile: with open(str(outdir / 'jobinfo.json'), 'w', encoding='utf8') as outfile:
import json import json
@@ -354,34 +439,16 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
} }
json.dump(info, outfile, sort_keys=True, indent=4, cls=utils.JSONEncoder) json.dump(info, outfile, sort_keys=True, indent=4, cls=utils.JSONEncoder)
# We can now remove the local copy we made with bpy.ops.wm.save_as_mainfile(). def _make_job_name(self, filepath: Path) -> str:
# Strictly speaking we can already remove it after the BAT-pack, but it may come in """Turn a file to render into the render job name."""
# handy in case of failures.
try:
self.log.info('Removing temporary file %s', filepath)
filepath.unlink()
except Exception as ex:
self.report({'ERROR'}, 'Unable to remove file: %s' % ex)
self.quit()
return
if prefs.flamenco_open_browser_after_submit: job_name = filepath.name
import webbrowser if job_name.endswith('.blend'):
from urllib.parse import urljoin job_name = job_name[:-6]
from ..blender import PILLAR_WEB_SERVER_URL if job_name.endswith('.flamenco'):
job_name = job_name[:-9]
url = urljoin(PILLAR_WEB_SERVER_URL, '/flamenco/jobs/%s/redir' % job_info['_id']) return job_name
webbrowser.open_new_tab(url)
# Do a final report.
if missing_sources:
names = (ms.name for ms in missing_sources)
self.report({'WARNING'}, 'Flamenco job created with missing files: %s' %
'; '.join(names))
else:
self.report({'INFO'}, 'Flamenco job created.')
self.quit()
def validate_job_settings(self, context, settings: dict) -> bool: def validate_job_settings(self, context, settings: dict) -> bool:
"""Perform settings validations for the selected job type. """Perform settings validations for the selected job type.
@@ -397,8 +464,9 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
self.report({'ERROR'}, 'Job type requires chunks of at least 10 frames.') self.report({'ERROR'}, 'Job type requires chunks of at least 10 frames.')
return False return False
if settings['output_file_extension'] != '.mkv': if settings['output_file_extension'] not in {'.mkv', '.mp4', '.mov'}:
self.report({'ERROR'}, 'Job type requires rendering to Matroska files.') self.report({'ERROR'}, 'Job type requires rendering to Matroska or '
'MP4 files, not %r.' % settings['output_file_extension'])
return False return False
return True return True
@@ -422,6 +490,14 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
old_use_overwrite = render.use_overwrite old_use_overwrite = render.use_overwrite
old_use_placeholder = render.use_placeholder old_use_placeholder = render.use_placeholder
disable_denoiser = (context.scene.flamenco_render_job_type == 'blender-render-progressive'
and render.engine == 'CYCLES')
if disable_denoiser:
use_denoising = [layer.cycles.use_denoising
for layer in context.scene.view_layers]
else:
use_denoising = []
try: try:
# The file extension should be determined by the render settings, not necessarily # The file extension should be determined by the render settings, not necessarily
@@ -432,6 +508,10 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
render.use_overwrite = False render.use_overwrite = False
render.use_placeholder = False render.use_placeholder = False
if disable_denoiser:
for layer in context.scene.view_layers:
layer.cycles.use_denoising = False
filepath = Path(context.blend_data.filepath).with_suffix('.flamenco.blend') filepath = Path(context.blend_data.filepath).with_suffix('.flamenco.blend')
self.log.info('Saving copy to temporary file %s', filepath) self.log.info('Saving copy to temporary file %s', filepath)
bpy.ops.wm.save_as_mainfile(filepath=str(filepath), bpy.ops.wm.save_as_mainfile(filepath=str(filepath),
@@ -443,17 +523,26 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
render.use_overwrite = old_use_overwrite render.use_overwrite = old_use_overwrite
render.use_placeholder = old_use_placeholder render.use_placeholder = old_use_placeholder
if disable_denoiser:
for denoise, layer in zip(use_denoising, context.scene.view_layers):
layer.cycles.use_denoising = denoise
return filepath return filepath
async def bat_pack(self, filepath: Path) \ async def bat_pack(self, job_id: str, filepath: Path) \
-> typing.Tuple[Path, typing.Optional[Path], typing.List[Path]]: -> typing.Tuple[typing.Optional[Path], typing.Optional[PurePath], typing.List[Path]]:
"""BAT-packs the blendfile to the destination directory. """BAT-packs the blendfile to the destination directory.
Returns the path of the destination blend file. Returns the path of the destination blend file.
:param job_id: the job ID given to us by Flamenco Server.
:param filepath: the blend file to pack (i.e. the current blend file) :param filepath: the blend file to pack (i.e. the current blend file)
:returns: the destination directory, the destination blend file or None :returns: A tuple of:
if there were errors BAT-packing, and a list of missing paths. - The destination directory, or None if it does not exist on a
locally-reachable filesystem (for example when sending files to
a Shaman server).
- The destination blend file, or None if there were errors BAT-packing,
- A list of missing paths.
""" """
from datetime import datetime from datetime import datetime
@@ -461,19 +550,49 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
prefs = preferences() prefs = preferences()
proj_abspath = bpy.path.abspath(prefs.cloud_project_local_path)
projdir = Path(proj_abspath).resolve()
exclusion_filter = (prefs.flamenco_exclude_filter or '').strip()
relative_only = prefs.flamenco_relative_only
self.log.debug('projdir: %s', projdir)
if any(prefs.flamenco_job_file_path.startswith(scheme) for scheme in SHAMAN_URL_SCHEMES):
endpoint, _ = bat_interface.parse_shaman_endpoint(prefs.flamenco_job_file_path)
self.log.info('Sending BAT pack to Shaman at %s', endpoint)
try:
outfile, missing_sources = await bat_interface.copy(
bpy.context, filepath, projdir, '/', exclusion_filter,
packer_class=bat_interface.ShamanPacker,
relative_only=relative_only,
endpoint=endpoint,
checkout_id=job_id,
manager_id=prefs.flamenco_manager.manager,
)
except bat_interface.FileTransferError as ex:
self.log.error('Could not transfer %d files, starting with %s',
len(ex.files_remaining), ex.files_remaining[0])
self.report({'ERROR'}, 'Unable to transfer %d files' % len(ex.files_remaining))
self.quit()
return None, None, []
except bat_interface.Aborted:
self.log.warning('BAT Pack was aborted')
self.report({'WARNING'}, 'Aborted Flamenco file packing/transferring')
self.quit()
return None, None, []
bpy.context.window_manager.flamenco_status = 'DONE'
outfile = PurePath('{shaman}') / outfile
return None, outfile, missing_sources
# Create a unique directory that is still more or less identifyable. # Create a unique directory that is still more or less identifyable.
# This should work better than a random ID. # This should work better than a random ID.
unique_dir = '%s-%s-%s' % (datetime.now().isoformat('-').replace(':', ''), unique_dir = '%s-%s-%s' % (datetime.now().isoformat('-').replace(':', ''),
self.db_user['username'], self.db_user['username'],
filepath.stem) filepath.stem)
outdir = Path(prefs.flamenco_job_file_path) / unique_dir outdir = Path(prefs.flamenco_job_file_path) / unique_dir
proj_abspath = bpy.path.abspath(prefs.cloud_project_local_path)
projdir = Path(proj_abspath).resolve()
exclusion_filter = (prefs.flamenco_exclude_filter or '').strip()
relative_only = prefs.flamenco_relative_only
self.log.debug('outdir : %s', outdir) self.log.debug('outdir : %s', outdir)
self.log.debug('projdir: %s', projdir)
try: try:
outdir.mkdir(parents=True) outdir.mkdir(parents=True)
@@ -502,6 +621,20 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
bpy.context.window_manager.flamenco_status = 'DONE' bpy.context.window_manager.flamenco_status = 'DONE'
return outdir, outfile, missing_sources return outdir, outfile, missing_sources
async def compile_job(self, job_id: str, new_settings: dict) -> None:
"""Request Flamenco Server to start compiling the job."""
payload = {
'op': 'construct',
'settings': new_settings,
}
from .sdk import Job
from ..pillar import pillar_call
job = Job({'_id': job_id})
await pillar_call(job.patch, payload, caching=False)
def scene_frame_range(context) -> str: def scene_frame_range(context) -> str:
"""Returns the frame range string for the current scene.""" """Returns the frame range string for the current scene."""
@@ -655,6 +788,18 @@ class FLAMENCO_OT_disable_output_path_override(Operator):
return {'FINISHED'} return {'FINISHED'}
class FLAMENCO_OT_set_recommended_sample_cap(Operator):
bl_idname = 'flamenco.set_recommended_sample_cap'
bl_label = 'Set Recommended Maximum Sample Count'
bl_description = 'Set the recommended maximum samples per render task'
sample_cap = IntProperty()
def execute(self, context):
context.scene.flamenco_render_chunk_sample_cap = self.sample_cap
return {'FINISHED'}
async def create_job(user_id: str, async def create_job(user_id: str,
project_id: str, project_id: str,
manager_id: str, manager_id: str,
@@ -672,7 +817,7 @@ async def create_job(user_id: str,
from ..pillar import pillar_call from ..pillar import pillar_call
job_attrs = { job_attrs = {
'status': 'queued', 'status': 'waiting-for-files',
'priority': priority, 'priority': priority,
'name': job_name, 'name': job_name,
'settings': job_settings, 'settings': job_settings,
@@ -831,6 +976,41 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
labeled_row.label(text='Job Type:') labeled_row.label(text='Job Type:')
labeled_row.prop(context.scene, 'flamenco_render_job_type', text='') labeled_row.prop(context.scene, 'flamenco_render_job_type', text='')
# Job-type-specific options go directly below the job type selector.
box = layout.box()
if getattr(context.scene, 'flamenco_render_job_type', None) == 'blender-render-progressive':
if bpy.app.version < (2, 80):
box.alert = True
box.label(text='Progressive rendering requires Blender 2.80 or newer.',
icon='ERROR')
# This isn't entirely fair, as Blender 2.79 could hypothetically
# be used to submit a job to farm running Blender 2.80.
return
if context.scene.render.engine != 'CYCLES':
box.alert = True
box.label(text='Progressive rendering requires Cycles', icon='ERROR')
return
box.prop(context.scene, 'flamenco_render_chunk_sample_cap')
sample_count = scene_sample_count(context.scene)
recommended_cap = sample_count // 4
split = box.split(**blender.factor(0.4))
split.label(text='Total Sample Count: %d' % sample_count)
props = split.operator('flamenco.set_recommended_sample_cap',
text='Recommended Max Samples per Task: %d' % recommended_cap)
props.sample_cap = recommended_cap
if any(layer.cycles.use_denoising for layer in context.scene.view_layers):
box.label(text='Progressive Rendering will disable Denoising.', icon='ERROR')
box.prop(context.scene, 'flamenco_render_fchunk_size',
text='Minimum Frames per Task')
else:
box.prop(context.scene, 'flamenco_render_fchunk_size')
labeled_row = layout.split(**blender.factor(0.25), align=True) labeled_row = layout.split(**blender.factor(0.25), align=True)
labeled_row.label(text='Frame Range:') labeled_row.label(text='Frame Range:')
prop_btn_row = labeled_row.row(align=True) prop_btn_row = labeled_row.row(align=True)
@@ -838,12 +1018,8 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
prop_btn_row.operator('flamenco.scene_to_frame_range', text='', icon='ARROW_LEFTRIGHT') prop_btn_row.operator('flamenco.scene_to_frame_range', text='', icon='ARROW_LEFTRIGHT')
layout.prop(context.scene, 'flamenco_render_job_priority') layout.prop(context.scene, 'flamenco_render_job_priority')
layout.prop(context.scene, 'flamenco_render_fchunk_size')
layout.prop(context.scene, 'flamenco_start_paused') layout.prop(context.scene, 'flamenco_start_paused')
if getattr(context.scene, 'flamenco_render_job_type', None) == 'blender-render-progressive':
layout.prop(context.scene, 'flamenco_render_schunk_count')
paths_layout = layout.column(align=True) paths_layout = layout.column(align=True)
labeled_row = paths_layout.split(**blender.factor(0.25), align=True) labeled_row = paths_layout.split(**blender.factor(0.25), align=True)
@@ -882,12 +1058,22 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
labeled_row.label(text='Effective Output Path:') labeled_row.label(text='Effective Output Path:')
labeled_row.label(text=str(render_output)) labeled_row.label(text=str(render_output))
self.draw_odd_size_warning(layout, context.scene.render)
# Show current status of Flamenco. # Show current status of Flamenco.
flamenco_status = context.window_manager.flamenco_status flamenco_status = context.window_manager.flamenco_status
if flamenco_status in {'IDLE', 'ABORTED', 'DONE'}: if flamenco_status in {'IDLE', 'ABORTED', 'DONE'}:
layout.operator(FLAMENCO_OT_render.bl_idname, if prefs.flamenco_show_quit_after_submit_button:
text='Render on Flamenco', ui = layout.split(**blender.factor(0.75), align=True)
icon='RENDER_ANIMATION') else:
ui = layout
ui.operator(FLAMENCO_OT_render.bl_idname,
text='Render on Flamenco',
icon='RENDER_ANIMATION').quit_after_submit = False
if prefs.flamenco_show_quit_after_submit_button:
ui.operator(FLAMENCO_OT_render.bl_idname,
text='Submit & Quit',
icon='RENDER_ANIMATION').quit_after_submit = True
if bpy.app.debug: if bpy.app.debug:
layout.operator(FLAMENCO_OT_copy_files.bl_idname) layout.operator(FLAMENCO_OT_copy_files.bl_idname)
elif flamenco_status == 'INVESTIGATING': elif flamenco_status == 'INVESTIGATING':
@@ -908,6 +1094,29 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
elif flamenco_status != 'IDLE' and context.window_manager.flamenco_status_txt: elif flamenco_status != 'IDLE' and context.window_manager.flamenco_status_txt:
layout.label(text=context.window_manager.flamenco_status_txt) layout.label(text=context.window_manager.flamenco_status_txt)
def draw_odd_size_warning(self, layout, render):
render_width = render.resolution_x * render.resolution_percentage // 100
render_height = render.resolution_y * render.resolution_percentage // 100
odd_width = render_width % 2
odd_height = render_height % 2
if not odd_width and not odd_height:
return
box = layout.box()
box.alert = True
if odd_width and odd_height:
msg = 'Both X (%d) and Y (%d) resolution are' % (render_width, render_height)
elif odd_width:
msg = 'X resolution (%d) is' % render_width
else:
msg = 'Y resolution (%d) is' % render_height
box.label(text=msg + ' not divisible by 2.', icon='ERROR')
box.label(text='Any video rendered from these frames will be padded with black pixels.')
def activate(): def activate():
"""Activates draw callbacks, menu items etc. for Flamenco.""" """Activates draw callbacks, menu items etc. for Flamenco."""
@@ -961,21 +1170,25 @@ def register():
scene = bpy.types.Scene scene = bpy.types.Scene
scene.flamenco_render_fchunk_size = IntProperty( scene.flamenco_render_fchunk_size = IntProperty(
name='Frame Chunk Size', name='Frames per Task',
description='Maximum number of frames to render per task', description='Number of frames to render per task. For progressive renders this is used '
'when the sample limit is reached -- before that more frames are used',
min=1, min=1,
default=1, default=1,
) )
scene.flamenco_render_schunk_count = IntProperty( scene.flamenco_render_chunk_sample_cap = IntProperty(
name='Number of Sample Chunks', name='Maximum Samples per Task',
description='Number of Cycles samples chunks to use per frame', description='Maximum number of samples per render task; a lower number creates more '
min=2, 'shorter-running tasks. Values between 1/10 and 1/4 of the total sample count '
default=3, 'seem sensible',
soft_max=10, min=1,
soft_min=5,
default=100,
soft_max=1000,
) )
scene.flamenco_render_frame_range = StringProperty( scene.flamenco_render_frame_range = StringProperty(
name='Frame Range', name='Frame Range',
description='Frames to render, in "printer range" notation' description='Frames to render, in "printer range" notation',
) )
scene.flamenco_render_job_type = EnumProperty( scene.flamenco_render_job_type = EnumProperty(
name='Job Type', name='Job Type',
@@ -997,7 +1210,7 @@ def register():
scene.flamenco_render_job_priority = IntProperty( scene.flamenco_render_job_priority = IntProperty(
name='Job Priority', name='Job Priority',
min=0, min=1,
default=50, default=50,
max=100, max=100,
description='Higher numbers mean higher priority' description='Higher numbers mean higher priority'
@@ -1057,7 +1270,7 @@ def unregister():
log.warning('Unable to unregister class %r, probably already unregistered', cls) log.warning('Unable to unregister class %r, probably already unregistered', cls)
for name in ('flamenco_render_fchunk_size', for name in ('flamenco_render_fchunk_size',
'flamenco_render_schunk_count', 'flamenco_render_chunk_sample_cap',
'flamenco_render_frame_range', 'flamenco_render_frame_range',
'flamenco_render_job_type', 'flamenco_render_job_type',
'flamenco_start_paused', 'flamenco_start_paused',

View File

@@ -6,10 +6,11 @@ import pathlib
import re import re
import threading import threading
import typing import typing
import urllib.parse
import bpy import bpy
from blender_asset_tracer import pack from blender_asset_tracer import pack
from blender_asset_tracer.pack import progress, transfer from blender_asset_tracer.pack import progress, transfer, shaman
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -19,6 +20,7 @@ _packer_lock = threading.RLock()
# For using in other parts of the add-on, so only this file imports BAT. # For using in other parts of the add-on, so only this file imports BAT.
Aborted = pack.Aborted Aborted = pack.Aborted
FileTransferError = transfer.FileTransferError FileTransferError = transfer.FileTransferError
parse_shaman_endpoint = shaman.parse_endpoint
class BatProgress(progress.Callback): class BatProgress(progress.Callback):
@@ -63,8 +65,8 @@ class BatProgress(progress.Callback):
else: else:
self._txt('Pack of %s done' % output_blendfile.name) self._txt('Pack of %s done' % output_blendfile.name)
def pack_aborted(self): def pack_aborted(self, reason: str):
self._txt('Aborted') self._txt('Aborted: %s' % reason)
self._status('ABORTED') self._status('ABORTED')
def trace_blendfile(self, filename: pathlib.Path) -> None: def trace_blendfile(self, filename: pathlib.Path) -> None:
@@ -93,13 +95,46 @@ class BatProgress(progress.Callback):
pass pass
class ShamanPacker(shaman.ShamanPacker):
"""Packer with support for getting an auth token from Flamenco Server."""
def __init__(self,
bfile: pathlib.Path,
project: pathlib.Path,
target: str,
endpoint: str,
checkout_id: str,
*,
manager_id: str,
**kwargs) -> None:
self.manager_id = manager_id
super().__init__(bfile, project, target, endpoint, checkout_id, **kwargs)
def _get_auth_token(self) -> str:
"""get a token from Flamenco Server"""
from ..blender import PILLAR_SERVER_URL
from ..pillar import blender_id_subclient, uncached_session, SUBCLIENT_ID
url = urllib.parse.urljoin(PILLAR_SERVER_URL,
'flamenco/jwt/generate-token/%s' % self.manager_id)
auth_token = blender_id_subclient()['token']
resp = uncached_session.get(url, auth=(auth_token, SUBCLIENT_ID))
resp.raise_for_status()
return resp.text
async def copy(context, async def copy(context,
base_blendfile: pathlib.Path, base_blendfile: pathlib.Path,
project: pathlib.Path, project: pathlib.Path,
target: pathlib.Path, target: str,
exclusion_filter: str, exclusion_filter: str,
*, *,
relative_only: bool) -> typing.Tuple[pathlib.Path, typing.Set[pathlib.Path]]: relative_only: bool,
packer_class=pack.Packer,
**packer_args) \
-> typing.Tuple[pathlib.Path, typing.Set[pathlib.Path]]:
"""Use BAT🦇 to copy the given file and dependencies to the target location. """Use BAT🦇 to copy the given file and dependencies to the target location.
:raises: FileTransferError if a file couldn't be transferred. :raises: FileTransferError if a file couldn't be transferred.
@@ -108,11 +143,11 @@ async def copy(context,
global _running_packer global _running_packer
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
wm = bpy.context.window_manager wm = bpy.context.window_manager
with pack.Packer(base_blendfile, project, target, compress=True, relative_only=relative_only) \ packer = packer_class(base_blendfile, project, target,
as packer: compress=True, relative_only=relative_only, **packer_args)
with packer:
with _packer_lock: with _packer_lock:
if exclusion_filter: if exclusion_filter:
# There was a mistake in an older version of the property tooltip, # There was a mistake in an older version of the property tooltip,

View File

@@ -32,6 +32,8 @@ class Manager(List, Find):
Tries to find platform-specific path prefixes, and replaces them with Tries to find platform-specific path prefixes, and replaces them with
variables. variables.
""" """
assert isinstance(some_path, pathlib.PurePath), \
'some_path should be a PurePath, not %r' % some_path
for varname, path in self._sorted_path_replacements(): for varname, path in self._sorted_path_replacements():
replacement = self.PurePlatformPath(path) replacement = self.PurePlatformPath(path)
@@ -52,3 +54,15 @@ class Job(List, Find, Create):
""" """
path = 'flamenco/jobs' path = 'flamenco/jobs'
ensure_query_projections = {'project': 1} ensure_query_projections = {'project': 1}
def patch(self, payload: dict, api=None):
import pillarsdk.utils
import json
api = api or self.api
url = pillarsdk.utils.join_url(self.path, str(self['_id']))
headers = pillarsdk.utils.merge_dict(self.http_headers(),
{'Content-Type': 'application/json'})
response = api.patch(url, payload, headers=headers)
return response

View File

@@ -26,7 +26,8 @@ from contextlib import closing, contextmanager
import urllib.parse import urllib.parse
import pathlib import pathlib
import requests import requests.adapters
import requests.packages.urllib3.util.retry
import requests.structures import requests.structures
import pillarsdk import pillarsdk
import pillarsdk.exceptions import pillarsdk.exceptions
@@ -42,7 +43,16 @@ RFC1123_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
_pillar_api = {} # will become a mapping from bool (cached/non-cached) to pillarsdk.Api objects. _pillar_api = {} # will become a mapping from bool (cached/non-cached) to pillarsdk.Api objects.
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_retries = requests.packages.urllib3.util.retry.Retry(
total=10,
backoff_factor=0.05,
)
_http_adapter = requests.adapters.HTTPAdapter(max_retries=_retries)
uncached_session = requests.session() uncached_session = requests.session()
uncached_session.mount('https://', _http_adapter)
uncached_session.mount('http://', _http_adapter)
_testing_blender_id_profile = None # Just for testing, overrides what is returned by blender_id_profile. _testing_blender_id_profile = None # Just for testing, overrides what is returned by blender_id_profile.
_downloaded_urls = set() # URLs we've downloaded this Blender session. _downloaded_urls = set() # URLs we've downloaded this Blender session.

View File

@@ -512,7 +512,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
ex = self.async_task.exception() ex = self.async_task.exception()
if isinstance(ex, pillar.UserNotLoggedInError): if isinstance(ex, pillar.UserNotLoggedInError):
ex_msg = 'You are not logged in on Blender ID. Please log in at User Preferences, ' \ ex_msg = 'You are not logged in on Blender ID. Please log in at User Preferences, ' \
'System, Blender ID.' 'Add-ons, Blender ID Authentication.'
else: else:
ex_msg = str(ex) ex_msg = str(ex)
if not ex_msg: if not ex_msg:

View File

@@ -11,8 +11,12 @@ import bpy
import gpu import gpu
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') if bpy.app.background:
texture_shader = gpu.shader.from_builtin('2D_IMAGE') shader = None
texture_shader = None
else:
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
texture_shader = gpu.shader.from_builtin('2D_IMAGE')
Float2 = typing.Tuple[float, float] Float2 = typing.Tuple[float, float]
Float3 = typing.Tuple[float, float, float] Float3 = typing.Tuple[float, float, float]

View File

@@ -3,7 +3,7 @@
lockfile==0.12.2 lockfile==0.12.2
pillarsdk==1.7.0 pillarsdk==1.7.0
wheel==0.29.0 wheel==0.29.0
blender-asset-tracer>=0.8 blender-asset-tracer==1.1
# Secondary requirements: # Secondary requirements:
asn1crypto==0.24.0 asn1crypto==0.24.0

View File

@@ -236,12 +236,16 @@ setup(
'wheels': BuildWheels}, 'wheels': BuildWheels},
name='blender_cloud', name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.', description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.11.0', version='1.12.0',
author='Sybren A. Stüvel', author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu', author_email='sybren@stuvel.eu',
packages=find_packages('.'), packages=find_packages('.'),
data_files=[('blender_cloud', ['README.md', 'README-flamenco.md', 'CHANGELOG.md']), data_files=[
('blender_cloud/icons', glob.glob('blender_cloud/icons/*'))], ('blender_cloud', ['README.md', 'README-flamenco.md', 'CHANGELOG.md']),
('blender_cloud/icons', glob.glob('blender_cloud/icons/*')),
('blender_cloud/texture_browser/icons',
glob.glob('blender_cloud/texture_browser/icons/*'))
],
scripts=[], scripts=[],
url='https://developer.blender.org/diffusion/BCA/', url='https://developer.blender.org/diffusion/BCA/',
license='GNU General Public License v2 or later (GPLv2+)', license='GNU General Public License v2 or later (GPLv2+)',