Skip to content

Commit cdedde5

Browse files
committed
Report render failures instead of raising out of the operator
A failed write let a RuntimeError escape execute(), so the user got a Python traceback in the Info editor and no indication of how far the batch had got. The traceback in the bug report was that: the operator had nothing to say about which file it could not write. Record each failure with its filename and reason, print the detail to the System Console, and summarise at the end. A single unwritable image no longer throws away a long batch, but if nothing has been written yet the batch stops there, since that is a setup problem and every remaining image would fail the same way after burning the render time. Creating the output folder and each product folder are handled the same way.
1 parent 71bbb94 commit cdedde5

1 file changed

Lines changed: 63 additions & 13 deletions

File tree

‎multiview_product_renderer/__init__.py‎

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,10 +1595,29 @@ def _do_clay_render(scene, filepath, clay_color):
15951595
scene.display.shading.show_cavity = saved['show_cavity']
15961596

15971597

1598+
class _BatchAborted(Exception):
1599+
"""Stop the batch early without dumping a traceback on the user."""
1600+
1601+
15981602
class MV_OT_RenderAll(Operator):
15991603
bl_idname = "mv.render_all"
16001604
bl_label = "Render All"
16011605

1606+
def _fail(self, failures, done, path, exc):
1607+
"""Record one failed write, aborting the batch if nothing has landed.
1608+
1609+
Collected and reported at the end rather than raised, so a single
1610+
unwritable file cannot throw away a long batch that is otherwise fine.
1611+
Before the first success there is nothing to salvage and the cause is
1612+
almost always the setup, so that case stops immediately instead of
1613+
failing the same way for every remaining image.
1614+
"""
1615+
note = f"{os.path.basename(path)}: {exc}"
1616+
failures.append(note)
1617+
print(f"[MultiView] FAILED {note}")
1618+
if not done:
1619+
raise _BatchAborted from exc
1620+
16021621
def execute(self, context):
16031622
scene = context.scene
16041623
s = scene.mv_settings
@@ -1615,7 +1634,11 @@ def execute(self, context):
16151634
return {'CANCELLED'}
16161635

16171636
out_root = bpy.path.abspath(s.render_output_dir)
1618-
os.makedirs(out_root, exist_ok=True)
1637+
try:
1638+
os.makedirs(out_root, exist_ok=True)
1639+
except OSError as exc:
1640+
self.report({'ERROR'}, f"Cannot use output folder '{out_root}': {exc}")
1641+
return {'CANCELLED'}
16191642

16201643
saved_hide = {}
16211644
for pc in products:
@@ -1630,6 +1653,7 @@ def execute(self, context):
16301653
total_per_pair = (2 if both else 1) * len(cameras)
16311654
total = len(products) * total_per_pair
16321655
done = 0
1656+
failures = []
16331657

16341658
try:
16351659
for i, pc in enumerate(products):
@@ -1640,27 +1664,41 @@ def execute(self, context):
16401664

16411665
label = product_label(s.start_prefix, s.start_number, i, s.products_per_letter)
16421666
product_dir = os.path.join(out_root, safe_name(f"{label}_{pc.name}"))
1643-
os.makedirs(product_dir, exist_ok=True)
1667+
try:
1668+
os.makedirs(product_dir, exist_ok=True)
1669+
except OSError as exc:
1670+
self._fail(failures, done, product_dir, exc)
1671+
continue
16441672

16451673
for cam in cameras:
16461674
scene.camera = cam
16471675
view_name = cam.name.replace("MV_Cam_", "")
16481676
base = safe_name(f"{label}_{pc.name}_{view_name}")
16491677

1678+
clay = (tuple(s.clay_color),)
16501679
if mode == 'FULL':
1651-
_do_full_render(scene, os.path.join(product_dir, base))
1652-
done += 1
1680+
attempts = ((_do_full_render, base, ()),)
16531681
elif mode == 'CLAY':
1654-
_do_clay_render(scene, os.path.join(product_dir, base + "_clay"),
1655-
tuple(s.clay_color))
1656-
done += 1
1682+
attempts = ((_do_clay_render, base + "_clay", clay),)
16571683
else:
1658-
_do_full_render(scene, os.path.join(product_dir, base + "_render"))
1684+
attempts = ((_do_full_render, base + "_render", ()),
1685+
(_do_clay_render, base + "_clay", clay))
1686+
1687+
for render, name, extra in attempts:
1688+
path = os.path.join(product_dir, name)
1689+
try:
1690+
render(scene, path, *extra)
1691+
except RuntimeError as exc:
1692+
# bpy.ops.render.render raises when the image cannot
1693+
# be written. Letting that escape put a Python
1694+
# traceback in front of the user instead of the name
1695+
# of the file that failed.
1696+
self._fail(failures, done, path, exc)
1697+
continue
16591698
done += 1
1660-
_do_clay_render(scene, os.path.join(product_dir, base + "_clay"),
1661-
tuple(s.clay_color))
1662-
done += 1
1663-
print(f"[MultiView] {done}/{total}: {scene.render.filepath}")
1699+
print(f"[MultiView] {done}/{total}: {scene.render.filepath}")
1700+
except _BatchAborted:
1701+
pass
16641702
finally:
16651703
for pc in products:
16661704
lc = find_layer_coll(context.view_layer.layer_collection, pc.name)
@@ -1669,7 +1707,19 @@ def execute(self, context):
16691707
scene.camera = saved_cam
16701708
scene.render.filepath = saved_path
16711709

1672-
self.report({'INFO'}, f"Wrote {done} image(s).")
1710+
if not failures:
1711+
self.report({'INFO'}, f"Wrote {done} image(s).")
1712+
return {'FINISHED'}
1713+
1714+
if not done:
1715+
# Nothing landed at all, so the batch stopped at the first failure
1716+
# rather than burning render time failing the same way every time.
1717+
self.report({'ERROR'}, f"Nothing rendered. {failures[0]}")
1718+
return {'CANCELLED'}
1719+
1720+
self.report({'WARNING'},
1721+
f"Wrote {done} of {total} image(s). {len(failures)} failed, "
1722+
f"first: {failures[0]}. Full list in the System Console.")
16731723
return {'FINISHED'}
16741724

16751725

0 commit comments

Comments
 (0)