1. Summary
CyclePack is an asset archive and virtual file system for games and real-time applications. It is a Rust library with a C API, beta plugins for Unreal Engine 5, Unity 6 and Godot 4, and an embedded Lua 5.4 runtime for pipeline scripts. A game mounts archives and reads files from them by path.
- Stored files are read in place. A file stored uncompressed and unsealed comes back as a borrow of the memory-mapped archive, with no copy and no lock on the read path.
- Everything else is checked. In the CPK3 archives
cyclepack-studiopacks, a compressed or sealed file is decoded into a buffer and compared with its BLAKE3 digest. - Archives can be sealed under your own 32-byte key with AES-256-GCM or ChaCha20-Poly1305. Without the key nobody can list or read the files, and any edit makes the open or the read fail.
- Sealing is not DRM. A game that opens its own sealed archives ships the key, and whoever extracts it reads everything.
This public overview covers the architecture at block level, the security model and its limits, what was measured, engines, platforms and licensing. The format specifications, key derivation, module internals, the C API reference and the test suites are in the technical reference that comes with SDK access (section 8).
Everything needed to judge the security model is here. Its protection rests on the keys, not on a secret design.
DirectStorage, GPUDirect Storage, NVMe ZNS, NVMe computational storage and RDMA over RoCE v2 are models that run on the CPU in this release, labelled Emulated; the library links no DirectStorage, CUDA or RDMA verbs library. Two hardware interfaces are real: Linux io_uring, used by the Rust read backend when the kernel allows it (run so far on one kernel, in a container), and CXL memory that Linux exposes as a DAX device, mapped directly (not yet run on a real device; without one, CXL is emulated too).
Ask the binary rather than this page: cyclepack doctor prints a verdict for every capability on the machine it runs on, and cyclepack_query_capability_backing returns it to C callers (2 Native, 1 Emulated, 0 Unavailable).
cyclepack doctor on an Apple-silicon Mac, 2026-10-02. The seven native ones are formats and algorithms implemented in software. Run it on your own machine.2. The Problem: Per-File Loading and Open Archives
Most engines start with a loader that opens and reads one file per asset, and an archive format anyone can parse. Four costs follow, and each has an answer in CyclePack:
3. Architecture at Block Level
The core is Rust. Engines reach it through a C API (one header, a shared or static library), the engine plugins, or the Rust, Node/Deno/Bun and Python packages. The asset pipeline, the part that changes per project, is Lua 5.4 run by the same library.
3.1 Mounts and Layers
Archives mount under path prefixes, each with a priority. The highest-priority mount that has a path wins, so a patch, a DLC or a mod overrides the base game without rewriting it; copy-on-write overlays layer changed files over a base archive. An optional mount type looks paths up with a learned index; no lookup time has been measured for it.
3.2 Streaming Containers
The library writes and reads containers for Gaussian splats, meshlet cluster hierarchies, neural textures and prebuilt bounding volume hierarchies, and keeps the tile residency of sparse virtual textures whose tiles are ordinary archive entries. Culling, level-of-detail selection, tile residency and ray queries run on the CPU; uploading, binding and drawing on the GPU are the engine's job. Two limits:
- Neural textures: the bundled encoder downsamples a texture into the container and trains no decoder, so no compression ratio is claimed. Decoding runs on the CPU.
- DirectStorage: the DirectStorage-shaped container holds LZ4 blocks decoded on the CPU, which a GPU decompressor cannot read. A native Windows backend is planned and needs Windows CI.
3.3 Scheduling and I/O
A scheduler orders requests in priority bands with deadlines, merges overlapping reads of the same asset, cancels, and applies backpressure. A zone-graph prefetcher adds speculative, cancellable requests; its hit rate has not been measured. This release has no path into GPU memory, so an emulated backend refuses a request meant for GPU memory unless the host allows it, and then the bytes land in host memory.
3.4 Hardware Paths
Every capability reports what serves it on the current machine (Figure 3.2). Linux io_uring is reachable from the Rust API only; the C API and the plugins read through memory maps. CXL memory has three placements: the process's heap, a shared mapping on one host, and a Linux DAX device, the only one reported as Native. None adds coherency across hosts, and nothing makes a write durable on persistent memory.
3.5 Around the Core
- Live updates: a new generation of the asset tree is published with one pointer swap; readers keep their snapshot, and the host can roll back.
- Schema code generation:
cyclepack codegenturns one schema into packed, layout-identical structs for C++, C#, Python, Go, Rust and Lua. - Off-thread reads: a worker pool runs reads off the caller's thread. The Python package awaits them without blocking asyncio; the Node package's reads are synchronous.
- Telemetry: recorded in process. The host exports it as Prometheus text or OTLP JSON; the library opens no socket.
- Remote chunks: remote reads go through a range fetcher the host implements. Fetched chunks are cached in memory and on disk, where a BLAKE3 checksum catches local damage; the fetch itself authenticates nothing. A peer-delivery model checks each chunk against its BLAKE3 address before storing it.
4. The Read Path
Figure 4.1 is what the code does when a host asks for a file. No GPU and no storage controller takes part.
- Locking: in Rust, reads take no lock and many threads can read at once; mounting needs exclusive access. The C API adds no lock of its own, and its contract says one handle is not used from several threads at once unless a function says it may be, which no
cyclepack_vfs_*function does: a C host serialises its calls on one handle. - Verification: a borrow is not hashed. Run
cyclepack verifywhen you install an archive: it decodes every entry through the copying path and compares each with its digest. Archives written from Lua use the older CPAK container, which records no digests, so neither kind of read checks them. - Sealed archives lend nothing zero-copy, with or without the key. Every read of a sealed entry is a checked copy.
5. Security Model and Its Limits
This section is meant to be complete enough to evaluate on its own. Four layers apply (Figure 5.1); none depends on a TPM, a secure enclave or a drive controller.
5.1 What Sealing Protects
Hidden Without the Key
Every path, every per-entry size, every digest, and where one entry ends and the next begins.
Bound to the Key
Every header byte, the manifest, and every entry together with the path it is stored under. An entry that is moved, swapped, dropped or copied in from another archive fails.
Visible Without the Key
The magic, version and flags; the cipher and key-derivation identifiers; the archive UUID; the generation; the total payload and manifest sizes. The manifest is padded, so its size gives only a rough entry count. At run time, anyone watching the reader's I/O sees where entries start and end.
The ciphers are AES-256-GCM and ChaCha20-Poly1305. A key is exactly 32 bytes; any other length is refused, never padded or cut. Each archive derives its own keys from yours with HKDF-SHA256, every entry gets a fresh random nonce, and digests inside a sealed archive are keyed, so they cannot be matched across archives. A wrong key fails at open. Anyone can read the generation, but only a keyed open authenticates it. The key is given at open and nowhere else: no call adds a key to an archive that is already open, in any of the C, Rust, Node and WASM interfaces.
5.2 Older Archives
Every keyed open takes a minimum generation, and an authenticated archive below it is refused. The library keeps no state between runs, so the floor comes from the host: a constant built into the game, or the highest generation it has opened. A floor of 0 accepts any archive.
5.3 Whom It Does Not Protect Against
- The key holder. A game that opens its own archives ships the key, and whoever extracts it reads everything. Sealing stops keyless datamining and tampering, on disk, on a CDN or on a mirror. It is not DRM.
- Anyone who can read the process's memory. Keys and decrypted bytes live in ordinary process memory. CyclePack wipes its own key material when dropped, but not the internal state of third-party crypto libraries, nor copies the compiler leaves on the stack.
- A substituted archive. Any archive sealed under the same key at or above the floor opens, a DLC archive in place of a base one for instance. Use one key per archive family.
- A host that ignores a failed mount, or a missing archive. Reads then fall through to the layers below, and the library cannot notice an archive it was never asked to open. Mount an explicit list, and treat a failed keyed mount as fatal.
5.4 What Sealing Costs Today
- Sealed entries are never lent zero-copy (section 4).
- Every repack draws fresh nonces, so two sealed releases share no chunks and chunk patches save nothing between them.
- Chunked entries cannot be sealed yet.
- The engine plugins mount unsealed archives only. For a sealed one, call the C API's keyed mount,
cyclepack_vfs_mount_mmap_with_key, from your own code. - Lua and the Python package have no keyed open, and archives written from Lua use the older CPAK container, which has no per-entry digests and cannot be sealed.
5.5 Integrity Without Sealing
Every archive cyclepack-studio packs records a BLAKE3 digest per entry; copying reads check it, borrows do not. In an unsealed archive the digests are plain, so anyone who can rewrite the file can rewrite an entry and its digest together: they detect damage, not a deliberate edit. Authentication needs sealing or, for updates, a signed patch manifest.
5.6 Untrusted Input and the C Boundary
- Sizes and counts read from a file are bounded before anything is allocated for them.
- Decompressing an entry stops at the size the archive declares, and at 4 GiB natively or 256 MiB in WASM.
- Paths are sanitised. Extraction refuses a path that escapes the output directory, and packing does not follow symlinks.
- All 166 C exports run inside a panic guard, and release builds keep unwinding enabled so it can catch: a Rust panic reaches the host as an error code.
5.7 Scripts
The default Lua runtime is trusted: scripts get the full standard library and host file access, like any program the user runs; the cyclepack CLI runs pack scripts this way. Mods and downloaded pipelines belong in the sandboxed runtime. It removes process control, environment access and module loading, accepts text chunks only (no precompiled bytecode), and confines CyclePack's file bindings to folders the host chooses, re-checking each path after resolving symlinks. In both runtimes, CyclePack's buffers and file reads are capped at 512 MiB and its decompression helpers at 256 MiB. A script's syntax error, nil dereference or stack overflow comes back to the host as an error, and the next script on the same runtime still runs.
The sandbox does not limit CPU time or the Lua heap, and it runs inside the host's process. Run untrusted scripts in a separate process if a runaway script must not stall the host.
5.8 Updates
The library provides the pieces of an update, not an updater:
- Patches built by content-defined chunking carry only the chunks that changed.
- Signed patch manifests: an RSA signature (PKCS#1 v1.5 over SHA-256) under a public key the host supplies; the library sets no minimum key size, so supply a key of 2048 bits or more. A received manifest stays unusable until the signature verifies (only its claimed package id can be read, for logging); skipping the check takes a method named for exactly that, so every skip can be found by searching the code.
- A limit in this release: the signed digest joins text fields and list items without their lengths, so a signature also verifies the same text split differently (a removed object
ablisted asaandb). Treat the boundaries between list items as unauthenticated until that is fixed. - Atomic file replace: write a temporary file, fsync it, rename it over the target, then sync the directory where the platform allows it.
- Generations with rollback.
The host downloads the patch, supplies the key and connects the steps; the library downloads nothing. A crash-consistency suite simulates an interruption at each step of a patch and requires the old or the new generation to be active, never a mix.
5.9 Web Mode (Preview)
Web mode is a small sealed container, ChaCha20-Poly1305 over DEFLATE, for the JSON a web page loads. The server derives a bucket key per time slot from a secret that never leaves it; the page fetches the keys it needs and decodes in a WASM decoder. Without the bucket key, a reader sees only the header (key id, generation, payload kind, codec, whether a context is bound) and the payload's length.
- The secret must be random, 32 bytes or more, not a passphrase: any bucket key lets someone test guesses of the secret offline.
- Every page that decodes holds the bucket key, so the person using the page, and any script in it, can read the data. Web mode raises the cost of scraping; it is not DRM.
- Authentication shows only that a key holder wrote the container, and every page is one. Serve containers over HTTPS, and treat a payload as untrusted input even after it authenticates.
- A context binds a container to its purpose, so one item's data cannot be served as another's, but not to a time: an older container for the same context still opens.
- The page's decoder caps decoded output at 64 MiB by default and refuses nesting deeper than 512 levels. A hostile payload can still make it hold a few times that limit (at most 281 MiB of WASM memory in the cases measured), which the page does not get back; a page that knows its payload sizes lowers the limits.
- The same rule holds for the WASM archive reader: a key used inside a page can be read by whoever runs it. Issue short-lived per-session keys after server-side authorisation.
5.10 Not Covered in This Release
- The computational-storage model's "decrypt" step is a repeating-key XOR on the CPU, whatever cipher a command names. It protects nothing; sealing is what protects content.
- No Miri, Kani, Loom or sanitizer runs.
- Nine fuzz targets exist; they were not run for this paper.
- Release artefacts are not signed, and no SBOM or provenance attestation ships.
- No confidential-computing support.
6. What Was Measured
Every figure names the command behind it. Commands ran on macOS 26.2, arm64 (Apple silicon); "not re-run" marks a figure carried over from an earlier run of this release. The library's tests also run on Linux (arm64, in a local container); Windows is not tested.
| Measure | Result | How it was obtained | Notes |
|---|---|---|---|
| Capability backing | 7 of 14 native, 7 emulated, 0 unavailable | cyclepack doctor |
2026-10-02, debug build; the answer differs by host |
| C exports | 166, each behind a panic guard | python3 scripts/generate_abi_manifest.py --check for the count and the header; a scan of the C API source for the guards |
2026-10-02; no test enforces the guard yet |
| Line coverage | 80.2% (floor 77%) | scripts/coverage.sh |
2026-09-24, before web mode; not re-run |
| Patch size | 17,553 bytes for a 153,639-byte file | cargo run --release --example cross_fastcdc_subfile_patch |
One changed chunk; re-run 2026-10-02 (debug build), same figure |
| WASM archive reader | 132,833 bytes gzipped (358,792 raw) | scripts/build_wasm.sh |
Budget 250 KiB; gzip -9 of the 2026-10-02 build, which was not rebuilt for this page |
| Web-mode page decoder | 40,152 bytes brotli | scripts/build_wasm_web.sh |
Script 3,286, module 36,866; budget 64 KiB; brotli 11 of the 2026-10-02 build, the same as on 2026-10-01 |
| Web-mode container vs. brotli 11 | 0.43 to 1.10 of the brotli size | node scripts/web_size_bench.mjs |
Six synthetic fixtures, from a numeric chart (0.43) to a small crossword grid (1.10); 2026-10-01 |
| Converter parity | 25 of 25 matched | node scripts/studio_parity_test.mjs |
Native and WASM outputs byte-identical, except a packed archive's random UUID; not re-run |
| Defects fixed in review | 135 over five rounds | The changelog | Before web mode, which had its own review on 2026-10-01 |
No latency or throughput figure is published. The benchmark suite times in-memory and page-cached operations, not a disk; run cargo bench --bench vfs_bench on your own machine. When a benchmark reads a real archive from storage, its figure will be published with its command and machine.
Fault-injection tests corrupt and truncate archives and require an error, not a panic; crash-consistency tests are described in section 5.8. Neither simulates device faults such as an NVMe error or a network partition. Every suite, and how to re-run it, is in the technical reference.
7. Engines and Platforms
One library and one C header serve every integration. The three engine plugins are Beta, and all three mount unsealed archives only (section 5.4).
Unreal Engine 5.4+
Loads through Unreal's file layer and I/O dispatcher; reads CyclePack archives, not .pak or .utoc files. Not yet run inside Unreal: only part of its code has been checked to compile, against stand-in headers.
Unity 6
A UPM package that mounts archives and reads files from C#. Tested outside the Editor, on .NET 8 on macOS; Editor and IL2CPP runs are still to come.
Godot 4.3+
A GDExtension that mounts archives and reads files from GDScript. Runs headless on Godot 4.7.2; exported games are not tested yet.
- C and C++: a shared or static library with one header and a CMake target, for engines without a plugin.
- Examples: 33 language driver examples call the C API; most of their tests skip when the toolchain is absent. Sixteen engine examples are starting points to copy, not qualified like the plugins; six are incomplete.
- Packages: the Rust crate; a Node, Deno and Bun package that binds part of the C API and reads synchronously; and a Python package that awaits reads on a worker pool.
- Browser: the WASM archive reader, built and smoke-tested; web mode, a Preview (section 5.9); and a WebGPU splat preview package that does no depth sorting and was not run for this paper.
For this release the library was tested on macOS (Apple silicon) and on Linux (arm64, in a local container). Not tested: Windows, Intel Macs, and Linux on x86-64 or on a physical machine. There is no console, iOS or Android build.
8. Licensing and the Technical Reference
The terms below are the ones in the pricing section; where this page and that section differ, that section is right.
- Tiers: Indie is $0 for studios whose revenue plus funding over the last 12 months is under $200,000. Pro is $2,500 per title, one time, for budgets up to $2,000,000, with the C++ drivers and 12 months of SDK updates. Enterprise starts at $15,000 per title, or studio-wide, for larger budgets, and adds the source code of each licensed release. No tier pays a runtime royalty.
- Evaluation: free for 30 days above the Indie threshold; no commercial release under it.
- Support: community on Indie, 48 hours on Pro, 8-hour priority on Enterprise.
- Enforcement: nothing in this release checks a licence. No command, tool or read path requires one.
- Offline: the library opens no network socket, and nothing is sent home.
This page stops at the block level. The technical reference that comes with SDK access goes the rest of the way:
- the CPK3 archive and CPKW web-container specifications, with their layouts and key derivation;
- the C API reference and the ABI stability policy;
- how the index, the scheduler and its cost model, the CXL regions and the streaming containers work;
- the evidence behind section 6: the command for each figure, the fault-injection and fuzz suites, and what has not been run.
Customers read it at Customer docs (sign-in required), signing in with the email address their access was granted to: an emailed code, or Google or GitHub with the same address. To get access, request it with the button below; it comes in the team's email reply.