linked paths · share
Active renders
| Job | Progress | Worker | ETA | |
|---|---|---|---|---|
Rendering capacity
connected workers
Require review
Failed jobs and errors
Jobs
↑ ↓ to walk the list| Job | Progress | Worker | Preset | Prio | ||
|---|---|---|---|---|---|---|
|
|
|
Queue templates
selected
Queue draft · jobs
| Shot | Map | Preset | Range | Prio | Master | ||
|---|---|---|---|---|---|---|---|
| ✓ | |||||||
| Select shots from the left to start a queue. | |||||||
Path mappings
| Shot ↕ | Map / Level ↕ | Version ↕ | Status |
|---|---|---|---|
Map distribution
- CPU
- GPU
Completed renders
↑ ↓ to walk the list| Report | Pipeline | Worker | Finished | Render | ||
|---|---|---|---|---|---|---|
|
|
Preferences for this project. Naming conventions drive how scenes, shots and versions are read out of asset names.
Sign in as a server admin to see who this licence is issued to and the machine ID for renewals.
One folder per stream, one file per day, kept 30 days. events/ workers joining and leaving, renders finishing, queues arriving, sign-ins. activity/ the full trace of everything the server did. crash/ errors with their tracebacks. Read-only in demo mode - readable and downloadable, but only a server admin clears them.
| Filename | Stream | Size | Last write | |
|---|---|---|---|---|
How the pieces fit together, the web front-end, the server it talks to, and the render worker that does the work.
The shape of the system
RenderDeck is three parts that only ever meet over HTTP:
- The server (
server.py), a FastAPI app holding one SQLite database per project, serving both the REST API and this page. - This front-end, server-composed page templates plus native JavaScript modules (Alpine.js). There is no frontend build step.
- Workers, one
workerprocess per render machine. Each one launches Unreal, watches the render, and reports back.
Everything is scoped to a project. Every API call carries ?project=NAME, and a project owns its own jobs, path maps, pipelines, reports and naming conventions. Switching projects in the picker switches the whole app.
The front-end, page by page
- Overview
- Live counts for the project, the renders in progress with a stop/inspect on each, worker capacity as a strip of blocks, and the jobs waiting on review.
- Queue Manager
- Two workspaces sharing one page, switched from the top bar. New Queue: search and select shots by scene, apply a preset, output path, priority or master sequence to the whole selection at once, build a draft, then save or load it as a template. Each draft row can still be edited on its own for its map, frame range or master. Pathmapper: Scene Inspector browses by scene or by map, links a shot to a map, and filters by linked / missing / conflict; Map coverage charts how linked paths split across maps. A banner appears here whenever the shot search index needs rebuilding after a pathmapper sync. Building or submitting a queue needs the editor role; the Pathmapper views are read-only for anyone who can reach the page.
- Jobs
- The render queue as a list with a persistent inspector alongside it: filter by status, sort, filter by tag, search, walk the list with the arrow keys, and act on the focused job or on a bulk selection from the footer.
- Workers
- Every connected machine, its hardware, what it is rendering, its distributed toggle, and the stop / shutdown controls.
- Pipelines
- Render presets: an MRQ config path, output folder, resolution and format. Applying one to a job fills in
uconfig_pathand the output path. - Reports
- Finished renders, in the same list-plus-inspector shape as Jobs: filter by status, sort, search, and read the analytics pane - success rate, average render time, and average time by pipeline - until a report is focused.
- Settings
- Per-project naming conventions, with a live preview and an escape hatch to a custom regex, per-browser display preferences, and the server's licence.
How the page stays current
Two channels, no polling loop:
- REST for everything you ask for.
GET /api/getfor jobs,/api/get/workers,/api/reports/get_all, and so on. The Queue Manager's shot lists are the exception that never fetches a whole table: scenes come from/api/pathmaps/index/summaryand a scene's shots a page at a time from/index/entries, so opening it stays cheap on a project with thousands of shots. - Server-sent events on
/api/updates?project=NAMEfor everything you did not. The dot at the bottom of the sidebar is this connection.
The server pushes three kinds of message:
- data_refresh
- Something changed, so the page re-fetches whatever the current view needs.
- job_updated
- A single job moved. Patched in place, so a progress bar can tick without refetching the queue.
- custom_message
- A one-off notice, shown as a toast.
Naming conventions
Scene, shot and version are read straight out of an asset's name using three patterns set per project under Settings. XXXX matches letters or digits, #### matches digits only; everything else is literal.
With the defaults SC_XXXX / Shot_XXXX / vXXXX, the asset SC_0001_Shot_0010_v001 reads as scene SC_0001, shot Shot_0010, sequence id SC_0001_Shot_0010 and version 1. Change the patterns and the Path Maps grouping, the queue builder and the job records all follow.
A convention that carries no literal marks which number is which - a purely positional scheme such as show_episode_seq_scene_shot - cannot be written that way. Toggle Custom regex on a field in Settings instead and write a plain regular expression for it, with the value in capture group 1.
A job, start to finish
- un-assigned (and its equivalent, ready to start): submitted, waiting. Either is a state a worker will pick up.
- in progress: a worker has claimed it; the server stamps the worker name and start time.
- rendering: Unreal has started and is reporting progress.
- success: finished. A render report is written with the render and total time.
- errored: the render died. A report is written too, with the progress it reached.
- cancelled: stopped from the UI. Also reported.
Reset puts a job back to un-assigned so it can be picked up again. A job left in progress by a worker that died stays there until you reset it, that is deliberate, so a crash is visible rather than silently retried.
Who renders what
Each worker runs in one of two modes, toggled per worker on the Workers page:
- Distributed on
- The worker pulls the highest-priority unassigned job in the project. This is the normal farm setup, add workers, they share the queue.
- Distributed off
- The worker only renders jobs it was explicitly given with Assign. Use it to pin a heavy shot to a specific machine.
Sorting is priority first, then oldest-first. A worker set to more than one CONCURRENT_TASKS asks for a batch rather than a single job: within one priority band the batch is grouped by map, so one Unreal instance renders as many same-map jobs as it can before it has to reload the level - grouping never promotes a lower-priority job over a higher-priority one. With a deep queue and several idle workers, a fair-share cap stops one worker claiming the whole thing before the rest of the farm gets a turn. The server decides throughout: a worker asks for work and takes what it is handed.
Roles
Roles are granted per project. Editor on one show grants nothing on the next, and the role chip in the header always shows what you hold on the project that is currently open.
Nothing is readable without an account. The server refuses every route to a caller with no token - the pages, the job list, the reports, the pathmapper, the API - so the sign-in screen is the whole application until somebody signs in. The handful of exceptions carry no data: the version string, the licence state (which holds the activation request ID a fresh install needs), and whether an admin account exists yet.
- worker
- A machine credential, not a person. It is off the role ladder entirely: it reaches no page and no role-gated endpoint, only the job endpoints a render worker polls - claim a job (single or in a batch), report progress, report finished. Give the farm its own account with this role and put it in the worker's settings (Username / Password, beside the server URL). A stolen worker password is worth the render loop and nothing else.
- viewer
- Read-only. Every page is visible.
- editor
- Everything a viewer can do, plus submitting queues, resetting and deleting jobs, changing priority, assigning and stopping workers.
- admin
- Everything on that project, plus its pipelines, its naming settings and its render history.
- server admin
- A tier of its own rather than a role: manages accounts, creates, locks and removes projects, is admin on every project, and holds the server's licence. Open it from the shield in the header.
Where a grant applies
Each account holds a role per project name, plus an optional All other projects fallback used for anything not named explicitly. A project without either is simply out of reach.
How it is enforced
Every gated endpoint resolves the project the request is actually touching and checks the role held there, so the UI greying a button is a convenience, never the control. Accounts live in settings/server/users.json with passwords stored as PBKDF2 hashes.
Sign-in returns a signed token that expires (12 hours by default, token_ttl_hours in the server settings). Changing or resetting a password invalidates that account's other sessions immediately. Deleting settings/server/secret.key signs everybody out at once.
Locking yourself out
You cannot delete or demote the last server admin through the UI. If users.json loses its last one anyway - a hand edit, or a demo login that was the only admin - the server says so in the log and raises a red banner in the header, and otherwise leaves the file alone. That banner carries a Create super user button: it names a new account and writes it as a server admin, then signs you in as it, which saves the hand edit and the restart. You can still fix it by hand instead, by setting "server_admin": true on an account and restarting. This is first-run setup, and it is the only unauthenticated write the server has: it exists because the state it repairs is the state where no credential can be presented. The server refuses it with a 409 the moment a server admin exists, refuses it in demo mode, and logs the account and the address it was claimed from. Know the trade it makes - for as long as no admin exists, anyone who can reach the port can claim the first one, which is the same bargain every first-run setup screen makes. That window is also how a fresh install is set up: RenderDeck no longer ships an admin / admin account, because a default credential every install shares and most never change stays guessable until somebody remembers, while this window shuts for good the first time somebody walks through it. Claim the account as soon as the server is up. RenderDeck writes no account on its own, ever - not on a first run, not to fill a gap. It has to be asked.
Licensing
RenderDeck is licensed per server, not per worker seat: one signed licence file, locked to the machine it was issued for, with a worker count as its pricing tier. The worker and the Path Mapper are free to run anywhere - only the server needs a licence.
Without one, the server runs a 30-day evaluation with no worker limit. Past that, or on an expired licence, it moves through a 14-day grace period and then a read-only state. None of these currently stop anything: the worker count, the state badge and the days-remaining shown in Settings are reporting, not a cap, and a farm can grow past what its licence covers without a worker being refused or a render being stopped.
First run: the server prints a request ID to the console, writes it to settings/server/REQUEST_ID.txt, and raises an unauthenticated banner across the top of the app (there being no account yet to sign in with and read it from). Send that ID to your RenderDeck vendor, then install the licence they send back from the same banner, or from Settings → Licence - paste it, upload it, or drop the file at settings/server/license.key.
Settings → Licence shows the worker count against the licensed number, expiry and days remaining, which features the licence names, and - server admin only - who it is issued to and the machine ID for renewals. The count is server-wide, not per project: a facility running several projects off one server sees the whole farm's worker count here, not one project's slice of it.
The worker script
The worker is a separate script, launched with worker.py. It is not started by the server and does not run inside it: one worker process runs on each render machine, and the two only ever talk over HTTP and a websocket.
python worker.py # desktop UI python worker.py --nogui # headless
Like the server, it builds its own virtual environment in ./venv on first launch and re-launches itself inside it, so a render machine needs nothing installed but Python and Unreal. It never renders anything itself: it launches Unreal and relays what happens.
Shipping it to a render machine
The worker shares util/ and modules/worker/ with the server, so it is not just one file to copy. Build a self-contained folder from the server repo:
python3 build/build_worker.py # -> dist/worker/ python3 build/build_worker.py --zip # -> dist/renderdeck-worker.zip python3 build/build_worker.py --list # show what it would collect
The script reads the imports rather than a fixed file list, so it keeps picking up the right modules as the shared code changes. Re-run it whenever you update the server. There is a matching build/build_server.py for the server itself.
Settings
Read from settings/worker/settings.json:
- WORKER_NAME
- Must be unique, it is the worker's identity on the server, and a duplicate connection is refused.
- MANAGER_URL
- Where the server lives, e.g.
http://127.0.0.1:6789. - CURRENT_PROJECT
- The project this machine renders for.
- UNREAL_EXE / UNREAL_PROJECT
- The editor binary and the
.uprojectto open. - OUTPUT_PATH
- Where frames are written on this machine.
- CONCURRENT_TASKS
- How many jobs this worker renders inside one Unreal instance. 1 (the default) is the classic one-job-per-process path; higher switches to the manifest path below, up to 64.
- POLL_INTERVAL
- Seconds between asking for work while idle.
- IDLE_TIMEOUT
- Seconds of no work before the worker shuts itself down. Set it long, or the machine leaves the farm during a quiet spell.
- WINDOWED / RESOLUTION_X / RESOLUTION_Y
- Windowed launches the editor visibly; otherwise it renders offscreen.
- DISTRIBUTED_RENDERING
- The starting mode. The Workers page can flip it live.
- WORKER_USERNAME / WORKER_PASSWORD
- The account this worker signs in with. There is no shipped default - give the farm its own account with the worker role (or editor on the projects it renders), or it connects to nothing.
What it does, in order
- Opens a websocket to
/ws/<WORKER_NAME>?project=…and answers the handshake with its CPU, GPUs, RAM and output path, that is what the Workers page shows. - Holds that socket open, answering
pingwithpong, and listening forstop_job(abandon the current render) andshutdown(leave the farm). - Asks for work:
GET /api/get/job/<WORKER_NAME>for one job, orGET /api/get/job_batch/<WORKER_NAME>?count=NwhenCONCURRENT_TASKSis above 1. A 404 means idle, and the idle timer starts. - Given a job (or a batch), builds the Unreal command line and launches the editor.
- Relays progress while it runs, then reports the outcome and goes back to asking for work.
With CONCURRENT_TASKS left at 1, each job gets its own Unreal instance, launched exactly as below. Set it higher and the worker instead writes a manifest and hands the whole batch to renderBootstrap.py, which renders the jobs back to back inside one Unreal instance - no repeat engine start, and no map reload between two jobs sharing a map.
The Unreal command line
Built from the job record, the map, the level sequence and the MRQ config all come from the queue you submitted:
UnrealEditor.exe <UNREAL_PROJECT> <umap_path> -game -MoviePipelineLocalExecutorClass=/Script/MovieRenderPipelineCore.MoviePipelinePythonHostExecutor -ExecutorPythonClass=/Engine/PythonTypes.customExecutor -windowed | -RenderOffscreen -resX=… -resY=… -JobId=<uid> -LevelSequence=<useq_path> -MoviePipelineConfig=<uconfig_path> -output_folder_id=… -version=… -sequence_id=… -output_path=<OUTPUT_PATH> -is_master=… -MasterSequence=<master_path>
How Unreal talks back
The worker listens on 127.0.0.1:6783. The executor running inside Unreal sends it JSON commands:
- StartTask
- Rendering has begun, the job flips to rendering.
- SetProgress
- A percentage. The worker forwards it in 5% steps and derives an ETA from elapsed time, so the server is not hit on every frame.
- SetStatus
- A status string, passed straight through.
- EndTask
- Success or failure. On success the worker posts the final progress and render time, then registers the job as successful; on failure it marks the job errored.
If the editor exits without an EndTask, the worker re-checks the job on the server: already finished means it was a clean exit, anything else is treated as a crash and the job goes to errored.
Endpoints a worker uses
- GET /api/get/job/<worker>
- Ask for one job.
- GET /api/get/job_batch/<worker>?count=N
- Ask for up to N jobs at once, when
CONCURRENT_TASKSis above 1. - PUT /api/put/json/<uid>
- Progress, ETA, render time.
- PUT /api/put/job_status/<uid>
- Status changes, including errored and cancelled.
- PUT /api/put/job_success/<uid>
- Finish a job and file its report.
- GET /api/get/job_status/<uid>
- Check whether a job is still ours.
- POST /api/worker/shutdown/<worker>
- Leave the farm cleanly.
Those seven calls are the whole worker contract. Anything that speaks them can act as a worker, which is exactly what demo mode does.
The API
Every endpoint, with request and response shapes, is browsable at /docs, FastAPI generates it from the server itself, so it cannot drift out of date.
Two rules worth knowing before you script against it: every project-scoped call needs ?project=NAME, and write endpoints expect an Authorization: Bearer <token> header from POST /auth/token.