faceless-photolib

@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-graph

Usage

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

ExportDescription
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.
CompiledRenderGraphThe compiled, backend-agnostic graph: passes, output, viewport, canvas, merkleKey.
RenderPassThe discriminated union of pass nodes (source/fill/text/blend/adjustment/lut3d/colorTransform/resample/mask/clip/colorConversion/outputTransform).
GpuBackendThe port every backend adapter satisfies: info(), render(graph), dispose().
BackendKind / BackendInfoBackend selection (webgpu/webgl2/cpu) and its availability/health info.
TextRasterizerThe port that rasterizes a TextSpec to RasterizedText (per-platform; outside the cross-backend pixel-parity guarantee).
TextSpec / RasterizedTextInput/output shapes for the TextRasterizer port.
ResolvedAssetsSynchronous resolver injected into compile for content-addressed lut3d / colorTransform assets.
TileBudgetA per-tile working-set budget (max device pixels per tile).

License

MIT

API reference

15 public exports · 15 documented · generated from source.

compilefunction
compile(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.

emptyResolvedAssetsfunction
emptyResolvedAssets(): ResolvedAssets

An 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.

merkleKeyForfunction
merkleKeyFor(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.

planTilesfunction
planTiles(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.

tileBudgetFromEdgefunction
tileBudgetFromEdge(maxEdge: number): TileBudget

Build a budget from a max edge length (square tile), the common form.

BackendInfointerface
interface BackendInfo

Acquire/health info for a backend device.

CompiledRenderGraphinterface
interface CompiledRenderGraph

The 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).

GpuBackendinterface
interface GpuBackend

The 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`.

RasterizedTextinterface
interface RasterizedText

A rasterized text raster: straight RGBA, row-major, length = width*height*4.

ResolvedAssetsinterface
interface ResolvedAssets

Synchronous 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.

TextRasterizerinterface
interface TextRasterizer

The `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).

TextSpecinterface
interface TextSpec

A line of text the rasterizer should render (TextRasterizer port input).

TileBudgetinterface
interface TileBudget

A working-set budget expressed as the max number of device pixels per tile.

BackendKindtype
type BackendKind

Backend selection requested by a render.

RenderPasstype
type RenderPass

A single pass node in the compiled graph. One backend-agnostic effect per pass.

On this page