Completes RenderPipeline2 phases 3-5. Additive throughout: with validation.enabled=false the pipeline behaves exactly as before. Validation (§8) runs on the worker straight after a render, while it still holds the job and the files are local. Everything is read from the EXR header — one 64 KB read per sampled frame, no pixel decode — so the engine needs no oiiotool. The render job deliberately stays open through the stage, so a worker that dies there is reaped and retried instead of stranding the export in VALIDATING. Timecode is the primary check and is anchored on the plate, never the API: measured on the live show, Shot.timecodeStart is the editorial in-point and sits one handle-length (8f) ahead of the plate, so checking against it would pass the defective exports and fail the correct ones. Reference precedence is manifest timing plate -> plate found on disk -> API value, and the API rank is WARN-only. The panel now resolves plate provenance by walking the comp's own footage layers, so the worker never guesses which element carries the comp's timing. Verified against real studio renders via the new --validate mode: it catches both defect classes seen in shipped work — the +8 handle offset and frame-derived timecode (00:00:41:17 = frame 1001 at 24fps). QC (§10) is one service behind two front doors, API-key for the panel and session for the web. PASS means ready to upload and nothing else; FAIL sets the comp task to CHANGES. The panel gains a QC section that imports the real EXR sequence at full res rather than the H.264. Delivery (§11) rides the existing queue as a DELIVERY_BUILD job: hard links with a copy fallback, verification by re-hashing delivered bytes against the validation-time digest, manifest.json and the CSV report. fsutil hardlink list is unavailable over SMB, so link identity is never queried — reading the bytes back covers links and copies alike. Also fixes ApiClient.ProgressAsync throwing on transport failure, which would have aborted work in flight during a brief server outage; progress reporting is best-effort by contract. Removes the stale RenderWorker/ rule from the root .gitignore. The worker's source is tracked like the rest of the repo and its own .gitignore already covers bin/, obj/ and publish/; the rule silently excluded new files while the existing 20 stayed tracked. Not yet applied: migration 20260813000000 was hand-written because the dev database was unavailable — run prisma migrate deploy and verify before relying on it. None of this has run against a live server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VFXReview RenderWorker
Windows service that drives aerender.exe on the artist workstations (RenderPipeline2 §7).
It talks only HTTP to the VFXReview server (/api/ext/*, API-key auth) — it never
gets a database connection. All scheduling policy (render windows, Render Now,
urgent priority) is enforced server-side in the claim endpoint; the worker polls dumbly.
One click → EXR + MOV + MP4
A single Queue Export click produces all three deliverables. The server chains two jobs:
- AE_RENDER —
aerenderwrites the clean EXR sequence (no overlay, no LUT). - PREVIEW_ONLY — created automatically when the render completes. The
worker runs
AfterFX.com -noui -r scripts/vfxr_build_preview.jsx, which opens the studio slate/overlay template AEP, imports the rendered EXRs, rebuilds the shot around them (OCIO →_SHOW LUT→UNG_VFX_OVERLAY), duplicatesUNG_EXPORT_TEMPLATEinto a preview comp with the slate filled in, queues the MOV (4444 Tri) and MP4 (REVIEW_PREVIEW) output modules, and saves a throwaway AEP. The worker then runsaerender -project <that aep>with no-comp, rendering both outputs in one launch.
The MOV and MP4 land beside the EXRs. The MP4 is uploaded and registered as an
ordinary Version — internal-only, never client-visible, and it changes no task
or shot status (§10.0). Preview settings live in SystemConfig under
preview.*, so template names and paths are changed without redeploying.
If the preview stage fails, only it is retried — the validated EXRs are never re-rendered. The temp AEP is kept on failure so you can open it and see exactly what the farm built.
Proving headless AE first
Headless AE is the one real unknown, so prove it before relying on it (spec 18.2-C4). Close After Effects, then:
.\scripts\test-preview-build.ps1 -ExrDir "V:\_EXPORTS\...\v002" -ShotCode "UNG_111_001_030" -Version "v002"
It runs the exact build the worker runs and prints the result plus any warnings
(missing LUT comp, missing slate layer, …), then gives you the aerender command
to render what it built. If it reports no result file, AE cannot script headlessly
under that account — fall back to having the panel pre-build the preview comp in
the artist's AEP (then it is pure aerender), or the ffmpeg engine.
What it does (Phase 2 scope)
- Registers on startup (E6) and receives server-supplied tuning (poll/heartbeat/lease/stall).
- Heartbeats every 30 s (E7) — the heartbeat response is also the cancel channel.
- Claims one
AE_RENDERjob at a time (E8), runsaerender.exewith the manifest's comp/frame-range/templates, parsesPROGRESS:lines, reports progress + ETA (E9, renews the lease). - Stall watchdog: no progress for
stallTimeoutSeconds(default 600) → kill process tree, retryable fail. - Deterministic errors (missing footage / missing comp / unopenable project) → non-retryable fail (E10);
transient errors auto-requeue server-side up to
maxAttempts. - On success: uploads the full aerender log via presign (E20), reports complete (E11).
- Crash recovery:
current-job.jsonwritten on claim; on restart the worker asks the server what became of the job and reports a retryable fail if it was still ours. Partial renders are never resumed — the next attempt clears its own output files and re-renders. - Durable reporting: complete/fail reports spool to disk and replay in order with backoff — rendering continues while the server is down.
- Validation (Phase 3): after a successful render the worker checks the sequence it just produced and reports every result in one call (E12). See below.
Validation stage
Runs on the worker straight after the render, while it still holds the job and the files are
local. The Export sits in VALIDATING until the report lands; the server decides what the
results mean (any FAIL → VALIDATION_FAILED, otherwise the preview stage is queued).
Checks: file_count, missing_frames, duplicate_frames, first_frame, last_frame,
filename_consistency, zero_byte, corrupt, resolution, bit_depth, alpha_channel,
exr_type, timecode_plate_match, timecode_consistency, timecode_fps.
No oiiotool needed. Everything is read from the EXR header — one 64 KB read per sampled
frame, no pixel decode. Full-sequence checks (count, gaps, names, sizes) touch every file;
header checks sample first/middle/last plus every Nth frame (validation.sampleEvery).
Timecode is the primary check (spec §8.1a) and is compared against the plate, in this
order: the timing plate named in the manifest → a plate found under plateSearchRoots →
the API timecode minus handles, which is WARN-only because the API stores the editorial
in-point and is a handle-length ahead of the plate by design.
Checksums are xxHash64 per file, with a sequence digest stored on the Export and reused by the delivery builder to verify what it packaged.
Server-side switches (SystemConfig, no worker redeploy): validation.enabled,
validation.sampleEvery, validation.handleFrames, and validation.overrides for per-check
severity, e.g. {"exr_type":"warn"}.
Checking a render by hand
.\VFXReviewWorker.exe --validate "V:\_EXPORTS\UNG\102\UNG_102_019_010\v005" --plate "V:\_FOOTAGE\UNG\UNG_102\UNG_102_019_010_BG01_TT_v001\UNG_102_019_010_BG01_TT_v001.01001.exr"
Runs the whole engine over an existing folder with no server, job or queue, and prints the report. The frame range, pattern and resolution are inferred from the files. Exits non-zero if any check fails, so it also works in a script. Useful for auditing work that predates the pipeline, and for proving the checks on real shots before turning the stage on.
Preview generation (Phase 4) plugs into this same service.
Packaging for another machine
On a machine with the .NET 8+ SDK (only the packaging machine needs it):
.\scripts\publish.ps1 -Zip
Produces publish\win-x64\ containing exactly two things: a self-contained
VFXReviewWorker.exe (~67 MB, no .NET runtime needed on the target) and
scripts\vfxr_build_preview.jsx, which the exe loads from its own folder at
runtime. Keep them together — the script is deliberately not embedded in the
exe so the slate/preview build can be patched without a rebuild. -Zip also
writes VFXReviewWorker_<date>.zip for copying.
Installing on the target
-
Copy the folder to the render machine, e.g.
C:\pipeline\VFXReviewWorker. -
Create
C:\ProgramData\VFXReviewWorker\config.json(next section). -
Verify before installing the service:
.\VFXReviewWorker.exe --checkThis resolves every path, confirms
aerender.exe/AfterFX.com/ the build script exist, checks eachpathMappingstarget is reachable from that machine, and pings the server to confirm the API key is accepted. It exits non-zero if anything would stop the worker running, so a wrong path is caught here instead of in the logs an hour later. -
Install the service (below), then confirm the machine appears on the web Pipeline → Machines page.
Run --check again under the service account (runas /user:STUDIO\svc-render)
if the service will run as someone other than the logged-in artist — mapped
drives and share permissions differ per account, and that is the most common
cause of a worker that registers fine but cannot find footage.
Upgrading later: stop the service, replace the folder, start it again. Config lives in ProgramData and is untouched.
Configure
Create C:\ProgramData\VFXReviewWorker\config.json (§7.8):
{
"serverUrl": "https://review.twotalesvfx.com",
"apiKey": "<API_SECRET_KEY>",
"machineName": "RENDER-01",
"aerenderPath": "C:\\Program Files\\Adobe\\Adobe After Effects 2026\\Support Files\\aerender.exe",
"aeVersion": "24.3",
"ffmpegPath": "C:\\pipeline\\bin\\ffmpeg.exe",
"plateSearchRoots": ["V:/_FOOTAGE/UNG"],
"pathMappings": [
{ "from": "//SAN/", "to": "S:/" }
]
}
pathMappings translate the manifest's canonical UNC paths to this machine's drive
mappings. plateSearchRoots is optional: it is only consulted when a manifest carries no
timing plate (older panel versions), to find the shot's plates for the timecode check.
Everything tunable (poll interval, lease, stall timeout, max attempts, validation) lives in
the server's SystemConfig and arrives at registration — no per-machine tuning files.
Run interactively (first-time smoke test)
VFXReviewWorker.exe
Logs go to the console-less service log at %ProgramData%\VFXReviewWorker\logs\worker_YYYYMMDD.log
(14-day rolling). Confirm the machine appears on the web Pipeline → Machines page, then stop it.
Install as a Windows service
Run as a studio account with SAN access (works whether or not an artist is logged in):
sc.exe create VFXReviewRenderWorker binPath= "C:\pipeline\VFXReviewWorker\VFXReviewWorker.exe" start= auto obj= "STUDIO\svc-render" password= "<password>"
sc.exe description VFXReviewRenderWorker "VFXReview render pipeline worker (aerender)"
sc.exe start VFXReviewRenderWorker
Uninstall: sc.exe stop VFXReviewRenderWorker && sc.exe delete VFXReviewRenderWorker.
Rollout notes (spec §17.2)
- Install on one workstation first; add the second after a clean week.
- Set
render.maxAttempts = 1in SystemConfig for the first week (observe before auto-retrying). - Enter each machine's render windows on the Machines page (
availability), defaults: weekdays 19:00–08:00 + full weekends; Render Now override = 4 h. - The dashboard warns when the two workstations report different AE versions.
Tests
cd RenderWorker
dotnet test
Covers the progress parser against recorded aerender transcript lines (success, error, non-retryable), ETA math, path mapping, log-tail ring buffer, and the output-dir clearing guard (only files matching the job's own pattern prefix are ever deleted).