How the parts work together

Import and export

Every file format Studio reads or writes comes from a format plugin, a package that registers an SDK FileFormat. Core looks a format up by the data type the caller needs, then by the file's extension, then by its first bytes. File ▸ Import turns what the format returns into objects. A File node takes one value as its output. Export collects objects into the same items and passes them to the writer that the file's extension names. Reads and writes run on worker threads, and each read stays within a budget, because the file may be hostile.

On this page
object level inside a network adoptable types file_types next frame Purpose::Value File ▸ Importwhere the user is ProjectTask::importthe project worker File nodecooks in a network Formatstype, extension, bytes FileFormat::reada format plugin ImportedFileItems and warnings Scene::import_itemskinds adopt values Objectsone undo step The node's outputone value of wants

What a format declares

The contract is the SDK's format module (sdk/crates/api/src/format.rs, sdk/crates/api/src/format/) in kinogaki-studio-api. A package adds a format with Package::with_format. Installing a package (registry::install in kinogaki-network) rebuilds the registry's Formats from the formats of all installed packages, in install order. Core's own package registers no format, so nothing can be imported until a format plugin is installed.

MemberWhat it gives
id, labelkinogaki.format.obj, "Wavefront OBJ"; file dialogs list formats by label
extensionslowercase, without the dot
detects(head)whether a file whose first bytes (up to 64, Input::head) are head is of this format; no by default
producesthe data types a read makes: standard::geometry(), standard::materials(), standard::image(), …
optionsevery option by name with its default (FormatOptions)
schema(options, purpose)the rows that show those options for a Purpose
write_options, read_optionshow a node saves the options: by default one field per option, read back strictly
read(&input, options, &reading)the read, called on a worker
writer()a FormatWriter, when the format writes

Purpose::Objects is File ▸ Import, which makes objects from a file's items. Purpose::Value asks for one value of a type the host names: a File node's output, or a picture for core's image store. A format's Value schema can leave rows out; OBJ's leaves out Embed Textures and Bump Maps as Normal Maps. format::options (sdk/crates/api/src/format/options.rs) holds what formats share:

A read gets an Input, the options and a Reading:

The read returns Imported: FileItems, parents before children, and warnings for what it read with loss. Each item has a name, its parent's index, a placement relative to that parent, a visible flag and its Data. An item with no data is a group.

Finding the format for a file

kinogaki_network::Formats (studio/core/network/src/formats.rs) is the one place that looks up a reader or a writer:

  1. readers(types) keeps the formats whose produces holds a type the caller can use.
  2. by_extension picks the one whose extension the file name ends in, ignoring case. The longest matching extension wins (print.gcode.3mf is a gcode.3mf before it is a 3mf). On a tie, the format registered first wins.
  3. Otherwise pick takes the first format whose detects accepts the file's first bytes.

A writer is found by type with writers(types), which keeps the formats whose FormatWriter::accepts holds one of the items' types, then by extension alone (writer_for). When nothing fits, readable names what the installed formats read ("not an OBJ or FBX file (.obj, .fbx)").

CallerTypes it asks for
File ▸ Import (exchange::read_for_import)adoptable(): every type an installed object kind declares as adopted
File ▸ Import of a picture, and the image storestandard::image()
A File noderegistry::file_types(context): the types its network carries that a format reads into
File ▸ Export (exchange::export)the types of the items written
Final renders and canvas outputs (studio/core/image-pipeline/src/files.rs)standard::image()

The Import and Export file dialogs (studio/core/editor/src/shell/dialogs.rs) list one filter for each installed format that reads, or that has a writer.

File ▸ Import

Editor::import (studio/core/editor/src/frame/files.rs) decides by where the user is:

That read runs on the project worker (ProjectTask::import, studio/core/project/src/task.rs), a thread with Rust's default 2 MiB stack, while the UI keeps drawing. exchange::read_for_import (studio/core/project/src/exchange.rs) finds the format and reads with Reading::new(Purpose::Objects, units, colors) and the options the user set for that format on Settings ▸ Import (studio/core/editor/src/settings/import_page.rs, which draws each format's schema(options, Purpose::Objects)), keyed by the last part of the format's id (obj); a format with none set reads with its defaults. Items without a name take the file's stem.

At the start of the next frame, the editor takes the result and calls Document::import_items. That is one undo step, named Import (Undo), and it selects the top-level objects it added. Scene::import_items (studio/core/document/src/scene/import.rs) walks the items in order:

The format's warnings and the adoption's go to the Console.

The File node

The File node, kinogaki.core.file (studio/core/package/src/file_node.rs), belongs to core's package, kinogaki-core-package, and holds no format of its own. Tab offers it in any network that carries a type an installed format reads into, apart from images, which the Image node reads. The node holds two kinds of row:

The output port's type is the first type the extension's format produces that the network carries. The cook resolves the path and picks the reader as above. It reads with Purpose::Value, with wants set to the port's type, the project's units and colors, and a budget that the cook's own cancellation cancels. It takes exactly one value of that type; a file that reads as several values is an error. The node's external stamp hashes the path, size and modification time with blake3, so the node cooks again when the file changes on disk.

Units, up axis and colors

A project's settings hold its units: the up axis (Y or Z) and a length unit (millimetre, centimetre, metre, inch or foot), as lib-math's Units. A read receives them as the SDK's Units (the up axis and metres per unit, converted by units_to_sdk) in Reading::units, and converts the file's lengths into them. format::Conversion (sdk/crates/api/src/format/conversion.rs) is the change from a file's space into the project's, C = s·Q. Here s is a uniform scale, the project's units per file unit, and Q is a turn between up axes, or a mirroring for a left-handed file. It moves:

inverse is the way back, for a writer. Turns are signed permutations, applied exactly.

FormatWhere its file's space comes from
glTFmetres, Y up: Conversion::new(Units::METERS, units) (plugins/gltf/src/space.rs)
USDthe stage's metersPerUnit and upAxis (plugins/usd/src/space.rs)
FBXthe file's axis system and unit, through Conversion::of_axes (plugins/fbx/src/read/space.rs)
OBJthe options up, unit and scale (Y up in metres by default), since the file records none. The reader places its one Geometry with the SDK editor's gather (plugins/obj/src/read.rs); the writer writes Y up in metres

Lengths inside materials, such as a transmission depth or glTF's thickness, convert as points do (NetworkSeed::scale_lengths). Colors convert from the file's space into the project's working space through Reading::colors. Spaces are named by their interop names (color::LIN_REC709, color::SRGB_TEXTURE, color::SRGB_DISPLAY).

Materials

A 3D format brings its materials as one item of standard::materials(), first among its items: Materials { materials, images }.

Import adopts the item as a Material Library (studio/core/package/src/material.rs):

Export passes the writer the same kind of item, built by exchange::materials: every Material the written polygons and instances name, with its network as authored, its standard form from the installed engines and its Double Sided, and the images the domes name, every path absolute against the project's folder. A writer of one shading model reads a material with MaterialNetwork::try_from and translates it with MaterialNetwork::translated (to standard_surface, gltf_pbr or UsdPreviewSurface) from kinogaki-render-api. MaterialX documents go through format::materialx::{read, write} (sdk/crates/api/src/format/materialx/mod.rs), over the SDK's kinogaki-mtlx, which reads and writes the document itself. A document's colors are in lin_rec709 (format::materialx::SPACE) unless it names another space, and image_color and colorspace map a texture's colorspace to an ImageColor and back.

Images a file names

An Image node reads an ImageSource, which pairs two things:

The files a file names (an OBJ's MTL libraries and maps, a glTF's buffers and images, USD layers and textures, MaterialX includes) are reached through format::files::Named (sdk/crates/api/src/format/files/named.rs). It resolves a name relative to the read file's folder, or wherever an absolute name points, reads regular files only, identifies each by its canonical path, and charges what it reads to the budget. What it couldn't resolve is listed in one warning.

Named::image gives a texture on disk as a path, or as its bytes when Embed Textures is on. Files packed inside the read file are always embedded: a usdz's entries as held.slice ranges of the file's bytes, a GLB's images copied out once. A file held in memory has no folder, so its pictures keep the names it gives, and those names resolve from the project's folder.

Scene formats pass picture bytes through without decoding them. Core's image store decodes them: it reads an Image node's file through Formats::reader_for with Purpose::Value (studio/core/scene/src/render/image_files/read.rs). PNG, JPEG and OpenEXR read a file as one item holding one standard::image(), converting nothing: samples as stored, each channel group a layer (an OpenEXR's parts are layers of one Image), colors tagged as the file says (ColorTag) and every attribute as metadata. A writer whose display_referred is true (PNG, JPEG) receives a display's code values, tagged with the display; OpenEXR receives linear light in the working space.

Read limits

A file may be hostile, so every reader checks what the file claims against the read's ReadBudget (sdk/crates/api/src/format/budget.rs) before acting on it:

Limit (ReadLimits)DefaultChecked by
bytes, of the files the read names16 GiBtake, through Named::read
allocation, one allocation the file sizes4 GiBallocate, vec::<T>(count)
total, every such allocation together16 GiBallocate, vec
depth, nesting256nest
ratio, a decoder's output per input byte1032expand
pixels, one picture2²⁸pixels

The 256 levels of depth fit in the 2 MiB stack a read runs on. Every check also polls the read's cancellation, and long loops call Reading::check. Clones of a budget share what has been spent, so a reader's workers and the files it names all draw on one budget. An Input reads regular files only, up to the size each had when it was opened (files::regular). A refusal is an Error that names the file. What a reader left out is a warning.

kinogaki_studio_api_testing::read_hostile (sdk/crates/api-testing/src/hostile.rs) reads a test file the way Import does: on a 2 MiB thread, within small limits. A File node's read is cancelled by its cook and an image store read by the store; File ▸ Import reads with the default budget, which nothing cancels, since the project task has no cancel.

Parallel reading

Packages start no threads. Their parallel steps go through the SDK's geometry::parallel (map, map_blocks, chunks_mut; sdk/crates/geometry/src/parallel.rs). These run on the workers the host installs as it starts, or on the calling thread when none are installed.

OBJ streams a file in blocks of whole lines (plugins/obj/src/obj/read/stream.rs). One parallel::map of two jobs parses one block while the next is read, and each block's chunks are parsed in parallel. JPEG, OpenEXR, USD and FBX decode on the same workers.

Export

File ▸ Export… writes the scene's placed roots (exchange::placed_roots), Export Selected… the selected top-level objects (Editor::export). On the UI thread, exchange::items turns them into FileItems at the current time. A Part is a group; an object whose kind holds a network gives its shown result from the cook; one whose kind holds none gives its ObjectKindSpec::value; one of no installed kind is a group, said in the Console. Roots are placed in the world and children under their parents, and objects with no place in the world (a Material Library) are left out below the roots. The materials item goes first.

The write runs on the project worker (ProjectTask::export). exchange::export finds the writer for the path. An item of a type that writer doesn't accept becomes a group, with a warning for each. Export then calls FormatWriter::write with the writer's default options (FormatWriter::options). The Writing carries the project's units and colors, a CancelToken, and meshed for a writer of polygons: it meshes B-reps with the installed mesher at the project's display quality and tolerances, and realizes instances.

A writer writes each file whole or not at all, through format::files::replace. It writes a new file beside the target, <name>.<pid>-<n>.part, syncs it and renames it over the target; on Unix the folder is synced too. A written document names its images through format::files::Beside. A file on disk is named by its path relative to the document. An embedded file is written once, into a <stem>_textures folder beside the document. format::Unique gives each name out once. The writer's warnings ("3 curves left out") go to the Console.

Which plugins read and write what

PluginFormat idExtensionsFirst bytesReadsWrites
OBJkinogaki.format.objobjGeometry, materials (MTL)Geometry, B-reps and instances as polygons, materials as an MTL beside the file
MaterialXkinogaki.format.mtlxmtlxmaterialsmaterials, as one document
USDkinogaki.format.usdusd, usda, usdc, usdza usda or usdc signaturematerials, Geometry, instances, lights, camerasthe same, and B-reps meshed
glTFkinogaki.format.gltfgltf, glbglTF (a GLB)materials, Geometry, instances, lights, camerasthe same
FBXkinogaki.format.fbxfbxa binary signature, or an ASCII commentmaterials, Geometry, instances, lights, camerasread only
PNGkinogaki.format.pngpngthe PNG signatureImageImage, display-referred
JPEGkinogaki.format.jpegjpg, jpegFF D8 FFImageImage, display-referred
OpenEXRkinogaki.format.exrexr76 2F 31 01ImageImage, linear light

OBJ's reading options are up, unit, scale, pieces (by o or g), normals, uvs, materials, Embed Textures and Bump Maps as Normal Maps; USD, glTF, FBX and MaterialX have Embed Textures. The writers' options are PNG's depth and compression, JPEG's quality and subsampling, and OpenEXR's compression and parts, each written at its default. The guide for writing a new format is docs/plugins/FORMAT.md; how plugins load is Plugins.

Written in the tree as docs/how/import-export.md.

← The image store · Saving and loading a .kino →