@faceless-photolib/render-graph
Compiles a faceless-photolib document layer stack into a color-managed, backend-agnostic render graph of GPU passes, with the GpuBackend and TextRasterizer port definitions.
Part of faceless-photolib — a headless, color-managed, GPU-accelerated image-editing engine.
This package compiles a document's layer stack into a backend-agnostic
CompiledRenderGraph: a demand-driven DAG of GPU passes
(source / fill / text / blend / adjustment / lut3d / colorTransform /
resample / mask / clip / colorConversion / outputTransform). It auto-inserts the
reference↔working-space color conversions and the display output transform, and keys the whole
graph by a Merkle hash so identical subtrees share a cache identity. It compiles only — it
performs no I/O and never executes; backends do. It also owns the GpuBackend and
TextRasterizer port definitions every adapter satisfies.
Install
pnpm add @faceless-photolib/render-graphUsage
import {
compile,
merkleKeyFor,
emptyResolvedAssets,
planTiles,
tileBudgetFromEdge,
} from "@faceless-photolib/render-graph";
import type { Document, Viewport } from "@faceless-photolib/schemas";
// `doc` and `viewport` come from @faceless-photolib/document-model / schemas.
declare const doc: Document;
declare const viewport: Viewport;
// Compile is pure and synchronous. lut3d / colorTransform assets must already be
// resolved by the engine and handed in; emptyResolvedAssets() fails loud if one is needed.
const result = compile(doc, viewport, "web", emptyResolvedAssets());
if (result.kind === "ok") {
const graph = result.value;
console.log(graph.passes.length, "passes; output =", graph.output);
// Split the output region into bounded tiles for a memory-budgeted render.
const tiles = planTiles(viewport, tileBudgetFromEdge(2048));
if (tiles.kind === "ok") {
console.log(tiles.value.length, "tile(s)");
}
} else {
console.error("compile failed:", result.kind);
}
// Cheap identity key — works even on documents that don't yet compile
// (unresolved assets), so you can decide whether to recompile.
const key = merkleKeyFor(doc, viewport);API
| Export | Description |
|---|---|
compile(doc, viewport, surface, resolved?) | Compiles a document subtree + viewport into a Result<CompiledRenderGraph>. Pure, synchronous, no I/O. |
merkleKeyFor(doc, viewport) | Independent Result<string> identity/cache key over the enumerated subtree + settings + viewport; works on documents that don't compile. |
emptyResolvedAssets() | A ResolvedAssets whose every lookup fails loud (rejected("not-implemented")); the default for compiles with no lut3d/colorTransform layers. |
planTiles(region, budget) | Splits a region into a row-major sequence of bounded sub-viewports covering it exactly with no gaps/overlap. |
tileBudgetFromEdge(maxEdge) | Builds a TileBudget from a max square-tile edge length. |
CompiledRenderGraph | The compiled, backend-agnostic graph: passes, output, viewport, canvas, merkleKey. |
RenderPass | The discriminated union of pass nodes (source/fill/text/blend/adjustment/lut3d/colorTransform/resample/mask/clip/colorConversion/outputTransform). |
GpuBackend | The port every backend adapter satisfies: info(), render(graph), dispose(). |
BackendKind / BackendInfo | Backend selection (webgpu/webgl2/cpu) and its availability/health info. |
TextRasterizer | The port that rasterizes a TextSpec to RasterizedText (per-platform; outside the cross-backend pixel-parity guarantee). |
TextSpec / RasterizedText | Input/output shapes for the TextRasterizer port. |
ResolvedAssets | Synchronous resolver injected into compile for content-addressed lut3d / colorTransform assets. |
TileBudget | A per-tile working-set budget (max device pixels per tile). |
License
MIT
API reference
15 public exports · 15 documented · generated from source.
compilefunctioncompile(doc: { schemaVersion: 1; id: string & $brand<"DocumentId">; name: string; size: { width: number; height: number; }; settings: { connectionSpace: string & $brand<"ColorSpaceId">; blendSpace: "linear-light" | "perceptual"; outputTransform: "aces-1.x" | "aces-2.0"; dissolveSeed: number; }; layers: readonly Layer[]; }, viewport: { ...; }, surface: "web" | ... 3 more ... | "node-sdk", resolved?: ResolvedAssets): Result<...>Compile a document subtree + viewport into a backend-agnostic render graph. The pipeline (D3/D4/D5/D6/D7/D8): 1. `safeParse` the document — a schema-violating document is a `validation-error` naming the offending fields, with no graph emitted. 2. `enumerateForComposite` for bottom→top paint order (invisible layers and subtrees already removed; pass-through groups flattened; isolated groups carried as a single unit). 3. Lower each layer to float, color-managed passes in the connection space (auto-inserting reference↔workingSpace conversions, mask-multiply, resample, clip, isolated-group sub-composite), blending each contribution over the running backdrop. 4. Append the Display + View output transform for the surface's display. 5. Key the whole graph by the Merkle hash of the output pass + viewport. Determined solely by `(document subtree, viewport, surface, resolved)` — pure and deterministic. Asset descriptors (lut3d / colorTransform) are resolved by the caller through `resolved`, so render-graph performs no I/O.
emptyResolvedAssetsfunctionemptyResolvedAssets(): ResolvedAssetsAn empty {@link ResolvedAssets} — every lookup fails loud (`rejected("not-implemented")`) with a beacon. Used by the pure-compute callers (and tests) that compile documents with no lut3d/colorTransform layers; reaching a lookup means an asset was needed but no resolver was supplied, which must fail loudly rather than guess a descriptor.
merkleKeyForfunctionmerkleKeyFor(doc: { schemaVersion: 1; id: string & $brand<"DocumentId">; name: string; size: { width: number; height: number; }; settings: { connectionSpace: string & $brand<"ColorSpaceId">; blendSpace: "linear-light" | "perceptual"; outputTransform: "aces-1.x" | "aces-2.0"; dissolveSeed: number; }; layers: readonly Layer[]; }, viewport: { ...; }): Result<...>Compute the Merkle cache key for a document subtree + viewport (identity caching, D4). Equal subtrees + viewport produce an equal key; any edit to a contributing layer's parameters changes the key. This is an *independent* identity key over the raw enumerated subtree (+ render settings + viewport), NOT equal to a compiled graph's `merkleKey` — it must work on documents that do not compile (unresolved assets), so it never builds passes and is cheap to call before deciding whether to recompile.
planTilesfunctionplanTiles(region: { x: number; y: number; width: number; height: number; scale: number; }, budget: TileBudget): Result<readonly { x: number; y: number; width: number; height: number; scale: number; }[]>Split `region` into a row-major sequence of bounded sub-`Viewport`s, each within `budget.maxTilePixels` at the region's scale, that together cover the region exactly with no gaps or overlap. A region already within budget yields a single tile equal to the region (so an "untiled" render is the degenerate one-tile plan — the property the spec's "tiled equals untiled" scenario relies on). The region itself is `safeParse`-d so a malformed region (e.g. negative dimensions) is `invalid-request` rather than a silent empty plan.
tileBudgetFromEdgefunctiontileBudgetFromEdge(maxEdge: number): TileBudgetBuild a budget from a max edge length (square tile), the common form.
BackendInfointerfaceinterface BackendInfoAcquire/health info for a backend device.
CompiledRenderGraphinterfaceinterface CompiledRenderGraphThe compiled, backend-agnostic render graph. The same value drives every adapter unchanged. `merkleKey` is the identity/cache key (Merkle hash of the subtree + parameters). `canvas` is the document's pixel size — the size at which fill/text/resample passes paint (so canvas-sized passes and source-sized passes share dimensions for blending).
GpuBackendinterfaceinterface GpuBackendThe single `GpuBackend` port every adapter satisfies (gpu-backend spec). It accepts the same `CompiledRenderGraph`, reports only through the canonical vocabulary, returns a `Resource<RenderResult>` for the async render, and never throws. Device loss surfaces as the in-flight `Resource` transitioning to `error`.
RasterizedTextinterfaceinterface RasterizedTextA rasterized text raster: straight RGBA, row-major, length = width*height*4.
ResolvedAssetsinterfaceinterface ResolvedAssetsSynchronous resolver for content-addressed assets the compiler needs lowered INTO the graph (a lut3d table, a CLF color-transform program). The engine resolves the bytes through its AssetStore port up front and hands this synchronous lookup to `compile`, so render-graph never touches an asset store and stays a pure function of `(document, viewport, surface, resolved)`. A missing/unparseable asset surfaces as a canonical failure, never a silent identity.
TextRasterizerinterfaceinterface TextRasterizerThe `TextRasterizer` PORT (alongside `GpuBackend`). Text is rasterized by a per-platform adapter (Node: SVG → `sharp`/librsvg → RGBA), NOT by a hand-written WGSL/CPU pass — so text is explicitly OUTSIDE the cross-backend pixel-parity guarantee (per-platform rasterizers differ). A backend executing a `text` pass calls this port; absent an injected rasterizer the pass is a loud `rejected("not-implemented")` + beacon (never a blank/silent skip).
TextSpecinterfaceinterface TextSpecA line of text the rasterizer should render (TextRasterizer port input).
TileBudgetinterfaceinterface TileBudgetA working-set budget expressed as the max number of device pixels per tile.
BackendKindtypetype BackendKindBackend selection requested by a render.
RenderPasstypetype RenderPassA single pass node in the compiled graph. One backend-agnostic effect per pass.
@faceless-photolib/image-codec
Node image decode/encode (PNG/JPEG/WebP/AVIF via sharp) and an SVG-backed TextRasterizer — the ImageCodec / AssetStore boundary adapters for the faceless-photolib engine.
@faceless-photolib/backend-cpu
The CPU reference backend for faceless-photolib — executes a compiled render graph in pure float TypeScript, the golden renderer other backends are measured against.