Blast Processing

A large ROM collection gives a collection manager plenty of work: reading catalogues, checking files, finding matches and keeping the interface responsive. Our performance work focuses on where that work happens, how much data it moves, and whether it needs to happen again.

A native Rust application

Archivist is written in Rust, including its desktop interface. Catalogue processing, verification and launch preparation live in Rust libraries that the application calls directly.

Rust gives us control over the costs that matter when a collection grows. We can borrow a record instead of cloning it, reuse a buffer instead of allocating another one, and share immutable data between workers. Ownership makes those choices explicit in the code.

That control is useful because collection work has several different bottlenecks. Reading a disk, calculating a digest and drawing a list each need a different approach. We keep those operations separate so a slow storage operation doesn’t require the interface to wait for it.

GPUI removes the UI IPC boundary

We moved away from Tauri to GPUI so the interface and application logic could run together in Rust. The old browser interface required interprocess communication, or IPC: data had to cross a boundary between the Rust backend and the webview.

The catalogue bytes already occupy memory buffers in the Rust process. With Tauri, those bytes have to be copied and sent across an IPC socket, then copied into buffers owned by the JavaScript process. When the catalogue changes, the changed data has to travel through that same path to update the browser’s copy.

With GPUI, the application and rendering code run in one Rust process. They can access catalogue memory through references in the same address space. Updating the interface no longer requires sending those catalogue bytes to another process and maintaining its separate copy.

Tauri IPC buffer copies and GPUI memory access In the Tauri approach, catalogue bytes in a Rust process memory buffer are copied through an OS IPC socket into a separate JavaScript process memory buffer. Changed bytes must follow the same route. With GPUI, Rust and GPUI code run in one process and access the catalogue bytes in that process's memory; changes do not need to cross an IPC socket. Tauri approach Rust process Memory buffers JavaScript process Separate memory buffers OS IPC socket Catalogue bytes Socket buffer Catalogue bytes copy copy On catalogue changes Changed bytes Socket buffer Changed bytes copy copy Initial data and later updates cross the same process boundary GPUI approach Rust process · one address space Rust application code GPUI rendering code Catalogue bytes in RAM writes reads
Tauri copies catalogue bytes between separate process buffers through IPC. Updates take the same route. GPUI accesses the catalogue within the Rust process.

Fewer catalogue copies with rkyv

A normal buffered file read uses two places in physical memory. The operating system brings file pages into its page cache, then copies the requested bytes into a buffer owned by the application. The application reads its copy through the virtual addresses of that buffer.

A read-only memory mapping gives the process virtual addresses backed by the file’s pages. When a page is resident, the CPU accesses the same physical RAM page held in the OS page cache. A page that isn’t resident must still be fetched from storage when needed. Mapping removes the extra copy into an application-owned input buffer.

rkyv handles a different part of the problem: the format of the bytes. Its archived structures can be accessed directly in their stored layout. That works in a suitable byte buffer as well as in a file mapping. Deserialising into new structs and strings would be an additional allocation step; direct archive access avoids that step.

Archivist’s mapped catalogue work combines these two properties. The operating system supplies file-backed pages, and the runtime accesses rkyv’s archived fields in those pages. It can use the catalogue without first copying the input bytes into a separate buffer or rebuilding the archive as owned objects.

The same file bytes, with and without a read-buffer copy For a buffered read, file bytes A and B occupy physical pages in the OS page cache and are copied into separate physical pages backing the application's byte buffer. The process's virtual addresses map to the copied buffer pages. For a read-only file mapping, the process's virtual addresses map directly to the file-backed physical pages in the OS page cache. Both paths can access rkyv data directly; only the buffered read duplicates the bytes into an application buffer. read(): copy file bytes into an application buffer Process virtual addresses Application byte buffer address A address B page-table mappings Physical RAM OS page cache Application buffer pages File bytes A File bytes B 52 4B … 07 00 … File bytes A File bytes B 52 4B … 07 00 … copy bytes The same byte contents occupy two separate sets of RAM pages Read-only mmap(): address the file-backed pages Process virtual addresses Mapped file range address A address B page-table mappings Physical RAM OS page cache File bytes A File bytes B 52 4B … 07 00 … CPU loads access these pages through the mapped addresses
Solid arrows copy bytes between physical pages. Dashed lines map process virtual addresses to physical pages. rkyv accesses the archived fields at those addresses.

Assembly and hardware-assisted hashing

Verification needs to establish what is actually in a file. Names and folder paths can help locate it, but matching against preservation catalogues depends on content digests.

Our hashing optimisation work reaches down to hand-tuned assembly and CPU intrinsics. We prioritise hardware-accelerated implementations where the processor supports them, with instruction selection matched to the digest being calculated. The required digest still comes from the catalogue: a faster, unrelated algorithm can’t replace the hash we need to compare.

There is also useful work to avoid before reaching the hashing routine. Archivist calculates the requested digests from the same stream of file data, feeding each buffer to the selected algorithms. It doesn’t need to read the file separately for every digest.

Faster hashing matters most when digest calculation is the bottleneck. When storage is supplying bytes more slowly than the processor can consume them, avoiding another read is the more useful improvement. Both cases shape the verification pipeline.

CPU execution paths for SHA hashing A software SHA implementation uses general-purpose ALU operations on message and state registers. A SHA-capable processor can perform SHA work in a specialised execution unit. Both paths update the state for the same digest. Conventional software path CPU-accelerated path CPU execution CPU with SHA support Message + state registers Updated hash state Message + state registers Updated hash state General-purpose ALU rotate · XOR · add ALU operations SHA execution unit Dedicated SHA instructions SHA rounds Same algorithm and digest; different CPU instruction paths
SHA hashing illustrates the approach: select specialised instructions when the CPU supports the required algorithm.

Hashing ahead of time on the NAS

Historian is an always-on daemon for storage appliances that we’re developing alongside Archivist. It moves collection scanning and hashing to the machine that holds the files.

The plan is to hash items as they arrive in storage and keep those observations up to date as the collection changes. When you open Archivist, it can use the results already collected by Historian to match files against the catalogue. The desktop won’t need to pull every file across the network and hash it before it can tell you what you have.

Listing a large directory tree over NAS can be expensive before any file contents have been read. Hashing then needs to read those contents. Historian maintains the file list and digests on the storage appliance, so the desktop can retrieve prepared results and perform the comparatively cheap digest lookup. Detecting changed files and refreshing their digests is part of the daemon’s job, so a changed file doesn’t keep an old identity.

A collection shouldn’t need to be read from scratch each time you want to play something. Historian is how we intend to make the work of checking a NAS collection happen as it grows, leaving the desktop to use the results.

Historian moves expensive work before the collection is opened Widths indicate qualitative work. Without Historian, opening the collection triggers an expensive NAS file listing, expensive full-file hashing and a cheap digest lookup. With Historian, local file tracking and the same hashing work happen on the NAS before opening. The desktop receives the saved inventory and digests and performs the same cheap lookup. Hashing bars have identical widths and colours in both cases, as do the digest lookup bars. Without Historian Before opening After opening Desktop List Hash file contents Match digests Listing and hashing start when you open the collection With Historian Before opening After opening NAS daemon Track arrivals locally Hash file contents Desktop Saved file list + digests Match digests Bar widths show indicative relative work NAS file listing: expensive · Full-file hashing: expensive · Digest lookup: cheap
The dashed line marks opening the collection. Historian does the hashing earlier and maintains the file list on the NAS. The desktop uses those saved results for the comparatively cheap digest lookup.

Privacy

Archivist uses privacy-focused site analytics and remembers which Archivist pages and policy panels you visit before signup. If you join the release list, we store those details with your address, choices, delivery status and any campaign label in the link you followed. We don’t sell personal information or use advertising pixels.

Read the full privacy policy

Game software ownership

You supply your own legally obtained game software. Archivist sorts, verifies, displays and launches it.