Files
vfxreview/RenderWorker/VFXReviewWorker/WorkerOptions.cs
twotalesanimation 70cf45ead4
Deploy / deploy (push) Successful in 7m2s
feat(pipeline): standalone Blender renders on the same queue
Blender work is not a shot submission: no Export, no version, no
validation, QC or delivery. It is "Ctrl+F12, but on the farm", so it is
modelled as what it is — a queue entry and nothing more.

That turned out to need very little. Claim, progress, fail and
complete-render already tolerated a job with no Export behind them, so a
Blender job reuses the whole lifecycle; RenderJob gains projectId, label
and submittedBy so a job with no shot code still has something to show.

Capability gating is new and was already missing: the claim now filters
job types by the machine's reported tools. Without it a workstation
without Blender would claim Blender work and fail it repeatedly, burning
the attempts while a machine that could have rendered it sat idle. The
worker asks for Blender jobs unconditionally and the server refuses them
— trusting the worker to self-select would have left the same hole.

The runner treats a clean exit with no saved frames as a failure:
Blender exits 0 when a frame range is empty or an output path is
unwritable, and a job that silently rendered nothing is the worst
outcome available.

Standalone jobs auto-requeue on a transient failure like any other work;
delivery builds are deliberately excluded, since a half-written package
wants a human first.

Adds the Blender add-on (BlenderPanel/) and its docs. The add-on refuses
to submit an unsaved file — the farm opens the .blend from disk, so
unsaved changes would render the previous state without warning. Its
payload is pinned by a contract test, since add-on and server only ever
meet over JSON.

Worker 1.5.0. Adds migration 20260815000000_blender_render_jobs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:53:19 +02:00

104 lines
4.7 KiB
C#

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace VFXReviewWorker;
/// <summary>
/// Machine-local config (RenderPipeline2 §7.8) — deliberately minimal;
/// everything tunable lives in server SystemConfig and arrives via E6.
/// Default path: C:\ProgramData\VFXReviewWorker\config.json
/// </summary>
public sealed class WorkerOptions
{
public string ServerUrl { get; set; } = "";
public string ApiKey { get; set; } = "";
public string MachineName { get; set; } = Environment.MachineName;
public string AerenderPath { get; set; } = @"C:\Program Files\Adobe\Adobe After Effects 2026\Support Files\aerender.exe";
/// <summary>AfterFX.com — the console AE used for the headless preview build. Defaults beside aerender.</summary>
public string? AfterFxPath { get; set; }
/// <summary>vfxr_build_preview.jsx — defaults to scripts\ beside the worker exe.</summary>
public string? PreviewScriptPath { get; set; }
public string? FfmpegPath { get; set; }
/// <summary>
/// blender.exe. Its presence is what makes this machine eligible for
/// Blender jobs: the version is reported at registration and the server
/// only hands BLENDER_RENDER work to machines that have it.
/// </summary>
public string? BlenderPath { get; set; }
public string? OiiotoolPath { get; set; }
public List<PathMapping> PathMappings { get; set; } = new();
/// <summary>
/// Optional footage roots searched for a shot's plates when a manifest
/// carries no timing plate (rank 2 of §8.1a) — e.g. ["V:/_FOOTAGE/UNG"].
/// Leave empty and old manifests simply fall through to the API fallback.
/// </summary>
public List<string> PlateSearchRoots { get; set; } = new();
/// <summary>
/// Read from the assembly, not from config.json: a hand-maintained version
/// string is exactly the thing that goes stale, and a machine reporting a
/// version it is not running is worse than no version at all.
/// Includes the git SHA when publish.ps1 stamped one ("1.1.0+feb1c7b").
/// </summary>
public string WorkerVersion =>
typeof(WorkerOptions).Assembly
.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? typeof(WorkerOptions).Assembly.GetName().Version?.ToString()
?? "unknown";
public string? AeVersion { get; set; }
public static string DataDir =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "VFXReviewWorker");
public static string DefaultConfigPath => Path.Combine(DataDir, "config.json");
public static string LogsDir => Path.Combine(DataDir, "logs");
public static string SpoolDir => Path.Combine(DataDir, "spool");
public static string ScratchDir => Path.Combine(DataDir, "scratch");
public static string StateFilePath => Path.Combine(DataDir, "current-job.json");
/// <summary>AfterFX.com sits beside aerender.exe in a standard AE install.</summary>
public string ResolveAfterFxPath()
{
if (!string.IsNullOrWhiteSpace(AfterFxPath)) return AfterFxPath;
var dir = Path.GetDirectoryName(AerenderPath);
return dir is null ? "AfterFX.com" : Path.Combine(dir, "AfterFX.com");
}
public string ResolvePreviewScriptPath()
{
if (!string.IsNullOrWhiteSpace(PreviewScriptPath)) return PreviewScriptPath;
return Path.Combine(AppContext.BaseDirectory, "scripts", "vfxr_build_preview.jsx");
}
public static WorkerOptions Load(string? path = null)
{
path ??= DefaultConfigPath;
if (!File.Exists(path))
{
throw new FileNotFoundException(
$"Worker config not found at {path}. Create it with serverUrl, apiKey and machineName (see RenderWorker/README.md).");
}
var json = File.ReadAllText(path);
var opts = JsonSerializer.Deserialize<WorkerOptions>(json, JsonOpts)
?? throw new InvalidOperationException($"Could not parse {path}");
if (string.IsNullOrWhiteSpace(opts.ServerUrl)) throw new InvalidOperationException("config.json: serverUrl is required");
if (string.IsNullOrWhiteSpace(opts.ApiKey)) throw new InvalidOperationException("config.json: apiKey is required");
if (string.IsNullOrWhiteSpace(opts.MachineName)) opts.MachineName = Environment.MachineName;
return opts;
}
public static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
}
public sealed class PathMapping
{
public string From { get; set; } = "";
public string To { get; set; } = "";
}