Class reference

Native serialization

Native serialization

Core has one Document and three ways to write it down. All three round-trip to the same Document; the choice is only how the bytes are laid out and what they carry.

.prisma is to .prism what a .ts is to a .js: author in text, compile to binary, convert either way at any time with no loss. The extension is only an authoring convention — a host always reads the encoding from the content, never the suffix.

#prisma 1.0
def group "world" {
    def object "ball" {
        float radius = 1.5
        float3 albedo = (0.9, 0.15, 0.15)
    }
}
#include "kinogaki/Serialize.h"
using namespace kinogaki;

std::string text   = serialize(doc);              // → .prisma text
std::string crate  = serializeBinary(doc);        // → .prism binary crate
Document back = *deserialize(text);               // text OR crate, auto-detected

Encoders

serialize — text

std::string serialize(const Document& stage, int indent = 4);

Write the Document as .prisma text. indent is spaces per nesting level. The output is canonical and diff-clean: properties are sorted, defaults are omitted (linear interp is the default), and non-finite scalars are written as 0. Every value is self-describing — its dtype and shape are written alongside it — so any scalar, array, or multi-dimensional array round-trips exactly. The first bytes are the header #prisma 1.0.

def serialize(doc: Document, *, binary: bool = False) -> "str | bytes"

Python's serialize(doc) returns the text as a str. It does not expose the indent knob; the binding always writes Core's canonical 4-space form. Document.serialize() is the method spelling of the same call, and doc.to_string(kg.Codec.PRISM) reaches it through the codec path.

serializeBinary — binary crate

std::string serializeBinary(const Document& stage, bool compress = false);

Write the Document as a PRSMC crate: a packed, self-describing binary holding the same elements, properties, time samples and connections. It loads faster than text (packed floats are read directly, with no decimal parse) and is dramatically smaller for documents heavy in numeric arrays — meshes, images, long animation tracks. Same Document → same bytes.

With compress = true, the crate body is packed with Core's own LZ codec (see Compression); the header stays plain and a flag bit records the choice, so deserialize inflates it transparently.

crate  = kg.serialize(doc, binary=True)                       # uncompressed PRSMC (bytes)
packed = kg.encode(doc, kg.Codec.PRISM_BINARY, compress=True)  # compressed PRSMC (bytes)

kg.serialize(doc, binary=True) writes an uncompressed crate. To set the compress flag from Python, go through kg.encode(doc, kg.Codec.PRISM_BINARY, compress=True) or doc.save(path, kg.Codec.PRISM_BINARY).

serializePackage — bundle

std::string serializePackage(const Document& stage, const AssetStore& assets,
                             bool textScene = false);

Bundle a Document and its AssetStore into one self-contained PRSMZ archive: a table-of-contents at the front, blob payloads concatenated after, sorted by name so the same (Document, AssetStore) yields byte-stable output. The scene is embedded as a binary crate by default; pass textScene = true to embed it as .prisma text for a diff-friendly bundle. Asset blobs carry their MIME hint and are keyed by the path of the element that owns them. There is exactly one scene chunk per package.

The AssetStore is a sidecar: Core's value model is purely numeric, so byte blobs live in the store, keyed by element path, and the rest of the graph reaches them through ordinary connections.

#include "kinogaki/AssetStore.h"

AssetStore assets;
assets.set(Path("/world/tex"), pngBytes, "image/png");
std::string bundle = serializePackage(doc, assets);   // → PRSMZ archive

The package encoder is not bound in Python. Build and read bundles through the C++ API or the kinogaki CLI.

detect — encoding by content

enum class Encoding { Unknown, Text, Binary, Package };
Encoding detect(std::string_view bytes);

Sniff the encoding from the leading bytes — cheap, no parse. detect checks the magic markers in order: PRSMC\0Binary, PRSMZ\0Package, then #prisma (leading whitespace tolerated) → Text. Anything else is Unknown. This is why a .txt that is really a crate still loads, and why deserialize needs no hint about which encoding it was handed.

switch (detect(bytes)) {
    case Encoding::Text:    /* .prisma source */          break;
    case Encoding::Binary:  /* .prism crate */            break;
    case Encoding::Package: /* PRSMZ bundle */            break;
    case Encoding::Unknown: /* not a native encoding */   break;
}

The three magic markers are #prisma (7 bytes), PRSMC\0 and PRSMZ\0 (6 bytes each, NUL included). The same constant drives both the encoders and the sniffer, so they can never disagree.

detect_encoding is not bound in Python. The detection still happens — kg.deserialize(data) routes by magic internally — there is just no standalone Python call to ask which encoding a buffer carries.

deserialize — one decoder for any encoding

std::optional<Document> deserialize(std::string_view bytes, ParseError* err = nullptr);

Decode any native encoding. deserialize runs detect, then delegates to the matching parser: text, binary crate, or package. A compressed crate is inflated automatically. For package input only the Document comes back — call deserializePackage to also recover the asset bytes. Returns nullopt on Unknown encoding or a malformed body; on failure, if err is non-null, it is filled with a located diagnosis (and left untouched on success).

ParseError err;
if (auto doc = deserialize(bytes, &err)) {
    use(*doc);
} else {
    std::cerr << "parse failed at " << err.line << ':' << err.column
              << " — " << err.message << '\n';
}

Python's kg.deserialize(data) takes text (str) or crate (bytes), auto-detects, and returns a Document. On failure it raises PrismError rather than returning a sentinel — the located diagnosis is folded into the message.

try:
    doc = kg.deserialize(open("world.prism", "rb").read())
except kg.PrismError as e:
    print("could not read it:", e)

ParseError

struct ParseError {
    std::size_t line = 0;     // 1-based, text only
    std::size_t column = 0;   // 1-based, text only
    std::string message;      // why the parse stopped
};

A located failure. For the text encoding, line and column (both 1-based) point at where the parser stopped, and message says why — expected '}', unknown type 'flost', missing or unsupported '#prisma <version>' header. The text position is file-absolute: the #prisma header counts as line 1. For binary, package, or unknown input there is no meaningful position, so line and column are 0 and message is a short diagnosis (malformed binary crate (.prism), malformed package (PRSMZ), unrecognized format).

Python folds this into PrismError's message; there is no separate ParseError type in the binding.

deserializePackage — bundle round-trip

std::optional<std::pair<Document, AssetStore>> deserializePackage(std::string_view bytes);

Decode a PRSMZ bundle into both halves — the Document and its AssetStore. Returns nullopt unless the input begins with the PRSMZ magic and parses cleanly. This is the full round-trip partner to serializePackage; plain deserialize on package bytes gives you only the Document and drops the assets.

if (auto pkg = deserializePackage(bytes)) {
    auto& [doc, assets] = *pkg;
    const Asset* tex = assets.get(Path("/world/tex"));   // bytes + MIME back
}

deserializePackage is not bound in Python.

Compression

#include "kinogaki/Compress.h"
std::string compress(std::string_view raw);
std::optional<std::string> decompress(std::string_view packed);

Core ships its own LZ coder — an LZSS-family sliding-window codec — with no third-party dependency: Core owns every line. It is built for the format's large value payloads, where runs and repeats pack well; random data passes through with small overhead. The compressed blob is self-describing (it carries the original length), and decompress is bounded — it fails closed on any corrupt, truncated, or hostile input and never emits more than the declared original length, so it returns nullopt rather than over-reading.

You rarely call these directly. They drive the compress flag on serializeBinary: the crate body is run through compress, a flag bit is set in the plain header, and deserialize reads the flag and inflates the body before parsing. A packed .prism opens exactly like an unpacked one.

std::string packed = serializeBinary(doc, /*compress*/ true);   // flag bit set
Document back = *deserialize(packed);                            // auto-inflated, same Document

The standalone compress / decompress functions are not bound in Python. Reach the compressor through the binary crate's compress flag.

Files and codecs

These free functions move bytes in memory. To read and write files, and to cross into foreign formats (JSON, Markdown, HTML, SVG, raw bytes), go through the codec layer — doc.save(path), doc.load(path), and Codec::Auto, which picks the codec from the file extension. See codecs and the binary encoding for the file-facing story; the native encoders here are what those paths call underneath.