---
name: set-anima-orchestration
description: How Set's scenes load. One job orchestrator runs every piece of loading work, namely API requests, armor builds, outfits, layering, shader compiles, motion bakes, floors, portraits and placements. It shares work by key so nothing is done twice, ranks it by priority so what is on screen goes first, bounds it with slots so the machine stays full without starving the page, joins parallel loads with barriers and ends every job, so no loader spins forever. Use when building a scene or page that loads several things at once, when a loader never finishes, when a page or scene loads slowly, when deciding what should load first, or when reproducing how Set loads its scenes.
license: "Free to use when obtained from set.world, commercially too (the API, what it returns and the skills). Its models, animations and textures, Anima included, are CC BY 4.0: free to use with attribution to Jimmy Lee (j@internet.dev). The source code of Set is © Jimmy Lee under CC BY-NC 4.0 and its commercial use needs a paid waiver."
metadata:
  version: "0.15.2"
  title: Orchestration
---

# Orchestration (how Set loads)

Every Set page that draws Animas loads many things at once: character responses, held weapons and their motions, armor and its cloth, the layering of an outfit, shader programs, the floor, portraits and whole armies. The orchestrator (`set-world-0-15-2-orchestrator-3`, `three-isometric-engine/common/orchestrator.ts`) runs all of it. It makes four promises:

1. **Every loader ends.** A job ends `done`, `failed`, `cancelled` or `stalled`, and whoever waits on it hears that end. No spinner waits forever.
2. **Nothing loads twice.** Work asked for under the same key runs once, and everyone who asked shares it.
3. **What is on screen loads first.** Every job has a priority, and scarce capacity goes to the highest priority waiting.
4. **The machine stays busy without starving the page.** Concurrency slots fill the workers and the main thread, but never past what helps.

It belongs to the scenes ([Scenes](https://set.world/skills/anima-scenes)), not to the Anima System's rules. It never changes an outcome, a seed, a placement order or a number a game reads; it only decides when work runs. [Viewport performance](https://set.world/skills/anima-viewport-performance) decides how much a scene draws; the orchestrator decides in what order everything is prepared.

## The rule behind it: no hand-made waits

Code never waits for work by itself. It never resolves a promise when a flag flips, never polls with frames or timers, never waits "two frames and checks", never puts a fixed timeout on work, never counts finished work by hand to release something and never awaits something that has no end. It waits on a job, a job shared by key, or a barrier.

Some things are not waits, and stay as they are:

- A leaf event that ends work resolves its own promise inside a job: a worker's reply, an image's `onload`, a GPU query result. "The browser has painted" is one shared abortable event (`nextPaint`).
- Pacing: animation and toast lengths, input debounces, the cooperative work slice that yields the main thread every 8 ms and release delays.
- Events may change state, such as a phase or a count on screen, as long as whatever sends the event is itself a job that ends.

## Call it

```ts
import { orchestrator, abortable, JobStalledError } from '@engine/common/orchestrator';

const image = await orchestrator.run('Painting the floor', async (job) => {
  const response = await fetch(url, { signal: job.signal });
  job.progress();
  const painted = await abortable(paintInWorker(await response.arrayBuffer()), job.signal);
  job.progress();
  return painted;
}, { key: `floor-surface:${scale}`, signal: view.signal, priority: 'visible' });
```

Wrap every await that cannot end by itself in `abortable(promise, job.signal)`, such as a GPU compile, a worker reply or an image decode, so that stopping the job runs its cleanup. Call `job.progress()` whenever the work advances.

## Schema

```ts
type JobPriority = 'visible' | 'scene' | 'background';           // in rank order: `visible`, `scene`, `background`
type PrioritySource = JobPriority | (() => JobPriority);           // a function is read live, every time a slot is granted
type JobState = 'running' | 'done' | 'failed' | 'stalled' | 'cancelled';

type Job = {
  signal: AbortSignal;              // aborts when the job is cancelled, stalls or fails
  progress: () => void;             // the job advanced; resets its stall clock
  priority: () => JobPriority;      // the highest priority of everyone following or waiting on it
};

type JobOptions = {
  key?: string;                     // callers with the same key share one run
  signal?: AbortSignal;             // withdraws this caller only
  stallMs?: number;                 // default 20 s
  priority?: PrioritySource;        // default 'scene'
};

type SlotPool = 'dressing' | 'layering' | 'motion-bake';

type JobRecord = {                  // what watch() and report() show
  id: number; key: string | null; label: string; state: JobState; priority: JobPriority;
  startedAt: number; endedAt: number | null; error: string | null;
};

interface Orchestrator {
  run<Value>(label: string, work: (job: Job) => Promise<Value>, options?: JobOptions): Promise<Value>;
  when<Value>(key: string, label: string, options?: Omit<JobOptions, 'key'>): Promise<Value>;
  running(key: string): boolean;
  forget(key: string): void;
  whenIdle(signal: AbortSignal): Promise<void>;
  slot(pool: SlotPool, job: Job): Promise<() => void>;   // resolves with the slot's release
  barrier(label: string, keys: readonly string[], options?: Omit<JobOptions, 'key'>):
    { arrive: (key: string) => void; arrived: () => number; settled: Promise<void> };
  watch(listener: (record: JobRecord) => void): () => void;
  report(): JobRecord[];
}
```

## Every job ends

- A job that reports no progress for its stall time while the page is visible is stopped. Its signal aborts with a `JobStalledError`, everyone waiting on it rejects with that error, and the console names it: `[Set orchestrator] Painting the floor made no progress for 20 s.` The check runs every 1 s. Time with the page hidden never counts.
- Stall times Set uses: 20 s by default; 30 s for every API request (each `GET /api/roll/character`, the home rolls, the craft); 60 s for measuring every held item at a level, one long worker pass; 7 s for the craft stone's fall.
- A caller decides what an end means. Retry it, fall back, or show the failure. Never keep a loader up. Set's portraits draw a stalled portrait again in a fresh renderer, then show it as failed. Its placements remove the half-placed Anima and offer a retry. Its craft stone falls back to its SVG drawing when its artwork is ready, and otherwise reports that the stone could not be prepared.

## Shared by key

- `run` with a `key` that is already running joins that run. Everyone who joined hears its result. Each caller's own `signal` withdraws only that caller, and the job is cancelled when its last caller leaves.
- `when(key)` waits for a keyed job's outcome, whether it is running, has not started yet or has already ended. The last 256 keyed outcomes are remembered; `forget(key)` drops one. A waiter sees the keyed job's progress as its own, so it stalls only if that job does. This replaces readiness passed through callbacks.
- `running(key)` says whether a keyed job is in flight.

The keys Set uses:

| Key | The work it shares |
| --- | --- |
| `armor-build:<garment>` | One garment's fitted cloth and plates, built in a worker; a build waits on the garments beneath it with `when` |
| `layering:<outfit>` | The insets that keep an outfit's layers from passing through each other |
| `weapon-motion:<motion inputs>` | One held item's sampled motion (its action, grip, hold, windup and hand) |
| `floor-surface:<palette and scale>` | One painted floor level |
| `ammo` | The physics engine |
| `craft-stone:<seed>` | The craft stone being ready to show; the craft waits on it with `when` |
| `item-portrait:<slot, gear, material>` | One item's portrait model |
| `item-analysis:<level>` | Every held item measured at one level |
| `fixed-birth:<seed>`, `model-team-births:<content version>` | A showcase page's shared births |
| `scene-prefetch:<placement>` | The next queued Anima on `/anima-scene`, prepared while the current one dresses |
| `preload:<task>` | One idle preload task |

## Priorities: what is on screen goes first

- `visible`: small work on screen, such as character and item portraits, an item view's armor and a portrait's armor.
- `scene`: a scene's own work. This is the default.
- `background`: work nobody is looking at yet, such as prefetching the next Anima, the idle preloader and a scene scrolled off screen.

A priority may be a function, read every time it matters. Set's scenes use `onScreen ? 'scene' : 'background'`, where on screen means the scene's canvas intersects the viewport and its host has not hidden it, so scrolling a battle away drops all its remaining work behind what is on screen without cancelling any of it, and scrolling back raises it again.

A job shared by key, or waited on with `when`, takes the highest priority of everyone following or waiting on it. A portrait that needs a garment queued behind a whole army therefore raises that one build to `visible`.

## Slots: bounded concurrency, by priority

`slot(pool, job)` waits for one of a pool's slots and resolves with its release. Slots go to the waiting job of the highest priority, and among equals to the one that asked first.

| Pool | Slots | What it bounds |
| --- | --- | --- |
| `dressing` | one per reported CPU core beyond 2 (at least 1; 4 cores are assumed where none is reported) | Each Anima placed in a scene: its held items, outfit, layering, compiles. A battle asks to place every soldier at once and the slots decide how many dress together |
| `layering` | 1 | Outfit layering, main-thread work. The main thread is one thread, so running one at a time costs no throughput and lets a `visible` outfit go before a queue of soldiers |
| `motion-bake` | 1 | Sampling a held item's motion |

- **A busy pool is not a stall.** A job waiting for a slot counts every slot that is handed out or freed as progress.
- **A slot never leaks.** A job that ends for any reason releases every slot it holds.
- **No deadlock.** A slot holder never waits for a slot that a waiter could be holding. A scene fixes each placement's turn in the registration order before the placement asks for its slot, and frees the next placement's turn if its slot never comes. All of one scene's placements share one priority, so their slots are granted in placement order. Seeded placement, actor ids and replays are unchanged.

The armor cloth workers keep their own queue of garment builds and serve it by the same priorities. They hand each worker only a few builds at a time, post the waiting build of the highest priority next, and among equals serve whichever wardrobe was served least recently. A portrait's armor never waits behind an army's.

## Barriers: waiting for several parallel loads

`barrier(label, keys)` is a job that ends when every named key has arrived. Each arrival counts as progress, and `arrived()` gives the count to show on screen. Wait on `settled`, and go on whether it resolves or stalls. Never wait on a counter of your own.

- **Battle with armies** waits on a barrier over all its soldiers before combat starts.
- **Adventure mode with party** waits on a barrier over the party's portraits before the maze starts walking, so the first fight never plays beside empty frames.

## Idle: background work waits for the page

`whenIdle(signal)` resolves when no job is running. Set's idle preloader starts, and takes each turn, only then. It never waits a fixed time for the page to settle: page work always goes before background work.

## Diagnose

- `report()` lists every running job with its label, key, state and current priority.
- `watch(listener)` hears every job start and end.
- A stalled job is named in the console with its label and stall time.

## What stays the game's

The orchestrator is the local game layer's, and a game may replace it. A game that loads Set's characters and items its own way should keep the four promises: every loader ends, shared work runs once, visible work goes first, and no wait is hand-made. The priorities, slot counts and stall times above are Set's choices for its own pages. Your game picks its own. None of them touch the API, a seed or a stored birth.

## Build it

1. **Jobs.** Write one runner whose jobs each carry an `AbortSignal`, a progress call and a live priority. Every await inside a job either ends by itself or is raced against the job's signal.
2. **Stalls.** Keep a stall clock per job. Reset it on progress, pause it while the page is hidden, and when it runs out, abort the job and reject everyone waiting on it.
3. **Keys.** Keep a map of running jobs by key. A second caller joins the run. A job is cancelled only when its last caller withdraws. Remember the last outcomes for `when`.
4. **Priorities.** Rank `visible` before `scene` before `background`. A shared job takes the highest priority of its followers and of anyone waiting on its key.
5. **Slots.** Give each pool a capacity. Grant by priority, then by arrival. Count every turnover as progress for waiters, and release a job's slots when it ends.
6. **Barriers and idle.** A barrier is a job that resolves when its last key arrives. Idle is the moment the running set is empty.
7. **Wire every loader through it.** Requests, models, outfits, layering, compiles, motions, floors, portraits and placements all go through the runner, with the priorities and keys in the tables above.
