How a frame is rendered
You do not need this page to use Premation. You need it to understand why certain classes of bug do not happen here, what happens when something goes wrong, and to predict what a change will do.
Three processes
Since 0.9 the editor is an interface to a native engine, not a renderer with an interface bolted on.
┌───────────────────────────┐ ┌────────────────────────┐
│ UI process │ │ Electron main │
│ React panels, timeline, │ ◄────► │ starts, watches and │
│ inspector, the viewport │ │ restarts the engine │
│ window that shows frames │ └───────────┬────────────┘
└─────────────▲─────────────┘ │ spawns
│ finished frames ▼
│ ┌────────────────────────┐
└─────────────────────┤ premation-engine │
│ C++20: the document, │
│ evaluation, the render│
│ graph on the GPU, │
│ decode, audio, text, │
│ effects, plugins, │
│ export │
└────────────────────────┘| Process | Owns |
|---|---|
| The UI (Electron’s renderer) | Everything you click: panels, selection, zoom, layout. It shows the finished frames the engine sends, and keeps a read-only mirror of the document to draw its panels from. It never edits the document directly |
| Electron main | Starting premation-engine, watching it, restarting it, and the dialogs when it will not stay up |
premation-engine | The document and its undo history, animation and expression evaluation, the render graph, the GPU, media decode, audio, text and vector drawing, effects, native plugins, and export |
The engine is a separate operating-system process — not code inside the page and not an add-on inside Electron main. That is the design decision behind the crash behaviour below.
Commands, not writes
The UI talks to the engine through one versioned command API: commands change the document, queries read it, and change events tell the UI what moved. Every command is a plain value with an exact inverse, so it can be logged, replayed and undone.
That is why nothing else — a panel, an AI tool, a script, the command line — has a private way to edit the project: they all send the same commands. It is also why “undo” is uniform: there is one history, and it is the engine’s.
Editor state is a different thing and never enters the document. Selection, zoom, panel layout and scroll live in the UI.
The pipeline
Document + animation + expressions
│ evaluated at ONE time
▼
A flat description of that frame layers, transforms, effects, text, media
│
▼
The render graph passes drawn on the GPU (Direct3D 12 or Metal)
│
▼
A finished frameEvaluation samples the animation at one time and produces a plain description of the frame. Everything downstream draws from it, whoever asked:
- the viewport and any pop-out viewports,
- the secondary views in 2-up and 4-up layouts,
- cached playback,
- the export dialog, the Render Queue and the command line.
This is why “the export doesn’t match the preview” is not a class of bug that can exist here. They are not two renderers that have been kept in sync — there is one engine, and preview, export and automation are three ways of asking it for a frame.
The engine’s shaders are written in WGSL and run through Dawn, the library Chromium uses for WebGPU, on Direct3D 12 (Windows) or Metal (macOS). The finished frames are handed to the viewport window; the UI does not draw the composition itself. See System requirements for what the GPU needs.
Precision
The project’s bit depth — 8, 16 or 32 bits per channel — sets the precision of the engine’s intermediate buffers. At 32 bits, values far above 1.0 and very small steps survive every pass, provided the GPU can filter and blend 32-bit float textures. A GPU that cannot renders the frame at 16 bits rather than failing.
Since 1.0 the final picture is dithered by at most half an 8-bit step, so a smooth dark gradient does not band when it is shown or delivered at 8 bits. The dither is a fixed function of each pixel — no randomness — so the same frame still comes out the same every time, and a flat colour is unchanged.
Alpha is premultiplied
Past the decode, every colour in the pipeline is premultiplied. There is one alpha space, no per-draw flag selecting between two, and no shader variant that handles the other case.
A file’s alpha mode never reaches the shading stage. It is consumed once at the decode, the only boundary that can answer the question once per file rather than once per draw:
| The file says | What happens at decode |
|---|---|
| Straight | The colour is multiplied by alpha |
| Premultiplied | The bytes already are, so they are used as they are |
Straight and Premultiplied in Interpret Footage ▸ Alpha describe the file; they change how it is decoded, not how it is shaded.
Why it matters
A bilinear sampler averages in whatever space it is handed, and straight is the wrong one: averaging a colour with a fully transparent neighbour drags the colour toward whatever the transparent pixel happened to store, which shows as a dark halo around every soft edge. Filtering premultiplied values is correct by construction.
Determinism
Frame time is exactly index / fps — never wall-clock. Two renders of the same
project produce identical frames, and a re-render after a crash resumes
identical work. Particles are deterministic for the same reason: the same frame
always produces the same field.
See Determinism.
When something goes wrong
The engine is built so that one failure does not become a lost session.
- A bad layer, effect, plugin, asset or decode does not blank the frame or stop the editor. It is isolated, recorded against that layer, and every other layer keeps rendering. A native plugin that faults disables only its own effect instance — see Plugins.
- If the engine process stops, Electron main restarts it and replays your recorded edits into the new one, so you are put back where you were. A toast reads “The engine restarted (…); N edits restored”.
- If it keeps stopping — three times inside a minute — the editor pauses editing and offers a copy of the project’s last autosave before you try again or quit. See When the engine stops.
Design rules you can rely on
These are enforced in the codebase and they show up in how the app behaves:
- The engine’s document is the source of truth. The UI reads a mirror of it and writes only by sending commands; nothing else holds a shadow copy.
- Every mutation is a command with an inverse. Direct writes would bypass undo, and undo is the feature people notice being broken first.
- Rendering is deterministic. No wall-clock, no unseeded randomness, no dependence on how the interface happens to be laid out.
- No silent degradation. If a code path cannot do what was asked, it raises an error with a reason you can act on. Producing a plausible-looking wrong result is treated as worse than failing.
That last rule is why video export refuses to write a zero-frame file, why the engine stops with a named reason instead of drawing a blank stage when it has no GPU, and why the info readout shows a placeholder instead of an invented colour.