Repository

Core and the app

This repository holds Kinogaki Studio's core and its application.

GitHub
kinogaki/studio-core (private)
Folder
studio/
Crates
23
Rust lines
120,373 in src, 46,243 in tests
Uses from other repositories This repository Used by other repositories lib-color2 crates lib-image1 crate lib-math1 crate lib-parallel1 crate lib-prism1 crate lib-script4 crates lib-ui5 crates lib-viewport3 crates SDK9 crates Studio's animation channels.anim492 lines B-reps as polygons, below the viewport, renders and file exchange alike: each body's mesh by the installed mesher, cached on its geometry ([`meshed`]), and a value's bodies joined into one polygons value ([`joined`]): each body's mesh, the faces' polygons ...brep-mesh466 lines A fresh id each time the crate naming it is compiled.build-id25 lines Compares two sets of UI reference captures (`tools/ui_captures.py compare BEFORE AFTER`).capture-diff357 lines Studio's color service: the OpenColorIO config (lib-color's built-in ACES 2.0 CG config, or a file), the working space, and the viewer's display, view, look, exposure and gamma.color-service991 lines Studio's core cooking engine, the same for every network kind (the owner's rule: the cooking DAG for nodes is core's).cook2.8k lines Core's own SDK package (`kinogaki.core`): what Studio holds whatever plugins are installed.core-package5.5k lines Studio's document: the scene, a tree of objects under one root, and the core network each object of a kind naming a context holds.document8.5k lines Kinogaki Studio's editor: the document and everything the window shows of it, as parts one frame borrows one by one.editor36k lines Geometry, curves and B-reps in the viewport, independent of any GPU API: the SDK's standard values as surface packets (over the SDK's display triangulation and shading normals, so Solid draws what Render renders), the one picker that answers rays and boxes ...geometry-view9.9k lines Studio's test harness: the real editor, its event routing and its background work, driven offscreen with only the packages a test names.harness1.6k lines `ImagePipeline`: what renders become on screen and in files.image-pipeline7.2k lines Studio's core networks (design: core-owned networks; "Cooking DAG for nodes is studio core").network11k lines Studio's plugin loader.plugin-host1.2k lines Studio's project files: a document as a Prism document.project6.0k lines The render side of Studio's standard geometry values: what render engines receive for them, whatever kernel made them.render-geometry880 lines Studio's scene: the document cooked object by object.scene8.7k lines Scripts over the standard values: lib-script's engine behind the SDK's [`ScriptEngine`], and what a script sees of each value.scripting2.5k lines The plugin SDK's UI layer over lib-ui: the SDK's [`Ui`](studio_ui_api::Ui) and [`Painter`](studio_ui_api::Painter), its sheets and the host's services, implemented over lib-ui.sdk-ui3.3k lines The crates Studio and its plugins share, built once as a Rust dylib.shared55 lines Timed spans of Studio's own work: where a frame, a cook or a render sync spends its time, without an instrumented build.spans208 lines Kinogaki Studio: the editor in a window, after the plugins load.studio271 lines Kinogaki Studio's core testkit: toy packages written against the SDK alone, so core is tested with no real plugin.testkit12k lines Comp1 crate OpenEXR1 crate FBX1 crate glTF1 crate JPEG1 crate Layout1 crate MaterialX1 crate Mesh2 crates OBJ1 crate Pixel1 crate PNG1 crate Spectre1 crate USD1 crate
depends on (an edge implied by others shows when you point at a box)used by tests alone

Crates

CrateWhat it isFilesLinesTest lines
kinogaki-animStudio's animation channels.4492663
kinogaki-brep-meshB-reps as polygons, below the viewport, renders and file exchange alike: each body's mesh by the installed mesher, cached on its geometry (meshed), and a value's bodies joined into one polygons value (joined): each body's mesh, the faces' polygons ...24660
kinogaki-build-idA fresh id each time the crate naming it is compiled.1250
kinogaki-capture-diffCompares two sets of UI reference captures (tools/ui_captures.py compare BEFORE AFTER).23570
kinogaki-color-serviceStudio's color service: the OpenColorIO config (lib-color's built-in ACES 2.0 CG config, or a file), the working space, and the viewer's display, view, look, exposure and gamma.299140
kinogaki-cookStudio's core cooking engine, the same for every network kind (the owner's rule: the cooking DAG for nodes is core's).112,831190
kinogaki-core-packageCore's own SDK package (kinogaki.core): what Studio holds whatever plugins are installed.255,4980
kinogaki-documentStudio's document: the scene, a tree of objects under one root, and the core network each object of a kind naming a context holds.438,4813,272
kinogaki-editorKinogaki Studio's editor: the document and everything the window shows of it, as parts one frame borrows one by one.13635,98517,392
kinogaki-geometry-viewGeometry, curves and B-reps in the viewport, independent of any GPU API: the SDK's standard values as surface packets (over the SDK's display triangulation and shading normals, so Solid draws what Render renders), the one picker that answers rays and boxes ...359,9333,580
kinogaki-harnessStudio's test harness: the real editor, its event routing and its background work, driven offscreen with only the packages a test names.111,5860
kinogaki-image-pipelineImagePipeline: what renders become on screen and in files.247,2120
kinogaki-networkStudio's core networks (design: core-owned networks; "Cooking DAG for nodes is studio core").4311,3905,138
kinogaki-plugin-hostStudio's plugin loader.81,2080
kinogaki-projectStudio's project files: a document as a Prism document.286,0385,470
kinogaki-render-geometryThe render side of Studio's standard geometry values: what render engines receive for them, whatever kernel made them.5880441
kinogaki-sceneStudio's scene: the document cooked object by object.328,6725,514
kinogaki-scriptingScripts over the standard values: lib-script's engine behind the SDK's ScriptEngine, and what a script sees of each value.112,539736
kinogaki-sdk-uiThe plugin SDK's UI layer over lib-ui: the SDK's Ui and Painter, its sheets and the host's services, implemented over lib-ui.143,2633,481
kinogaki-sharedThe crates Studio and its plugins share, built once as a Rust dylib.1550
kinogaki-spansTimed spans of Studio's own work: where a frame, a cook or a render sync spends its time, without an instrumented build.12080
kinogaki-studioKinogaki Studio: the editor in a window, after the plugins load.2271326
kinogaki-testkitKinogaki Studio's core testkit: toy packages written against the SDK alone, so core is tested with no real plugin.4811,9920

README

Kinogaki Studio: core and application

This repository holds Kinogaki Studio's core and its application. It is one part of the Kinogaki Studio tree and builds only inside it: clone the root repository, kinogaki/studio, and follow its docs/SETUP.md.

PathWhat
core/The core crates: networks, cooking, the document and undo, animation, .kino projects, the scene cook and render sync, the plugin loader and the kit (kinogaki-shared), the color and scripting services, the editor, the harness that drives it offscreen, and the testkit's toy packages
apps/studioThe application, kinogaki-studio: the editor in a window after the plugins load, or drawn headless with --screenshot
examples/scenes, examples/wranglesExample projects; the format tests open and save each one
docs/How core works: ARCHITECTURE.md, KINO-FORMAT.md, NETWORK-LEVELS.md, NODE-PATHS.md, ASSETS.md, ATTRIBUTES.md, CAMERAS-LIGHTS.md, MATERIALS.md, RENDERING.md, COLOR.md, CANVAS.md, LIVE-PIPELINE.md, WRANGLE.md

With no plugins, the application is the editor, networks, undo, files, Parts, Cameras, an empty Material Library and a File node that reads nothing. Everything else comes from plugins, each its own repository under the root's plugins/.

Conventions for working here: AGENTS.md.

Its documents

The repository's architecture document as written there (code blocks and links left out), and the opening of each other document.

Core architecture (studio/docs/ARCHITECTURE.md)

Core architecture

How core and the application are built: the crates of studio/core and studio/apps/studio, and the rules they keep. Where every repository sits is the root's LAYOUT.md; the plugin SDK is described in the SDK's ARCHITECTURE.md; the owner's rulings are the root's DECISIONS.md.

Crates

  • core/network: the network every context shares, core's one node shape, the compiled topology and walker, the registry of installed packages.
  • core/cook: scheduling, workers, wakes, node caches.
  • core/document: the scene, undo, where the user is, picks, slots, node paths.
  • core/anim: animation channels.
  • core/project: .kino files over the SDK's codecs (KINO-FORMAT.md).
  • core/scene: the document cooked object by object, the results board, the render sync.
  • core/image-pipeline: what renders become on screen and in files: the installed engines, the live pipeline, Comp plans, the display, finals and swatches (LIVE-PIPELINE.md).
  • core/render-geometry, core/geometry-view: the SDK's standard geometry values for render engines and for the viewport; core/brep-mesh: B-reps meshed for display and exchange.
  • core/package: core's own package, kinogaki.core (the standard types, File, Camera, Materials, the Comp context).
  • core/color: the color service over lib-color (COLOR.md); core/scripting: scripts over the standard values, lib-script behind the SDK's ScriptEngine (WRANGLE.md).
  • core/plugin-host: the plugin loader; core/shared: the kit, kinogaki-shared; core/build-id: the id of each compile of it.
  • core/sdk-ui: the SDK's UI layer over lib-ui; core/editor: the editor's parts; core/harness: the editor driven offscreen; core/testkit: toy packages on the SDK alone; core/capture-diff: pixel comparison of captures.
  • core/spans (kinogaki-spans): timed spans of core's own work (the editor's frame and its sections, the cook and its preparations, the geometry's preparations and refinements, the render sync), off unless KINOGAKI_SPANS is set or a measurement turns them on, a branch each when off; what they recorded a measurement prints as MEASURE lines, and the latency workloads add each span's median to their LATENCY line.
  • apps/studio (kinogaki-studio): the editor in a window after the plugins load, or drawn headless with --screenshot.

Core names no domain. Core owns the geometric values (the SDK's kinogaki-geometry: points, vertices, edges, polygons, polylines, curves, B-reps, attributes, groups); kernels bring the operations and the maths over them; plugins bring the tools and nodes. What core and a kernel agree on when a value is selected and changed is the SDK's kinogaki-edit: selection rules over a value's Incidence and the levels' modes (select), a move as data that a drag's preview and its commit both place points with (deform::Deform), a drag's moving points (preview::Moving), and the editor every stored value changes through (editor::Editor), so edit policy never ends up in the data crate. A kernel is a package of verbs over the standard values; it depends on kinogaki-studio-api, which carries the verbs and re-exports both crates.

Core is tested through core/testkit's toy packages, and the plugin loader through the SDK's example plugins built against the kit and tiny test libraries. The application before the rewrite is archived under the annotated tag donor-archive in this repository, a read-only reference (git show donor-archive:donor/<path>).

Each arrow is a direct dependency between core crates; one a crate also reaches through another is left out (the editor depends on the document directly too). The graph leaves out spans, which the editor, the harness, the scene and geometry-view depend on and which depends on nothing. Every core crate but capture-diff, build-id and spans also depends on the SDK (kinogaki-studio-api), and most on libraries (lib-ui and lib-viewport under the editor and sdk-ui, lib-parallel under cook, lib-color under color, lib-script under scripting); the graph leaves those edges out. Plugins depend on the SDK and the kit (core/shared), and on core's testkit and harness only in their tests.

The root's tools/check_architecture.py enforces the layers by zones (tools/architecture/zones.toml): every crate is core (studio/core), the app (studio/apps), a plugin, a library (kernel or environment library) or the SDK. Core never depends on a plugin or a kernel: its dependency closure, dev-dependencies included, holds only core, the SDK and the environment libraries (lib-ui, lib-viewport, lib-math, lib-color, lib-parallel, lib-prism, lib-svg, lib-misc-utils, lib-script, lib-render's spectra kinogaki-spectra, and lib-image's PNG for developers' reference images and captures), so a new kernel fails by itself. The app is held to core's rules: it loads plugins at run time, never by link. Plugins depend on the SDK, the kit, their own crates and their own kernels (core's testkit and harness as dev-dependencies alone). The SDK depends only on its own crates. --empty-env builds and tests core with nothing else present. Libraries never depend on Studio and reach only the libraries tools/architecture/libraries.toml names; crates outside its short UI/GPU list have no UI or GPU dependency. Every repository checked out is built and checked. Owned code forbids unsafe (outside the reviewed bindings tools/architecture/libraries.toml lists: dialogs, lib-ui's shell, the plugin loader's sys.rs, the Metal bindings); the frozen third-party crates and graphics drivers still contain unsafe implementations. Memory safety is not a claim of numerical correctness or crash immunity. Viewport packets and rendering are described in libs/lib-viewport/docs/ARCHITECTURE.md, UI widgets in libs/lib-ui/docs/ARCHITECTURE.md.

Evaluation and scheduling

Core cooking. The cooking DAG of every network is core's (DECISIONS.md: cooking and caching are core services). core/cook (kinogaki-cook) is the cooking engine every network kind uses; it depends on lib-parallel and the SDK alone (no kernel, package or UI), and network kinds plug into it:

  • Evaluation order (dag::order): each node after what it reads, a depth-first post-order from the targets with inputs in wire order. Every network's order (Network::cook_order, topological) is core's; the network says what an item reads (a node only the wires it will read, Node::reads_wire).
  • The per-node cache (NodeCache<K, E>): one entry a node, overwritten by its next cook so drags never grow it; recency on one cook clock for the whole process (kinogaki_cook::clock), so entries of different caches compare; eviction is core's memory budget (kinogaki_network::budget, below); and each node's scratch slot, state that only speeds its next cook, kept across entry replacements and dropped with the node. The network kind decides reuse (NodeCache::reuse with its comparison). A slot also keeps the node's frames: in playback (WalkCx::frames, the host's SceneCook::set_playing) a node the time moves keeps the result it showed by its stamp, and any walk whose stamp for the node has a frame takes it without cooking, so a played range plays again and scrubs back without cooking. Only nodes the time moves get frames; scrubs and drags take them but keep none; a node whose stamp moves at an unchanged time (an edit) drops its own, and so does what it feeds.
  • The memory budget (kinogaki_network::budget::Tally::keep_within): after every request the scene cook keeps all objects' caches (walked insides included) within the request's budget (SceneRequest, from the user's cacheBudget setting, a quarter of physical memory by default). Its tally keeps what each object's cache held when last counted and how many caches hold each value, so a request counts again only the caches cooked or evicted from since (an unchanged scene looks at no value). What each object used since its last walk of what it shows, or in this request, is needed and never evicted; everything else goes least recently used first, then frames newest-kept first (a looped range larger than the budget keeps its first part rather than evicting each frame just before the loop returns to it), and recooks when it is needed again. Each frame's bytes are counted once, when it is kept, and each cache keeps its frames' total, so a pass that fits looks at no frame; once frames go, the playback keeps no more until it plays again. It is a ceiling: drags and scrubs overwrite their slots and add no entries, and playback adds at most a frame per state a node reaches in the range. A new document (a generation of its own, SceneRequest::document; SceneCook::reset also empties the board, so nothing of the old document shows, picks or freezes) starts every object afresh, so the old one's caches and frames go even where ids repeat; a look outside keeps them.
  • Workers (latest): LatestTaskService, one running request, one replaceable pending one, one result; cancellation (lib-parallel's CancelToken), continuing gestures (submit_after), idle refinement (with_idle, held during gestures), stuck work seen and abandoned.
  • Lanes (Lanes<K, R, O, F>): one such worker per key, asked only when what it reads (F) changed; a stuck lane gets a fresh worker and leaves the old thread behind, never joined. The editor gives packages one lane per package and key for their own latest-wins jobs (Cx::latest, core/editor/src/cook/latest.rs).
  • The scene service (objects::SceneService<C: ObjectsCook>): every object of a scene on one worker: request numbers, which objects are cooking (marks set before they cook, waking the UI once one turns busy), idle refinement rounds of provisional results and settling back onto what the application showed (refinement_shown), and objects::run: sources first, the open object next, the rest in parallel, duplicates showing one network with the same inputs cooked once.
  • Held edits (Held<B, R>): a held node's base, captured by the first cook that reaches it, and its result, filled by the first cook to finish and shared by every copy of the held state (undo snapshots, the worker's), so undo shows the same allocation again and an obsolete cook can't publish into an adjusted state. nodes::held runs a package's HeldEdit on Held<Captured, Applied>. Edit Mesh is one (plugins/mesh/src/nodes/edit_mesh: its steps as the delta kinogaki.mesh.steps, replayed over the captured input).
  • Packages' node types (nodes): a node type cooked through the SDK contract (NodeType::cook, CookCx): its entry reused while its parameters, its inputs (by allocation), what it declared it reads with those parameters (Reads: time, input names, selection; asked again only when the parameters change) and the outputs something reads (CookCx::wants) are unchanged, what it didn't declare withheld, and its scratch slot lent from the cache. Node types describe themselves (NodeType::description: label, ports with their data types, the parameter schema). Mesh's tools are node types (plugins/mesh/src/nodes, each made from an operation in plugins/mesh/src/ops). A node of a new type cooks anew; a newly read extra output cooks with the outputs made before, which keep their allocations. Every parallel step of the process (packages' and the standard values' own) runs on lib-parallel's shared helpers: kinogaki_cook::workers::install installs them as the SDK's geometry::parallel::Workers when the app starts, and the testkit does the same.
  • Wakes (Wake): one wake per published result, the count of services with work under way (quiet, tests only); the UI never polls.

The scene cook (kinogaki_scene::objects, over core/network's walks; the project nodes cook in is kinogaki_network::project): every object of the host's scene (CookScene) holding a core network whose context cooks, on one SceneService (CookService), each with its own node cache (NetCache), so editing one object recooks that one. An object's answer (ObjectCook) is SDK data: the node's outputs, what the object shows of them, every node's warnings, every node's outputs with the stamp each cooked at (Cooked, shared with the walk's cache), and the pieces the shown value's data type drew (Displayable::prepare on the cook's workers, Displayable::refine as idle work while no gesture holds it). What a value draws of the values it holds (Instances, their prototypes) goes through one table of the cook's (HeldValues, the SDK's Held): a prototype many objects place is drawn once, its packets the same allocations for each, so the views upload it once and draw it instanced for every object, each in its own placements, colors and marks; an entry goes once no object's answer holds it. Nothing there names a context or a kernel: every network walks with core's CoreWalk, reading other objects through its path rows, each with where that object is placed in the reader's space at the request's time (CookScene::placement, CookCx::placement, stamped with what it reads); the nodes those rows read (ScenePaths::sources) cook before their readers.

The editor's wiring. kinogaki_scene::SceneCook drives that service from the document: it submits the document when what the cook reads changed and adopts the answers without waiting on the worker (submit, adopt, hold_refinement during gestures). The editor's CookState (core/editor/src/cook/mod.rs) owns it. Editor::poll (frame.rs) runs at the start of a frame: it adopts a finished open or save, adopts the cook's answer (a failure goes to the status line), runs each package's poll, and submits again when anything the cook reads changed (the document's revision, where the user is, the time, the files outside). Each result wakes the window once. The views render after every pane and control of the frame ran (ViewState::render), so what a control changed shows in the same frame; a frame that only moved a camera rebuilds nothing (view/drawn.rs) and writes that view's uniforms alone (view/gpu.rs).

Cancellation is cooperative. Some algorithms, triangulation, and BVH construction still have coarse checkpoints. A bounded queue prevents a backlog but does not guarantee shutdown or cancellation latency. Parallel geometry execution, adaptive budgets, and upload staging should be driven by measurements, not added by default.

Transactions and storage

The document (core/document, kinogaki-document): the scene with its frame range, undo history and gestures, where the user is (Location), the selections and the packages' session slots. Document::transact (document/transaction.rs) is the one way the scene changes: an edit changes a copy of the scene (an object is copied only when the edit changes it, its network only when the edit changes that); when it returns, the copy is checked by what it changed (Scene::validate_changes) and against the objects locked before (Scene::check_guarded), and only then replaces the scene. A failed edit or check changes nothing; an edit that changes nothing records no step.

Undo (document/history.rs): a Snapshot shares the scene's objects, bodies, networks and the slots' values, so it costs a map of pointers. A step is the snapshot before and after one edit. A gesture (a drag, begin_gesture) is one step: its edits take no snapshot of their own and its end checks the networks it changed once. It is its opener's: begin_gesture gives the GestureToken that alone finishes or cancels it, and none while another's gesture is open, so a drag begun inside a live tool's run joins it (the editor's gesture::Held: its end leaves the tool's gesture open, Escape goes back_to the mark where it joined). back_to(token.start()) goes back to its start and keeps it open. Selections are no steps; undo restores the selection its step came with. The history keeps the last 128 steps within a soft 64 MB, and always the newest.

Locations, picks and slots. Where the user is is one Location (document/location.rs): an object level, or a level of the network an object holds, or of the network a node of it holds (Description::inside, Houdini's matnet); going up, the breadcrumb and keeping the place valid after an undo are one behavior for every context. One NodePicks (document/picks.rs) holds what is picked on whichever level is open; picking is navigation, but undo restores the picks an edit came with. A package's own session state is a slot (document/slots.rs): a value type implementing the SDK's Slot, registered with its package, held in the SDK's copy-on-write tables (one for the undoable slots, which snapshots keep, one for the rest); after a restore or an edit each slot may bring its value back in line (Slot::restored, reading a SlotDoc). The components selected on an object, whatever its context, are core's own slot (core/package/src/component_selection.rs): a pick is an undo step, read and set through the SDK's HostSelection (selected_components, select_components); the open context's levels decide what a pick selects and check that a set fits the value it shows.

Repeat is core's for every context (core/editor/src/commands/repeat.rs): running an item its library marks repeatable (Places::repeatable) over the views makes it the item its vocabulary's Repeat (REPEAT) runs again, through that vocabulary as Invoked::Repeat, with its last values where the context keeps them; Repeat is named after it ("Repeat Bevel"). The record outlives a new document. Tools remember their settings (commands/tools.rs, the SDK's HostTools): a tool starts from its type's declared defaults with its remembered parameters' last values (Parameter::remembered says which), saved with the user's settings.

Projects (core/project, kinogaki-project) are Prism documents (lib-prism kinogaki-prism). A Project is its scene, its time, the view it saves and what it keeps of what no installed package reads (Kept). .kino files hold a binary Prism crate; saving to a .prisma path writes the same document as diffable text; loading picks the decoder from the content's magic, never the extension. The schema (core/project/src/schema/; its layout, naming table and strictness in KINO-FORMAT.md): /project carries the kinogaki.studio format metadata, settings (units, up axis, grid, precision and the packages' stored values, settings.rs), time (current, start, end in seconds, fps) and view (cameras, the network's pan and zoom, view.rs); /scene holds the object tree (scene.rs): each object's element core's, its body its kind's body child written by its ObjectCodec (objects.rs), then the network it holds, its network child, through core's one node codec (node.rs, levels.rs); held deltas and kept outputs go through their data type's DataCodec. Everything numeric is a typed value; names a user types are metadata strings. Opening and saving run on a worker (ProjectTask, task.rs): the UI keeps drawing, the worker reports Progress and wakes the UI with the result; a save is atomic (file.rs: a sibling temporary file, synced, then renamed over the target), and an open replaces nothing until it is adopted.

Object bodies, codecs and kept content

An object (kinogaki_document::Object) is a name unique beside its siblings, a transform and pivot, the outliner's flags, its display quality (Option<DisplayQuality>: how finely it shows when its display is tessellated, None for the project's display_quality; Scene::quality resolves it, and the scene cook and render views mesh at Detail::Viewport of it), a tile position at its object level, its children and its body (object/body.rs, Body): core's Part, a package's ObjectBody (Body::Package(Arc<dyn ObjectBody>)) or a Placeholder (an object no installed package reads). The document names no kind but the Part. A kind naming a network context also holds a CoreNetwork beside its body, never in it (Object::network, network_mut): core makes it with the object, copies it on edit, compares it by allocation and saves it. What the document needs of a kind (whether its objects are placed, stay at the root, show an object level of their own, hold a network and of which context) is a Kind (kinds.rs, kinds::kind), read from the SDK's ObjectKindSpec the installed packages declare, so code asks a kind instead of matching on ids.

Installing a package. What is installed is core/network's registry (kinogaki_network::registry::install): one install per package, admitted by the SDK's Registry checks (vendor, duplicate ids, data types it names, spare names, compilers), adds its network contexts, node types (each description read once), held edits and data types, and with them its object kinds, stored values, slots and formats, which the document and the project read. A refused package installs nothing and the caller reports why. The table only grows: packages are never unloaded. It starts empty: core's own package (kinogaki.core, core/package: the standard data types, the File and Stash nodes, Camera and Instances objects, Material Libraries and nodes, core's stored values and its Comp, Shading and Materials contexts) is installed by the host as it starts (kinogaki_core_package::install, beside kinogaki_cook::workers::install, before any plugin's package), and by the testkit's packages for tests.

Content no installed package reads is kept as read and written back (owner, 2026-10-30: "we should load nodes and show them as grey if they're unknown and retain what we loaded so that when we save it we don't kill information"). Opening and saving a project with a plugin missing loses nothing:

  • A core node whose type no installed package provides, or holding what one doesn't, loads as a placeholder (kinogaki_network::Placeholder): its element and everything below it are kept as read (kinogaki_network::Verbatim), its ports are the slots its wires use, its parameters show read-only, and its cook fails with "Needs <package>", so nothing downstream cooks. It draws grey on the network pane and can be moved, wired around, flagged and deleted. A network of a context no installed package declares loads with its nodes as placeholders (CoreNetwork::stored_as).
  • An object element of a kind no installed package declares loads as a placeholder object (Body::Placeholder): its element and body (its network included) are kept as read, its child objects load as their own; it shows muted in the outliner and can be renamed, moved in the tree and deleted.
  • What is kept holds what it shares with the rest of the file (kinogaki_network::Verbatim): a shared: reference to an element inside it moves with it, and one to an element elsewhere keeps a copy of that element, written where the reference was when the file written no longer holds it there. So deleting or moving what first wrote a shared value while the plugin is missing leaves the project opening once it is back.
  • A stored value of a package that isn't installed, and an asset definition of a context none declares, are kept as read (kinogaki_project::Kept, kept.rs) and written back in their place: core writes the children of the settings, a pane and the assets in name order. Anything else in core's own elements that core doesn't read (a field, metadata, a child, keys on a field that isn't animated) refuses the file, named with its element: every reader takes what it reads (core/project/src/taken.rs) and what nothing took is refused.
  • A file says everything about what it holds (KINO-FORMAT.md, "What a file doesn't hold"): a declared parameter (or a spare its values declare) a node's element doesn't hold, or a field a codec reads, is refused like an unknown one; nothing missing takes a default. A package's stored value the file doesn't hold at all (the package wasn't installed when it was saved) starts at its defaults and the load says so (LoadReport::defaulted); one of core's is refused. A value its parameter doesn't take loads as stored, is named (LoadReport::misfit) and fails its node's cook until fixed.
  • Readers report each kept item to the load's LoadReport (kinogaki_project::report: keep, keep_for), never an error (an error fails the whole load). The editor's open path writes the report to the Console as information, one line per missing package ("Couldn't load 3 nodes and 1 object: acme.trees isn't installed"): a known state, not an error. Its problems (a value its parameter doesn't take, a value the file doesn't hold) are errors.

The results board and data types

Core's surfaces reach objects' results only through the board (kinogaki_scene::Board, core/scene/src/board.rs) and the data types of the values on it. The board holds each object's answer (ObjectCook: its outputs, what it shows, warnings and stamps, the pieces its data type drew), why one failed (CookFailure), and the nodes asked for besides (node, focus: the inspected node's input, the spreadsheet's pin). Data types are registered with their packages (Package::data_types, through the network registry) and come with the SDK's facets: Displayable (levels, pieces as viewport packets, tallies), ObjectPicking, Look, Snappable, Tabular, Renderable. Core's standard values (polygons, curves, B-reps, the SDK's kinogaki-geometry) have their viewport side in core/geometry-view (surface packets over the SDK's display triangulation, object picks and snapping through the packet's one picker, drag previews, Shaded looks). The views pick components through the open object's own piece's picker (lib-viewport's Picker, kept with the packet), which the scene cook makes with the pieces it draws where the scene rests (refit from the last one's when points moved, built for a new topology), never on the UI thread; a box's release reads back the faces the GPU drew over it and their render side in core/render-geometry (GeometrySource, GeometryRenderable).

  • Drawing without per-frame conversion. What the views draw (core/editor/src/view/drawn.rs) is every shown object's pieces from the board, each placed by its object's world, in the look its role, its flags and the selection give it. The list is kept while nothing it reads changes (the board's answers, the placements, where the user is, the selection, the hover), so a frame that only moves a camera builds nothing and the renderer sees the same packets. Inside an object whose context has levels, the object's own piece is the edited surface: the views draw its component selection and hover on it, with what the open context's viewport layer adds (view/edited.rs). Pieces its context hides (Edited::pieces, a print's later layers) are held on the GPU while that value is the one shown, so they show again without an upload (drawn::held, lib-viewport's Scene::held). F frames the selected objects at an object level; inside an object it frames the open value: its context's selection as its levels give the pivot, else everything the shown value draws (Editor::frame_open), wherever it lies from the scene's origin.
  • Surfaces. Object picking, hover and the marquee at an object level (view/pick.rs; core's rule for every kind, as in Maya and Houdini: a box takes every object it touches, whichever way it is dragged, drawn as one solid rectangle, each kind answering through ObjectPicking::in_box and its Marquee), component picks of the open value (view/components.rs, handed to the context), framing and the status line read the board. The Spreadsheet (panes/spreadsheet.rs) draws the Tabular tables of the open object's result in a network, else the active object's; only the rows on screen are read.

Units and up axis

One unit of geometry is one project unit. A project's settings (kinogaki_document::ProjectSettings on the scene, so a change is one undo step) hold its Units (the SDK's: which axis is up, Y or Z, and metres per unit), the ground grid's spacing and subdivisions, how many decimals lengths show, and the installed packages' stored values of the project (ProjectSettings::stored, each package's own to draw). Changing the unit reinterprets the numbers, as Houdini's unit length does; Maya and Fusion keep physical sizes by converting instead. Nothing is rescaled silently. Changing the up axis turns the ground, the views and new primitives; geometry stays where it is.

Conversion happens only at the edges: files read into a project convert their lengths into its units, through the format's own import options (the formats installed packages register, looked up by kinogaki_network::Formats: by data type, then by extension, then by first bytes). Lengths show and parse through kinogaki_properties::numeric only (Unit::length stores and shows one unit, other suffixes convert; 25.4 mm is exactly 1 in); the SDK UI layer hands the project's unit and decimals to sheets (core/sdk-ui/src/units.rs). lib-viewport's Camera::ground (Ground) orbits about the up axis, picks the grid plane and steps the grid; every camera takes the project's ground each frame. Saved nodes keep their values.

Node types and the registry

Every node type comes from a package and is known by a declared id, <vendor>.<package>.<node> (kinogaki.mesh.extrude). The network registry (kinogaki_network::registry) holds each installed node type's description (label, contexts, section and tile category, family, aliases, ports, parameters by name, flags), the contexts and object kinds each package provides, and the file formats it reads. Packages install in dependency order (plugins load their dependencies first, kinogaki_plugin_host::resolve); a package whose dependency is missing is left out and its reason goes to the Console. Duplicate ids are refused. Tab (browser.rs), the menus and marking menus (packages/library.rs, frame/offers.rs), node creation, the network's tile colors (panes/network/category.rs) and project files read node types only from it. Projects store a node's type id with parameters by name; a node of a type no installed package provides loads as a grey placeholder, kept as read and written back (above). Commands have their own ids (command:<name>).

Networks and contexts

Every network shares one core (kinogaki_network::Network, of core's Nodes): identities, names, positions, input wires, the display flag, paste, wiring with cycle refusal, deletion, validation and cook order, and Frames (frames.rs): subnets (a node holding a body of the same network context, with Input and Output boundaries; Collapse, Extract and the flattened network every walk cooks; NETWORK-LEVELS.md). Identities are unique through every body of a network.

Core networks. Networks are core-owned. kinogaki-network holds, beside the shared Network, core's one node shape: a Node is a node type id, SDK Parameters (an Arc, compared by allocation first), wires, the core flags (bypass, template, freeze: its kept Outputs), an optional held delta (HeldNode: the delta and core's Held<Captured, Applied>), an optional network inside, and the spares and ports its type declares for its parameters. A CoreNetwork is a Network<Arc<Node>> of one context (copying it copies pointers; editing a node copies that node) with a topology Revision: every edit that changes what a cook visits (a node, a wire, a flag, the display flag, a subnet, a delta's capture) takes a new one, a parameter edit doesn't. Packages declare network contexts as data (the SDK's NetworkContext: evaluation, healing, the flags offered, the element its networks are stored under) and their node types say the rest (Description.contexts/inside, NodeType::reads_wire/partial_reads/ports/spares/held/validate). Core saves every network in one layout for every node type (core/project/src/schema/node.rs: parameters by their kinds, a data parameter (ValueKind::Data, a node's structured record) as a data child in name order, held deltas, captured bases and kept outputs through their data types' codecs, a node's own network as its inside child; a node holding data of a type that isn't installed, or that its codec can't read, is kept as read). A data parameter compares by allocation, by its type's stamp across a save and reopen (node::same_value), and its stamp is part of the node's. Packages read and edit networks through the SDK's NetworkRead/NetworkEdit (ui-api HostNetwork, each edit one undo step, over the editor in core/editor/src/host.rs), and the network pane (core/editor/src/panes/network/) draws every level and runs Delete, Display, Bypass, Freeze and a held node's edits through core's edits.

The walker (kinogaki_network::walk) cooks a core network to a node on kinogaki_cook: its topology is compiled once per revision (Compiled: cook order through subnets' boundaries leaving out unread wires, each node's flattened sources, wires per port, names, wanted outputs; a partial reader's or held node's reads are checked each walk and compile again when they differ), and per node, frozen → its kept outputs, bypassed → input 0, held → its held state's result or the delta applied to the captured base, else its type's cook through nodes::cook. A walk that reuses every node allocates nothing (core/network/tests/alloc.rs). A walk also keeps the results of the nodes WalkCx::also names (templated nodes, Freeze, the spreadsheet's pin); resolves each path parameter of a node that reads objects through the scene's Resolve (CookCx::object/path); and lends the project (units, directory, stored values) to nodes that read it. Every node it reaches gets a stamp of what made its result (its type, parameters, the stamps of what it read, the time when it reads it, what its paths resolved to: one rule, kinogaki_network::stamp), memoized per node, so a drag rehashes only the dragged node and what it feeds; a held node captures its base keyed by its input's stamp, and NetCache::inputs_changed says when that input changed since (the inspector's "Input changed"). A failed walk leaves the failing node and those after it without results. A bypassed node passes input 0 and gives its type's empty value (DataType::empty) on other outputs; a node type's external stamp (a File node's file size and time) is asked again only at the host's next look outside (WalkCx::outside, NodeCook::outside: when a document opens, the window is focused again, the file-system events report a change to a file the document reads, or a final render writes a file; the editor's frame/outside.rs) or when the node's parameters, project or reloads change. Whether a network reads the time is kinogaki_network::reading::reads_time (frozen, bypassed and held nodes don't), so a static scene playing scans nothing. CoreNetwork::set_levels takes an edited copy of the levels and keeps the topology revision when only parameters, positions or views changed.

Contexts are packages' UI parts. A context is what its package declares through the SDK: its network context (NetworkContext), its object kinds, and in its UiPackage its ContextParts (levels, viewport layer, inspector, vocabulary, network view, object and outline rows, creations; see The host interface). The editor's shared code reads those parts and names no context: the network pane, Tab and the menus come from the open context's library (packages/library.rs); the open context's selection levels show as the level row and the number keys (view/levels.rs); component picks are handed to the context (view/components.rs); and the transform tool (view/transform/: Q, W, E, R, T, its spaces, snapping and typed values) is the same for every context, moving the selected objects at an object level and the open context's selection inside an object. The object levels' library is core's own (shell/objects.rs: core's Part and Camera, the installed kinds' creations, the hierarchy commands), with what packages offer there (Places::objects, ObjectMenus) run in their own vocabularies. Views can look through Camera objects (view/through.rs) and draw objects that are lines, Cameras among them (view/glyphs.rs). A context's live tools are its viewport layer's: the views draw inside the dock, where no package can be called, so the frame asks the layer around them (frame/live.rs, view/live.rs): before the panes its holds, its controls over each view and each view's tools (which turn off core's gizmo, handles, picks, wheel zoom and right-click menu while a tool has them), from where the views drew last; after them each view's overlay and its cards over the view pane. Each press is the camera's or the tools', decided once as its first button goes down (view/press.rs, by lib-viewport's Move::of_press): Alt with any button, or the middle button, navigates until every button is up, and the layer, the handles, the gizmo, picks, the marquee and the views' menu see none of it; a press without them stays the tools' when Alt goes down during it. A card's body is the tool's node's rows, drawn by Cx::node_rows (HostCx) from the inspector's row building for RowsFor::Card (panes/inspector/node.rs's card) and edited on its path (inspector::edits::apply, inside the tool's gesture); the card and its Done, × and More or Less are placed for the harness (["Card", title, "Done"]). The context's Levels::pivot gives a frame (asked once a frame while the gizmo shows, Editor::selected); the pivot choice is kept per context, starting at its ViewportSpec::default_pivot (view/transform/pivot.rs), the gizmo sits at it resolved against the frame, a choice calls pivot_changed, and HostTransform::pivot gives it to the tool. While Pick waits for a click, the layer's tools aren't asked. While its live_node is shown, the open network's templated branches are picked in the views through the shown type's ObjectPicking::piece_ray (view/branches.rs): the one under the pointer draws hovered, those wired into the node's operand inputs selected, and a click wires one in (Command takes it off) by the network's rule (Network::branch_pick), inside the open gesture. A view's interaction id is absolute, so those calls see the view's own response of the frame.

Object levels, Output nodes, Freeze and subnets are described in NETWORK-LEVELS.md; node paths in NODE-PATHS.md.

The host interface

Packages' UI is written against the SDK's host interface (studio_ui_api::host, studio_ui_api::package): a UiPackage is a state type, its contexts (ContextPart<S>), its session slots and its settings pages over stored values; each part's methods take the package's state and a call context (&mut dyn Cx, or ViewCx/InspectorCx) reaching the host's capabilities (HostUndo, HostSlots, HostSelection, HostDoc, HostView, HostStatus, HostShell, HostTransform, HostPrefs, HostNetwork, HostTools, and the gizmo and units services). Studio's side:

  • The editor's parts (core/editor): the document (kinogaki_document::Document), CookState (cook/: the scene cook, the results it published, packages' latest-wins jobs), ViewState (view/: the views and the transform tool), Panes (panes/: the network pane over every level, the inspector, the outliner, the spreadsheet, the Console, the Timeline, docked by panes/dock.rs), the image pipeline's draws and swatch pictures painted (paint.rs; the pipeline itself is the kinogaki-image-pipeline crate), Shell (shell/: file dialogs, the document's file and its opens and saves, the status line, frame input, services; it holds the state of commands/ (key stores, menus, marking menus, Repeat, tools, shelves), settings/ (the user's settings, their file, the Settings window), log/ (the Console's log) and places.rs (where items drew), which the other parts read) and Packages (packages/: the installed packages with a UI and each one's state). A part's fields are private to its module and a part never names Editor. Editor's fields are private to frame, and frame alone names them all (its files: calls into packages, files, rendering, the harness's readers in frame/drive): it destructures the editor and passes each part what it reads, so no part is shared or locked, and where a step changes more than one part (a new document, an action) it is written there once. What a part counts for the harness it records into a probe (probe.rs), which holds nothing in the shipped editor. The image pipeline keeps what renders of one document in one value (DocumentRender: the render geometry and scenes, final renders, freezes, picture writes, swatches) that a new document replaces whole.
  • **HostCx** (host.rs): the SDK's host interface over the editor's parts for one call of a package's UI, built from borrows of every part but the packages', so a call reaches the document, the cook's results, the views and the shell through the SDK's traits alone and never another package's state. What would call back into a package (an undo ending a live tool, an action running a package's item) is queued (Later) and runs after the call returns.
  • **kinogaki-sdk-ui** (core/sdk-ui): the SDK's Ui, Painter, sheets and the host's services over lib-ui. Its rule is pixel identity: each SDK call makes exactly the lib-ui call a Studio panel makes for the same thing (core/sdk-ui/tests/equivalence/). Only Studio's host uses it; packages and plugins never depend on it, and it never names the editor.
  • **drive.rs** (the harness feature): the surface core/harness drives the editor through. It hands out the SDK's host interface and plain values, never a part.
  • **probe.rs**: what a part reports to the harness (what it drew, what it counted) it records into a Probe, which holds data only with the harness feature; recording costs nothing in the shipped editor. Each part's readers are its one place gated on the feature.

Core's testkit (core/testkit, kinogaki-testkit) holds toy packages written against the SDK alone: Board (toy.board, a context with every UI part), Blocks (toy.blocks, a drawable data type with every facet and node types of every parameter kind), Block Files (toy.files, a format), Shapes (toy.shapes, a headless kernel over the standard data types), and the linked toy.handles and toy.patches. The harness (core/harness, kinogaki-harness) runs the real editor offscreen with its event routing and background work and only the packages a test names (Harness::start(Selection::names("board,blocks"))); it is read through the SDK's host interface and plain values. Core's editor tests (core/editor/tests/) and every plugin's use it (plugins as a dev-dependency alone).

Shared libraries and plugins

Studio and its plugins share one standard library and one build of the crates they exchange values through, so values cross as plain Rust types, statics are shared and TypeIds agree. Every binary links std dynamically (-C prefer-dynamic, .cargo/config.toml), and core/shared (kinogaki-shared) is a Rust dylib holding the SDK (kinogaki-studio-api), its standard geometry values (kinogaki-geometry), its edit contracts (kinogaki-edit), its UI layer (kinogaki-studio-ui-api), the render interface (kinogaki-render-api), the viewport packet contract (kinogaki-viewport-api) and wgpu. apps/studio names it (use kinogaki_shared as _;), so the linker takes those crates from it while Studio's other crates use them as usual; its install name is @rpath/libkinogaki_shared.dylib, and the executable's rpaths are the bundle's Contents/Frameworks and, run from the target folder, deps. tools/package_macos.py copies std and kinogaki-shared into Contents/Frameworks, renames std's reference to @rpath, checks nothing else outside the system is linked and re-signs ad hoc. Rust can't combine a dynamic std with LTO, so the release profile has none; the owner accepted the cost (2026-10-29: startup about +4%, some mesh kernels 5–14% slower, interactive latency unchanged; the root's PERFORMANCE.md) to be recovered in the kernels later.

kinogaki-shared is built the same in every build (the app's, the plugins', the tests'): cargo resolves features per build, so it enables every feature the workspace enables on the crates it holds (core/shared/Cargo.toml; check_architecture.py names any it lacks). Its exact build is the kit. kinogaki_shared::BUILD names it: a fresh id each time the library is compiled (core/build-id, a procedural macro, runs at every compile, and cargo compiles it again exactly when something it is built from changed). Every plugin library names kinogaki_shared::link!() once, which records the BUILD it was compiled against, and the loader (core/plugin-host) reads that record right after opening the library and calls its entry only when it is the BUILD of the library Studio runs with; any other is refused with one Console line ("built against a different Kinogaki build; rebuild plugins"). So a cargo build that rebuilds the app and the shared library but not the plugins leaves them refused, never loaded against a changed layout. tools/build_plugins.py builds the shared library and the first-party plugins in one build (each a dylib exporting its package with export_package!) and writes target/<profile>/plugins/<kind>/<name>.dylib with its manifest (id, kind, version, dependencies); tools/package_macos.py builds the app and the plugins in one build before it bundles them (with no plugins the bundle has no PlugIns, and the app starts with core alone); --examples builds the SDK's example plugins against the kit alone, and --kit DIR writes what a third party builds against (the dylib, the metadata of the crates it holds, the proc macros, the compiler's standard library and a README).

The loader (core/plugin-host) finds plugins in the install's plugin folder (the bundle's PlugIns, or plugins beside an executable in a build folder) and the user's, reads each manifest without loading the library, chooses (resolve: one per id, the install's first; only those built for this install's kit; only those whose dependencies load, dependencies first), then opens each library and calls its entry once, never unloading it; what can't load is a Console line saying why. Its src/sys.rs (dlopen, dlsym) is the only unsafe code in Studio's own crates. A plugin built for another kit is refused before it is opened: nothing at link time would notice, and it would misbehave once called. apps/studio/tests/plugins.rs builds the SDK's example plugins against the kit alone, outside the workspace, as a third party does, and loads them: in dependency order, refused when what they need is off or their package is refused, their node cooked in a core network and their UI on the SDK's fake host. The plugin host's own tests compile tiny libraries with rustc to show one built for another kit refused without being called.

At startup (core/editor/src/packages/plugins.rs), before the first editor, the app loads the run's plugins once (kinogaki_editor::load_plugins): those not turned off in the user's settings (plugins.off in settings.prisma), or, given **--plugins mesh,render** (or KINOGAKI_PLUGINS), exactly the packages it names (by id or its last part, never a plugin's file name: mesh_kernel for the Mesh kernel kernels/mesh.dylib, whose id is kinogaki.mesh_kernel) and what they need, for that run alone (kinogaki_plugin_host::choice); a name that matches no plugin is a Console line. Each package a plugin brings is installed in core's registry (kinogaki_network::registry::install), where everything reads packages from; a package left out is absent everywhere, and its objects and nodes load as placeholders (above). The app starts on an empty scene unless a project is opened. Settings ▸ Plugins (settings/window.rs, shown when plugins were found) lists the plugins found and whether each loads at the next start; a loaded plugin is never unloaded.

Package icons (a package's own SVG icons named by its id) are not yet in core. The editor without any render engine is core's: no Render view or final render is offered (core/editor/tests/behaviour/render.rs, no_render_engine).

Time and the Timeline

One time model serves every context. The scene's time (kinogaki_document::SceneTime, core/document/src/time.rs): the frame range (start, end and frames a second) is the document's and undone with it; the current time is navigation, as the view is, and saves as /project/time. Moments are the SDK's Time (seconds), so a change of the frame rate moves no moment: keys stay where they are in time. Animation channels (core/anim, kinogaki-anim) are a node's curves by parameter name (Channels), resolved into its Parameters before it cooks (Channels::resolve_into); the keyframe maths is kinogaki_prism::curve, so a project file, an export and Studio agree bit for bit. Every sheet keys its rows one way (panes/inspector/keys.rs): a node's parameters, an object's placement and its kind's rows are keyable, their value fields tinted animated, keyed or overridden (lib-ui's ANIMATED, KEYED, OVERRIDDEN); Alt+click on a label sets or removes the key now; the row menu has Set Key or Remove Key, Clear Channel and Go to Previous or Next Key; Set Key (K, Global) keys the picked nodes' animated rows or the selected objects' transforms. Editing an animated row is a key at the current time, the edit rule, in the document alone: CoreNetwork::edit_parameters (the sheet and packages' NetworkEdit::set_parameter) and Object::set_transform_at / Document::edit_object_rows (the sheet, the gizmo, camera navigation), so a drag is one gesture and one key.

The Timeline pane (core/editor/src/panes/timeline.rs) draws lib-ui's time slider over the document's time, in frames at the range's rate, with a tick at each key of the inspected node. Scrubbing, stepping and typing a time move the current time (no undo step); dragging or typing an end of the range is one step. Playback runs on the window's frame clock: each drawn frame takes the time Playback gives for the frame's input clock and asks for the next frame, never a timer. It stops at the range's end, on a scrub, or when the Timeline isn't shown.

Console and the log

One log collects what every part of the application reports (core/editor/src/log/mod.rs, Log): a bounded ring of records (time, level, source, the object or node path when there is one, text, repeat count), at most CAPACITY (10,000) with a count of the dropped. A record that repeats the last one exactly only counts (Maya's "×12"). Adding takes one short lock from any thread and never waits on the reader: the reader copies what is new under the same lock and draws from its copy. Reporters: the cook's errors and warnings, opens and saves and their load reports, packages through HostStatus::report (Log::add_about carries a subject the context spells, what inside the object or node the record is about), plugins that didn't load, rendering.

The reader is woken through its Wake once when records arrive, never by polling: the first record after a read wakes, the rest find it woken. A frame holds the log's wakes (Log::hold_wakes) and reads what came before it lets go (Log::release_wakes).

The Console pane (core/editor/src/panes/console.rs) draws lib-ui's widgets::console over the log, a view only. It copies new records only while it is shown, so a record costs the log's append alone while it is closed; Clear empties the log behind it.

Visual reference

Every inspector is a property schema drawn by lib-ui's one renderer: node inspectors (core/editor/src/panes/inspector/) through the SDK's sheet, which draws with it (core/sdk-ui/src/sheet.rs), object inspectors with the transform rows and a kind's own rows, the Settings window's pages (settings/window.rs). Hosts apply the edits: one undo step per gesture, named after the field it began on.

The theme is lib-ui's pixel-art theme by default (owner, 2026-10-24); the reference application's neutral values (26/38/50), text (217), muted text (140) and orange (231,160,63) are its tokens. Network tiles show a node's kind of work by color alone, a bar along the node's left edge (panes/network/category.rs; owner, 2026-10-25: tiles carry no per-tool icon). Use source screenshots and live side-by-side review for each visual change; tools/ui_captures.py draws core's reference captures on testkit scenes and kinogaki-capture-diff (core/capture-diff) compares two sets pixel by pixel, exactly or within a tolerance (the root's TESTING.md). Source revisions are recorded in reference/source-revisions.txt.

References: SolidCraft, egui, wgpu.

Rendering

Render engines are packages: the render interface is the SDK's kinogaki-render-api; engines describe themselves as data (EngineDescription) and arrive as plugins (renders/spectre.dylib, renders/pixel.dylib), each bringing its own lights (and fog, when it renders one) as object kinds; Render Output, one render output of the scene, is core's own kind (core/package/src/render_output). The core render sync (kinogaki_scene::render) turns the board's results into engine prims once per document (RenderGeometry: one entry per object, rebuilt only for changed objects) and diffs them per engine instance (Target: each update the changed objects' prims alone); RenderScenes has one render scene per Render Output object and the active one, and Finals the final renders a command asks for. A value renders as the items its data type's Renderable gives (kinogaki_render_geometry::Items; the standard polygons through GeometrySource, arrays shared, after a drag only the points that moved). View cameras are view state: an orbit sends nothing. The ImagePipeline (core/image-pipeline, its own crate, with no UI; the editor holds it and paints what it draws) holds the engines the packages brought, which project files read their settings against; it renders into views, runs Comp's plans on Render Output and draws the display (LIVE-PIPELINE.md). See RENDERING.md and the Comp plugin's COMP.md.

Keys, menus and marking menus

Key stores (core/editor/src/commands/keys.rs): every key the editor answers is one table of bindings, each in a store. Global, the viewport's and the network pane's stores are core's; a context brings its own in its library (KeyStore), which falls back along its parent chain, then to the viewport's, then to Global. A key looks up from the innermost store outwards: a store's binding shadows an outer one of the same chord, and sibling contexts' keys never meet. Menus show their shortcuts from the same table (commands/menus/), so what a menu says and what a key does are one thing. The user's changes are kept in the settings (UserKeys), a changed store whole, by its targets' names: a binding whose item no installed package gives is kept and does nothing until the package is back. Settings ▸ Shortcuts is settings/shortcuts_page.rs.

Menus (commands/menus/): the menu bar and the right-click menu over the views as data, built from what applies now and drawn by one drawer. File, Edit and Window are the shell's; Create, Select, Modify and Transform, and the right-click menu, are the library's where the user is (packages/library.rs, frame/offers.rs), their items in submenus by family. Every entry holds an item of a named vocabulary (a node type's id, an object kind's id, or command:<name>), hidden by its When and its vocabulary's shown.

Marking menus (commands/marking.rs): A, S and D held over the views open a radial menu of eight compass items (North first, clockwise) and a list below South, drawn by lib-ui's radial without icons. Press the key, move toward an item and release: a flick chooses before the menu has drawn; holding while it draws, the item the pointer is toward, or, moved past South, the list's row under it; a click on an item chooses it too. A release in the dead zone, a tap, Escape and the window losing focus choose nothing. An open menu owns the pointer and its key until it is released. Each menu's items at each level and the menu's size are the user's (MarkingPrefs, Settings ▸ Marking Menus, settings/marking_page.rs).

File dialogs (shell/dialogs.rs): the editor asks, the system's panel answers later and wakes the UI, and the answer becomes an action in the next frame; one dialog is open at a time, and an offscreen editor waits for its driver to answer (Dialogs::answer).

Plugins in the build

Each plugin is its own repository under the root's plugins/ (PLUGINS.md): Mesh (plugins/mesh, its context, nodes, UI and kernel), OBJ, Spectre, Pixel, Comp and Layout. Core gives a context the network pane, Tab and menus over its node types, the node sheet, declared handles and the transform toolbar; the context declares data. The design decisions of live tools stay: a tool invocation is one gesture until Enter commits or Escape restores the snapshot before it; changing context accepts it, as Enter does; drags overwrite their caches in place and add no entries.

Assets (studio/docs/ASSETS.md)

An asset is a subnet saved under a name so it can be used many times: a leaf shape, a grass cluster, a print setup. It works the same in every network kind (Mesh, CAM, CAD and shading networks). The short version is in NETWORK-LEVELS.md; this page covers the model, the commands, the library and how it compares to Houdini and Blender.

Attributes (studio/docs/ATTRIBUTES.md)

A value carries named, typed attribute columns beside its positions, as Houdini's geometry does (the SDK's kinogaki_geometry::Attributes). A column belongs to one domain and holds one item per element of it:

Cameras and lights (studio/docs/CAMERAS-LIGHTS.md)

Lights and cameras are root-level objects. Camera and Light are object kinds beside Part and Mesh. They live at object levels (the root and inside Parts), carry a transform and the outliner's flags like any object, and have no network: their parameters are rows in the inspector. They are shaped as USD shapes them (UsdGeomCamera; UsdLux's RectLight, DiskLight, DomeLight, DistantLight, and SphereLight with ShapingAPI's cone) so a renderer reads them directly. The render interface's Camera and Light prims use the same names.

The 2D canvas (studio/docs/CANVAS.md)

The Canvas pane (a tab beside the Viewport) paints any value whose data type has a Canvas facet: the open object's result in a network, else the active object's. A context declared NetworkContext::on_canvas brings it to the front while open. In a compiled network it shows what the plan shows instead (COMP.md).

Color (studio/docs/COLOR.md)

Studio manages color with OpenColorIO configs read and run by lib-color (kinogaki-color, our own reader and processors; OpenColorIO itself is only the library's test oracle). The built-in config is the Academy's ACES 2.0 CG config (cg-config-v4.0.0_aces-v2.0_ocio-v2.5).

Images (studio/docs/IMAGES.md)

The Image node, the one node that reads a picture, in every network that carries images: where it is offered, what it reads, and how its picture reaches what it feeds.

The .kino format (studio/docs/KINO-FORMAT.md)

A project is a Prism document (lib-prism): .kino files hold its binary crate, a save to a .prisma path writes the same document as text, and a load recognizes the encoding by its magic. Core's schema is core/project/src/schema/ (each file's header shows its part of the layout); the format's tests are the kino_* tests of kinogaki-project (TESTING.md). No version numbers: a format change changes the format, and the repo's examples are converted once in the same landing.

The live pipeline (studio/docs/LIVE-PIPELINE.md)

The working loop this page describes: pin the Canvas on a Comp or Layout result, walk anywhere in the project (into a material, a Mesh object, a light), edit, and the pinned Canvas updates through render, Comp and Layout. Rulings: owner, 2026-11-04. The engine contract is Your first render engine; what Comp does is COMP.md; the pane is CANVAS.md.

Materials (studio/docs/MATERIALS.md)

How a scene says what a surface is made of: Material Libraries and Materials, the shading network inside a Material, the Material node that assigns one, and how a Material reaches a render and the shaded views.

Network levels (studio/docs/NETWORK-LEVELS.md)

Kinogaki's networks follow Houdini's split between the object level (/obj) and the networks inside objects (SOPs). This note says what each level holds, how the display flag and the Output node work, how one object reads another (a node's path row), how Freeze works, and what the viewport and the outliner show. Node paths, which name what such a row reads, are in NODE-PATHS.md.

Node paths (studio/docs/NODE-PATHS.md)

A parameter that names another node or object holds a node path, Houdini's operator path. Core handles every such parameter in one place (core/document/src/paths/: text parses and normalizes, walk resolves, fits tests what a row may name, follow keeps paths on their targets, copies relinks copies), and a node type has no path code of its own. A node type declares such a row as the SDK's ValueKind::Reference(RefFilter): the filter says what fits (a data type's results, a kind of object, a Material).

Rendering (studio/docs/RENDERING.md)

Render engines are packages. The contract between a host and an engine (prims, dirty bits, materials, previews, views) is Your first render engine, and this page doesn't restate it. This page says what Studio does on the host's side of that contract, and what the scene holds of a render. Spectre, the engine in the build, is described in its plugin (SPECTRE.md). Materials are MATERIALS.md; Light and Camera rows are CAMERAS-LIGHTS.md.

Wrangle (studio/docs/WRANGLE.md)

The Wrangle node (Mesh, Attribute) runs a few lines of code over the geometry: once per point, per primitive, per vertex, per edge, once for the whole geometry, or a given number of times. It is Houdini's Attribute Wrangle. The code is lib-script, a Python-like language with static types, and its function names follow VEX. The language reference is lib-script's docs/LANGUAGE.md, and every function is listed in docs/FUNCTIONS.md.