Skip to content

kithara host

Pavel Litvinenko edited this page Sep 10, 2026 · 2 revisions

kithara-host

Documentation reviewed from source revision 19ca073f2. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.

Ownership

The Host owns the session root sync group and its member values, the shared Firewheel graph, session transport, mix tap, output limiter, and native or web audio backend. It also owns TransportEvent, the committed tempo, play-state, seek, and failure facts published by that transport. The graph registry is only a projection of Host-owned members; it must not become a second mutable synchronization topology. On wasm the main-thread member owns the sendable synchronization state and desired level; the remote Worker Host retains the Queue runtime and its worker-bound JS resources until explicit removal.

kithara-play owns one player/deck, its render node, effects, fades, worker, and playback flow. kithara-warp owns synchronization and warp contracts. The dependency direction is kithara-host -> kithara-play and kithara-host -> kithara-warp; neither lower crate may depend on the Host.

Offline rendering

Host is the sole composition root for both realtime and offline sessions. HostConfig<S> directly selects the platform device backend or offline renderer; there is no nested session config or parallel offline Host type. The offline mode owns the same root group, Firewheel graph, limiter, transport, and member insertion path as realtime. Only its backend and scheduler differ. The backend starts lazily on the first render when no player has started it yet, so an empty master mix is valid silence rather than an unavailable graph.

Its renderer consumes one absolute finite frame range at a time. It may skip forward by rendering undisclosed frames, never rewinds an already consumed timeline, and writes only the requested range to RenderSink in blocks bounded by the offline HostConfig::max_block_frames. The same config supplies the composition root's PoolRegion<S>; each block is acquired from its f32 pool and returned after the sink call, preserving the shared hard budget. Signal-format mismatch, cancellation, backend failure, and sink failure are terminal for that request. The composition owner finalizes the sink on success and drops it on error; Host does not own storage or encoding.

The offline session is one kithara-worker task. Worker, dispatcher, and task budgets come from the offline Host config; rendering is unpaced by default and runs as fast as the caller requests. Optional pacing is probe-only and advances the same canonical cursor, so deterministic finite capture must not combine it with explicit render requests.

Runtime boundary

The selected session runtime is the canonical Host actuator. Realtime uses the platform session owner; offline uses one kithara-worker task. It owns the root group, member values, Firewheel context, graph commands, and offline cursor. Closing a player remains a caller-side two-phase operation: close the runtime first, then detach the member. Calling close from the owner loop could dispatch back into that same loop and deadlock.

Terminal shutdown first drops the command receiver, then drops session state, then acknowledges the Host. This order disconnects queued callers before Player destruction takes the per-player admission gate, so an admitted command cannot wait on the owner while the owner waits on that command's gate.

A HostOwned endpoint contains the canonical member identity and the player's cloneable control capability. It does not expose or own the inner Player, retain players through Arc or Weak, or introduce a second command route. The wasm remote Host's resident registry only retains Worker-bound runtime values; membership remains canonical in the main-thread topology. The immutable RootView publishes grid, topology, and status observations; it is a read-only projection, never another mutable root.

Worker teardown attempts explicit removal once. Success detaches and releases the resident; SessionGone ends teardown without a retry because the canonical owner no longer exists. Any remote Host that still has resident entries when it drops logs the invariant failure and retains only those players. It neither retries a potentially permanent failure nor drops runtime state while a main-thread topology may still reference its member.

Host::insert accepts one fully configured Player or decorator instance. The instance already owns its stable grid identity. Insertion attaches an opaque SessionBinding exactly once. Native transfers the instance into the Host root. Wasm transfers only its synchronization state and current desired level to the main-thread root, while the remote Worker Host retains the resident instance. The session graph copies that level when the player registers and owns later graph actuation. Decorators only delegate the binding and ownership split to their resident Player. Neither a config builder nor a raw SessionDispatcher crosses the insertion API.

Native and web dispatch wrap lower Player commands and Host topology commands in one private envelope. On web, the main-thread Host and its Worker facade use that same envelope and shared RootView; the Worker is never given a raw SessionHandle with which to construct an unattached player.

The envelope is typed by the composition root's pool schema. Host<S> accepts only PlayerControlSource<Schema = S>, and each graph deck retains the same PoolRegion<S> handle carried by the player's registration command.

On web, one local Host is exclusive per JavaScript thread. It owns the session state and every remote command receiver; TLS retains only a nongeneric active Host flag. Sender and receiver wrappers are capabilities, not owners. Host shutdown drops those receivers before session state so queued reply channels disconnect and a Worker call cannot wait forever. A replacement Host starts with cleared bridge playback observations.

If a command cannot be sent, ownership has not transferred and the original operation is rejected as owner-unavailable. If the sole owner thread stops after accepting an ownership-bearing command, commit state is unknowable. That single post-send boundary deliberately fails fast instead of fabricating a rejection or fallback state.

Route and sample-rate invariant

The selected HostConfig mode is the only product default for the initial output sample rate. Its realtime variant also owns the optional native output block override; leaving it unset preserves the backend default. Each inserted Player is built for an explicit initial rate and the one-shot SessionBinding carries only the canonical Host dispatcher. Insertion queries that session and rejects a mismatch before the Player can register or start. Session register/start commands derive their rate from the same query rather than accepting another caller-supplied value.

Web warm-up creates the AudioContext at the session's configured rate, the same rate every Player is built for, so the session-rate query answers with a rate insertion can validate against. The rate the device settles on reaches the resampler as the measured rate.

Route changes keep the existing flow: the Host observes the device change and the Player receives the resulting sample-rate update through its current control path. Rebuilding the output graph is reserved for a physical route change. Decode resampling, the Stream-to-decode hook, worker scheduling, fades, and playback semantics are not redesigned by this extraction.

Clone this wiki locally