How the parts work together

The image store

Core's image store is ImageFiles in kinogaki-scene: the document's one store of image files, on disk or embedded in the project. Every reader of a picture asks it by source: the render targets for their Texture prims, Comp's runner, the cooks of Image nodes, the shaded views, node tiles and the Canvas. This page covers how a file is read through the format plugins, how it is kept and shared, how it reaches engines, Comp and Layout, what makes it read again, how its colors are resolved, and the limits on all of it.

On this page

Pieces

PieceKindWhere
ImageSource, ImageFile, ImageColor, EmbeddedFileSDKkinogaki-geometry, sdk/crates/geometry/src/image/source.rs
The ImageFiles trait, FileImage, the Image node's rows, image_sourceSDKkinogaki-studio-api, sdk/crates/api/src/image_files.rs
FileFormat, Input, ReadBudgetSDKkinogaki-studio-api, sdk/crates/api/src/format/budget.rs
The Image node, kinogaki.core.imagecorekinogaki-core-package, studio/core/package/src/image_node.rs
The store: ImageFiles, Waiting, ImageKey, Readcorekinogaki-scene, studio/core/scene/src/render/image_files/mod.rs
A target's Texture primscorekinogaki-scene, studio/core/scene/src/render/images.rs
Looking outside for changed filescorekinogaki-editor, studio/core/editor/src/frame/outside.rs
Color resolution and conversioncorekinogaki-color-service, over lib-color
PNG, JPEG and OpenEXR readerspluginskinogaki-png-format, kinogaki-jpeg-format, kinogaki-exr-format

ImagePipeline::new makes one store and holds it in an Arc; the same store is lent to every target, to plan runners through RunCx::files, to cooks and final renders as Waiting, and to the canvas's pictures written (studio/core/image-pipeline/src/lib.rs).

What is asked

A source (ImageSource) is a file and how its colors read. The file is ImageFile::Path, a path as typed, relative to the project's folder unless absolute, or ImageFile::Embedded, an EmbeddedFile kept in the project. Its color is an ImageColor. A relative path in a project that has no folder yet fails, saying so.

The store answers three kinds of read (Read):

A request is an ImageKey: the file (FileKey: a resolved path, or an embedded file known by its bytes' BLAKE3 hash and name), the read, the space when the source names one, the layer an image or data read is of (a MaterialX image node's layer; none is the file's main layer), and most, the most pixels a side the asker holds. Every answer is an SDK Image in 64-pixel tiles; its color layers, and the layer of an image or data read, are RGBA half floats, and a whole picture keeps a data layer (ids, depths) as the file stores it.

An ask returns at once or waits. ask and get return at once: they answer FileImage::Read, FileImage::Failed or FileImage::Reading, and queue the file for the store's one reader thread, which wakes the window once a file. wait and wait_reduced read on the asker's own thread, waiting for a read already under way. Waiting wraps the store as the SDK's ImageFiles trait for askers that wait, cooks and final renders; the store itself implements the trait for askers that never wait, such as runners.

Reading through the format plugins

Decoding is the format plugins' work. studio/core/scene/src/render/image_files/read.rs asks the installed formats for a reader of the standard Image type, chosen by the file's extension, then by its first bytes, and reads with Purpose::Value and a default ReadBudget; a read on the reader thread is cancelled when the store's epoch moves. A file on disk reads through Input::file, which takes regular files alone, so a pipe or a device fails at once; an embedded file reads from its bytes in memory through Input::embedded. A format reads the file as one Image as the file holds it: samples as stored, each channel group a layer (an OpenEXR's parts are layers), color layers tagged as the file says (ColorTag), data layers raw, the file's attributes as metadata. With no installed format reading the file, the answer names its extension and asks for a format plugin that reads it. A format that panics fails that file and says so.

Each file is decoded once for the answers asked of it together (studio/core/scene/src/render/image_files/decode.rs): the decoded Image is held while the answers are made and for two frames after, then weakly. Each answer is made tile by tile on the process's workers (studio/core/scene/src/render/image_files/tiles.rs), a tile's pixels converted as floats in runs and reduced by whole-pixel boxes when the asker gave most; no whole-image plane is made.

Three things in the project lead to a read:

Kept and shared

The store keeps an entry per key: Reading, Read or Failed. A read image is held weakly, so it goes with its last holder, and while anything holds it every asker gets the same tiles: an upload keyed by a tile's identity uploads nothing new. An image read for an asker that didn't wait is held strongly until that asker asks again, or for two frames (ImageFiles::follow, once a frame); a failure is said until a look outside finds the file changed, and goes once nobody asked for two frames. The store has three counters askers compare: revision moves with every changed answer, epoch when the project's folder or the colors change (every entry goes and the read under way is told to stop), and changes when a look outside finds a file changed.

How images reach their readers

What makes an image read again

A window's ask for a file the store holds reads nothing from the disk. A look outside looks at the files: when the window is focused again, or when the system's file-system events report a change to a file the document reads. The lib-ui shell's Watch (libs/lib-ui/crates/shell/src/watch.rs) reports them through FSEvents on macOS; on other systems focusing the window is what looks outside. The editor watches the files its nodes' File rows name and every path the store holds or failed to read (ImagePipeline::image_paths). ImageFiles::look_outside compares the size and modification time of each file it holds or failed to read with those it was read at; a file written over, or put in place where it was missing, loses its entries and its decoded image, and changes moves. Then a target removes that Texture prim and sends the file again once it is read, plans run again, node tiles and the shaded views' images are made again, and the cook's own look outside recooks Image nodes whose file stamp (size and modification time) changed. An asker that waits compares the same stamp on each ask and reads a changed file again.

An embedded file never changes. A new project folder or new colors start a new epoch, and every file is read again as it is next asked for; new colors also make every target send everything colored again. At a new frame, prims that name an Image node by op: are built again, so a frame-numbered file is read for the frame.

Color

ImageColor is the Image node's Color row: Auto (the default), Data, or a space of the project's color configuration (ImageColor::Space). For a color layer, read.rs resolves the space:

  1. a space the source names, by its name or an alias;
  2. with Auto, the file's tag: a space the configuration knows; an ICC profile describing one of its displays, that display; another profile, converted to a wide display as it is read;
  3. else the configuration's file rules on the file's name (ColorManagement::file_space);
  4. else srgb_texture for integer samples and the working space for float samples.

An image read converts from that space to ACEScg; a picture read keeps a display-referred space's code values. Straight colors are premultiplied after conversion; premultiplied files stay so. Data is never converted. Format plugins hold no color maths: the conversions are the color service's (kinogaki-color-service, an OpenColorIO configuration whose default is lib-color's built-in ACES 2.0 configuration).

Limits

The render path follows Texture prims and Comp plans from here to the screen.

Written in the tree as docs/how/image-store.md.

← The render path · Import and export →