Reference

Architecture

The state machine, provider boundary, output identity, hand edits, the pinned editor, and durable writes.

PixelKiln separates provider mechanics from the project state machine:

manifest + lock + planning + review + recovery + quality + artifact pipelines
──────────────────── Provider interface ─────────────────────────
PixelLabProvider   RetroDiffusionProvider   ComfyUIProvider   ScenarioProvider   FakeProvider

Everything above the provider boundary is backend-neutral. URL shapes, auth headers, request/response schemas, and optional account capabilities remain in the adapter.

Manifest and resolved specs

The committed manifest is intent. Manifest loading resolves each style's extends chain before defaults and generator constraints are applied. Ordinary fields use child-wins replacement; quality and provider option namespaces get their documented focused merge. Unknown parents and cycles fail before a plan. The inheritance marker never reaches runtime state.

Spec resolution then combines one resolved style and one asset, loads/reference-hashes style images, applies overrides, chooses a provider-supported generator, and computes a deterministic spec hash. The hash excludes project root, output location, and tags but includes every pixel-affecting setting. A provider may resolve local files before hashing. ComfyUI uses this hook to parse and hash workflow JSON without making a network request. The runtime graph can then be submitted without putting a machine-specific path in the stable identity.

Assets may also declare provider-owned scalar inputs or a provider-defined sequence. Provider.resolveInputs maps those values into runtime inputs and, when needed, a separate stable identity. ComfyUI uses the split for custom LoadImage bindings: the runtime value keeps an absolute path long enough to upload the file, while the spec and lock retain only its content hash and format. A move does not invalidate art; changing the bytes does. For ComfyUI frames, one named sequence expands into ordered still prompts while all other bindings stay fixed. Adapters without this capability reject non-empty providerInputs.

See manifest reference.

Derived quality state

A style quality profile resolves beside the paid spec but is excluded from its hash. This keeps two questions separate: whether the provider output is current, and whether the derived PNG is ready to ship. A palette or threshold change cannot schedule another generation.

An ordinary quality record binds one raw/source PNG and one refined PNG. A ComfyUI frame record binds every ordered source/output role and adds fps plus per-frame step and phase. Both retain the fixer revision, palette, audit, and named approval. Planning and doctor inspect the record without changing it. Pack and mount require current approval and include the record downstream. Tiles and provider-native animation remain outside this contract.

Dependency graph

Six kinds of asset draw from another asset's generated bytes, and all six are edges from a child spec to a parent spec in the same style: a revision (image-to-image or inpaint from a still), a character state, animation, or portrait (drawn by PixelLab from the character it holds or its south-facing file, so the edge is to that one file), a mirror (a local left-to-right flip of the parent's files), an outfit (PixelLab re-clothing a loop's whole frame set from a reference image, so the edge is to every one of the parent's files, not one), and a pro base's styleCharacter (the look it follows). Manifest loading rejects missing parents, self-references, and cycles. Resolution includes transitive parents even when --only selects just the child, chooses the parent's approved quality output when one exists, and hashes the parent's bytes (and a revision's optional mask) into the child identity for the single-file edges, so a regenerated parent makes its children stale rather than silently mismatched. A mirror and an outfit depend on the parent's whole output set instead, tracked outside specHash (see below), since there is no one file to hash at resolve time.

Planning walks the graph from the child and reports blocked unless each parent is committed source, an intact current download, or a current approved quality output. Blocked work is not actionable and has zero planned spend. A child whose parent is actionable in the same plan is deferred, and gen runs the plan in waves until nothing new becomes actionable. The submit boundary repeats the walk after queue and rate-limit waits, closing the gap between a printed plan and provider work. A mirror is the one edge with no provider on the far side: submit flips the files itself and records the parent's output hashes, and the plan compares those directly. An outfit records the same kind of hash over the parent's output set, but does call a provider (transfer-outfit-v2) and costs generations; billed and the usual lock lifecycle apply to it exactly as they do to a state or loop.

Providers opt into revision modes through supportsRevision. The first implementation is ComfyUI: it uploads content-addressed inputs, binds only the declared workflow inputs, and retains lineage in both the generic lock entry and provider metadata. Other adapters fail during offline resolution.

Lockfile

pixelkiln.lock.json is the committed paid-work record. Version 2 entries retain:

  • style/asset identity and spec hash;
  • revision mode, parent id/hash, optional mask hash, and strength;
  • provider and remote object/job ids;
  • explicit lifecycle status and errors;
  • durable provider source references, candidates, and selections;
  • outputs[] with portable path, SHA-256, optional structural role, and optional PNG/GIF media type, plus raw (the provider's bytes' hash) when post-processing changed them;
  • postprocess: what fetch did to the provider's bytes before writing them, today a palette snap (colors, dither: "none", distance: "redmean"). Same raw bytes and same record always give the same output bytes, which is what lets a restore reproduce a recorded hash. It is compared against the manifest on every plan: a difference is recoverable, never stale, because no provider work is involved;
  • terrainSettings, terrain entries only: the settings the request was sent with that specHash leaves out (the older mode, transition size, view, shape, and raggedness knobs, and each terrain's description as sent). Hashing them would have made every existing terrain entry stale, so plan compares this record instead and reports stale naming each changed setting. An entry from before it was recorded has none and is taken as current. A history entry keeps its own, so a restore is compared against the generation it brought back;
  • temporary supersededOutputs[] ownership hashes while a stale generation is being replaced;
  • provider-specific metadata under a provider-id namespace;
  • successful submission cost and cost unit, as estimated before the work was spent;
  • billed: what the provider actually charged, read from the completed job rather than estimated at submit time; null until a poll reads one, and for a generator or provider poll never reads it from at all. gen's wave budget still spends against the cost estimate above, since the real number is only known after the fact;
  • history[]: the generations this entry replaced, newest first. Each keeps its object and job ids, durable sources, output hashes, prompt, cost, billed amount, and when it was retired. PIXELKILN_HISTORY or the manifest's history caps the list. A restore swaps a kept generation back in and moves the current one onto the list, and the content cache keeps every kept generation's bytes through a prune.

Lock keys are styleId/assetId. Output paths use manifest-relative / separators so a clone or moved checkout remains valid. Legacy absolute v2 paths are rebased in memory and rewritten portably on the next save. The current manifest remains destination authority; a stale lock path cannot redirect restore into an unrelated project file.

Cost units that differ are never summed. Built-in adapters currently use generations, usd, compute-units, and free; custom adapters may register another non-empty unit. Candidate count also belongs to the provider estimate rather than being assumed globally.

State machine

The provider pipeline is deliberately resumable:

plan → submit → poll → pick (when needed) → fetch
                    └──────── structural/inline outputs ────────┘

Remote ids are saved immediately after submission. Providers that need several requests for one asset receive a checkpoint callback. Each accepted remote id is written before the next request begins. An incomplete checkpoint remains submit-actionable; a complete checkpoint is safe to poll even if the provider is interrupted before returning. Checkpoints are reused only when provider, generator, and resolved spec hash still match.

Generation and download failures remain separate, so paid work with a temporary CDN failure is recoverable at zero generation cost. Each stage can be rerun independently; gen is only their everyday orchestration. Planning maps current processing, review, and selected/download-failed entries to poll, pick, and fetch, respectively.

Provider responses are runtime-validated before entering lock state. A 2xx response with a missing object id, malformed URL set, invalid estimate, or changed field type becomes an explicit adapter error rather than corrupted durable state.

Storage URLs whose query strings carry provider credentials are transient. Adapters should replace them with refreshable provider references before settled lock state; successful ingestion also strips sensitive, inline, and local-file sources. This preserves recovery without turning the committed lockfile into a credential store.

Output identity

One manifest asset may produce multiple structural members. Roles such as tile-03 are load-bearing identity, not presentation. Audits, packs, mounts, and exporters all use the same role model. A consumer must request a role when there is no unambiguous primary output.

PNG ingestion validates signature, chunks, CRCs, palettes, compressed data, scanlines, dimensions, and supported color modes. GIF ingestion walks the logical screen, color tables, extensions, image-data blocks, and trailer. Both formats are validated before bytes become durable output or recovery cache data.

Concurrency and lock saves

Lock writes use a same-directory temporary file and rename. An advisory writer lock serializes separate processes. In-process saves queue per path, and field-level dirty patches merge separate snapshots so updates to different assets, or to different fields of one asset, do not silently lose the earlier write. Stale advisory locks are recoverable after their safety window.

Derived artifact transactions

Refine, pack, mount, and export use a separate managed bundle writer:

  1. Recover an interrupted prior transaction.
  2. Validate provenance ownership and manual edits.
  3. Compare every desired member and skip identical bytes.
  4. Write an immutable transaction journal.
  5. Stage all changing members beside their destinations.
  6. Move existing members to unique backups.
  7. Promote every stage.
  8. Write a durable commit marker.
  9. Remove backups, stages, journal, and marker.

Before the marker, recovery restores the old complete set. After the marker, recovery keeps the new complete set and finishes cleanup. Journal paths are validated against current destinations and reserved same-directory temp names. See derived artifacts.

Caches

The project content cache is keyed by output SHA-256. The account cache maps provider object ids to remote content hashes for adoption/salvage. Neither is authoritative or committed; both can be deleted and rebuilt. Every recovery byte is structurally validated before use.

Hand edits

A hand edit is committed art laid beside a generation, never over it. The manifest points at it with asset.source (or sourceByStyle for one style); plan keeps reporting the generation from the lockfile, while pack, mount, export, quality profiles, and revisions read the edit. For an entry with several PNG outputs the source is a stem and each member is <stem>-<role>.png, the same rule generated outputs follow, unless a file exists at the source path, which is one image placed for the whole set. A browser save writes <edit>.edit.json beside the edit: the editor, the time, and per file the hash of the generation it started from and its own hash, so regenerated-since is decided by comparing hashes, and "unchanged" is decided by comparing pixels rather than bytes, since editors re-encode.

Pinned editor build

The gallery's in-browser editor is a web export of Pixelorama with PixelKiln's bridge extension, built in CI from a build-time overlay on the pinned upstream tag (tools/pixelorama-bridge/) and published as a GitHub release whose tag semantic-release ignores. The package does not carry the 46 MB build; it carries tools/pixelorama-bridge/pin.json, the release tag plus the SHA-256 and size of every file, inlined into src/editor/pin.ts at bundle time, so the shipped code and the build it trusts are one artifact. installEditor fetches only missing or mismatched files into a per-release directory under the user cache, verifies each against the pin before renaming it into place, and the gallery serves a file only after the whole directory verified and its size still matches; a file that changed on disk is refused rather than served. The release tag is part of the served path, so the browser may cache the files as immutable, and a new pin is a new path.

Provider capability boundary

Providers are selected from a registry by each style's provider, falling back to the manifest default. Planning groups resolved work by provider and cost unit. After submission, the provider recorded on each lock entry is authoritative for polling, selection, download, restore, and tag routing. Required members cover support/estimate, submit, poll, and download; candidate selection is required only when an adapter can return alternatives. Account-wide listing, tagging, deletion, and balance are optional. Commands such as adopt or salvage report a capability gap rather than failing through an undefined method.

All four adapters share one retry policy (src/http.ts): transport failures, 408, 429, and the transient 5xx codes are retried up to four more times with Retry-After honoured, and every attempt has its own timeout. Connectivity checks opt out so doctor reports an unreachable server at once. A 4xx is never retried.

PixelLabProvider is production and live-tested. RetroDiffusionProvider is an experimental still, tileset, and animation adapter. Authenticated RD Fast and RD Plus single-candidate still lifecycles have passed end to end. Its multi-candidate, tileset, GIF, and spritesheet paths retain mocked coverage pending paid live smokes. Its durable retrodiffusion:// references refresh temporary result URLs when recovery cannot use the local cache. ComfyUIProvider is an experimental self-hosted adapter. It resolves and hashes an API-format workflow offline, resolves arbitrary per-asset scalar bindings, uploads manifest-owned PNG/JPEG inputs by content hash, submits an input-bound clone, polls local history, and stores portable comfyui:// output references so a lockfile does not retain a workstation hostname or input path. It supports map candidates and atomic frames sets from one still-image output node. Each accepted frame prompt is checkpointed in the lockfile, so a retry resumes at the first unsubmitted frame instead of queuing the whole set again. Frame-set transport, ordered review/fetch, shared-palette refinement, step/phase verification, approval, and packing have deterministic integration coverage. Live Apple MPS runs cover a small Stable Diffusion 1.5 plumbing smoke and the SDXL Base plus Pixel Art XL composition benchmark, including four-candidate review, cache recovery, and native-grid refinement. ScenarioProvider is an experimental hosted still-image adapter that also uploads references for styleImages and image-to-image revisions. It keeps offline planning conservative, records the provider's free submit-time Compute Unit quote, and stores durable scenario:// job and asset references that refresh signed URLs. Authentication and CU preflight have passed live with BFL Flux 2 Dev, along with paid single/two-output jobs, human selection, and provider-backed recovery. Other model schemas remain mocked. FakeProvider implements the same contract in memory, which keeps the pipeline testable without credentials or network access. See library API and provider comparison.