Files
vfxreview/RenderWorker/VFXReviewWorker/BlenderProgressParser.cs
T
twotalesanimation 708e8a0d8f
Deploy / deploy (push) Successful in 3m28s
fix(worker): parse Blender 5.x output so rendered frames are counted
Blender 5 logs through CLOG, prefixing every line with elapsed time and a
category ("00:02.954  render | Saved: '...'"), and reports errors as
"ERROR ..." instead of "Error: ...". The parser anchored on "Saved:" at the
start of the line, so it counted no frames from a render that wrote all of
them and the job failed as "saved no frames". Progress (Fra:) and error
detection were dead on 5.x for the same reason.

The prefix is now stripped before matching; 4.x output parses as before.
Warnings no longer count as errors: add-ons print them on every launch and
one would otherwise be reported as the reason a render failed. A run that
saves nothing now reports Blender's own error when there is one.

Verified on the real DH2 test.blend under Blender 5.2: the 1.6.2 parser
counts 0 of 3 saved frames, this one counts 3.

Bump worker to 1.6.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 04:42:33 +02:00

89 lines
3.3 KiB
C#

using System.Text.RegularExpressions;
namespace VFXReviewWorker;
/// <summary>
/// Parses `blender -b` stdout. Blender reports the frame it is working on in
/// its status lines ("Fra:1042 Mem:512.00M ...") and confirms each written file
/// with "Saved: '...'". The saved lines are the reliable signal — a frame can
/// be reported for a long time while it renders — so progress counts those and
/// falls back to Fra: for a first sign of life.
///
/// Blender 5.x logs through CLOG, which prefixes every line with an elapsed
/// time and a category ("00:02.954 render | Saved: '...'") and
/// reports errors as "ERROR ..." rather than "Error: ...". The prefix is
/// stripped before matching, so both formats parse the same way.
/// </summary>
public static partial class BlenderProgressParser
{
[GeneratedRegex(@"^(?:\d+:)?\d{2}:\d{2}\.\d{3}\s+\S+\s*\| ?", RegexOptions.Compiled)]
private static partial Regex LogPrefix();
[GeneratedRegex(@"^Fra:\s*(\d+)", RegexOptions.Compiled)]
private static partial Regex FrameLine();
[GeneratedRegex(@"^Saved:\s*'(.+)'", RegexOptions.Compiled)]
private static partial Regex SavedLine();
// Errors only. Warnings are not failures, and add-ons print them on every
// launch ("Warning: add-on missing 'bl_info'"), so counting them made an
// add-on's complaint the reported reason for a failed render.
[GeneratedRegex(@"(?i:^Error: )|^(ERROR|FATAL) ", RegexOptions.Compiled)]
private static partial Regex ErrorLine();
/// <summary>The line without Blender 5.x's "elapsed category | " prefix.</summary>
private static string Message(string line)
{
var m = LogPrefix().Match(line);
return m.Success ? line[m.Length..] : line;
}
/// <summary>
/// Failures that will repeat identically on another machine — a missing
/// file, an unknown scene, an unwritable output. Retrying these burns the
/// queue for nothing; a human has to fix the .blend or the path.
/// </summary>
private static readonly string[] NonRetryablePatterns =
{
"cannot read file",
"file not found",
"unable to open",
"is not a blend file",
"failed to read blend file",
"unknown scene",
"scene not found",
"unable to create directory",
"couldn't create directory",
"cannot write file",
"cannot save:",
"no camera",
"python script fail",
"unknown argument",
};
/// <summary>The frame Blender is currently working on, or null.</summary>
public static int? ParseFrame(string line)
{
var m = FrameLine().Match(Message(line));
return m.Success && int.TryParse(m.Groups[1].Value, out var f) ? f : null;
}
/// <summary>The path of a frame Blender has just written, or null.</summary>
public static string? ParseSaved(string line)
{
var m = SavedLine().Match(Message(line));
return m.Success ? m.Groups[1].Value : null;
}
public static bool IsErrorLine(string line) => ErrorLine().IsMatch(Message(line));
public static bool IsNonRetryableError(string line)
{
foreach (var p in NonRetryablePatterns)
{
if (line.Contains(p, StringComparison.OrdinalIgnoreCase)) return true;
}
return false;
}
}