Skip to Content
Core conceptsHow a frame is rendered

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 │ └────────────────────────┘
ProcessOwns
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 mainStarting premation-engine, watching it, restarting it, and the dialogs when it will not stay up
premation-engineThe 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 frame

Evaluation 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 saysWhat happens at decode
StraightThe colour is multiplied by alpha
PremultipliedThe 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.

Last updated on