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, plusraw(the provider's bytes' hash) when post-processing changed them;postprocess: whatfetchdid 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 everyplan: a difference isrecoverable, neverstale, because no provider work is involved;terrainSettings,terrainentries only: the settings the request was sent with thatspecHashleaves 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, soplancompares this record instead and reportsstalenaming 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 providerpollnever 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_HISTORYor the manifest'shistorycaps 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:
- Recover an interrupted prior transaction.
- Validate provenance ownership and manual edits.
- Compare every desired member and skip identical bytes.
- Write an immutable transaction journal.
- Stage all changing members beside their destinations.
- Move existing members to unique backups.
- Promote every stage.
- Write a durable commit marker.
- 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.
