A virtual filesystem for retro game collections that can be mounted and transformed on-the-fly without duplicating files.
RetroMount is an experimental Rust project for building a virtual filesystem over retro game collections.
It allows ROMs, disc images, and archives to be mounted into alternative filesystem layouts on-the-fly, enabling different devices and emulators to view the same underlying collection in different formats without duplicating files.
Implemented examples include:
- Present supported PS2 DVD CHD/ISO inputs and PS2 CD ISO/CUE/BIN inputs as OPL-compatible ISO files
- Expose ROMs from ZIP archives through flat or grouped layouts
- Present supported PS1 CUE/BIN, ISO, and CHD inputs in a DuckStation layout
- Load custom presentation layouts from versioned YAML files
The long-term goal is a flexible input → transformation → output pipeline backed by a FUSE filesystem.
Retro game collections are often stored in formats that are not ideal for every device or emulator.
For example:
| Storage format | Works well for | Problems |
|---|---|---|
| ZIP archives | ROM management | Many emulators cannot read zipped files |
| CHD images | Space-efficient storage | Some tools require ISO/BIN files |
| CUE/BIN discs | Accurate disc representation | Some systems prefer CHD |
| Raw ROM sets | Simple emulators | Hard to manage at scale |
Traditionally, users solve this by duplicating their collection in multiple formats:
ROMs/
snes/
game.zip
game.sfc
or
PS1/
game.chd
game.iso
This wastes storage space and creates maintenance headaches.
RetroMount solves this by providing a virtual filesystem layer that can dynamically transform game data into the format required by the target system.
Example:
Storage (single copy)
│
▼
/roms/ps2/game.chd
RetroMount can present that file through its OPL presentation as:
/mnt/opl/DVD/game.iso
All backed by one underlying file.
Retromount is under active development and progressing through a structured roadmap.
- Phase 1 — Foundations
- Phase 2 — Core abstractions and initial pipeline
- Phase 3 — Pipeline consolidation and normalized content model
- Phase 4A — Mountable filesystem (FUSE integration)
- Phase 4B — Consumer views (multiple presentations)
- Phase 4C — Naming and conflict resolution policies
- Phase 4D — Extensibility and configuration layer
- Phase 5 — Runtime encoder plugin architecture
- Phase 6 — Declarative presentation specifications
- Versioned YAML presentation files and external presentation loading
- First practical consumer target — live PS2 DVD CHD to OPL-compatible ISO presentation
- First PS1 consumer target — the built-in
duckstationpresentation, with integration coverage for CUE/BIN, ISO, CHD, stored ZIP, multi-disc, and SBI inputs
- Optical-media capability expansion beyond the implemented OPL PS2 and DuckStation PS1 paths; materialized CHD encoding is deferred to caching work, and OPL/POPS integration still requires research
- Additional optical-disc inputs and consumer presentations
- Performance, caching, and optimisation after representative input and media workloads are available
- Advanced encoders and external integrations (e.g. torrent compatibility)
You need Git, a C compiler/linker, and Rustup. This
repository's rust-toolchain.toml selects the current stable Rust toolchain;
no minimum supported Rust version (MSRV) is declared.
To use the mount command, the Linux host must also provide kernel FUSE
support (/dev/fuse) and the fusermount3 helper (or fusermount on a FUSE 2
system). On Debian or Ubuntu, install the required system packages with:
sudo apt-get update
sudo apt-get install build-essential fuse3 gitOther distributions need equivalent packages providing Git, a system linker,
and the FUSE runtime and mount helper. The current Linux build uses fuser's
pure-Rust mount path, so libfuse development headers and pkg-config are not
required. If FUSE is installed but /dev/fuse is absent, load the kernel module
with sudo modprobe fuse.
git clone https://github.com/lloydsmart/retromount.git
cd retromount
cargo build --lockedRustup reads rust-toolchain.toml and installs stable Rust if necessary.
Inspect the tracked LICENSE file with the flat presentation:
cargo run --locked -- inspect LICENSE --presentation flatThis runs the complete pipeline without mounting anything. The report describes the input, decoded and normalized content, and proposed virtual filesystem. Its final section should be:
Output VFS:
/
LICENSE.bin
The .bin entry is the name Retromount would expose; the command does not write
a new LICENSE.bin file to the checkout.
RetroMount can expose a file, ZIP archive, or directory as a read-only FUSE filesystem. The mountpoint must already exist. For example, from the repository root:
mkdir -p /tmp/retromount
cargo run --locked -- mount LICENSE /tmp/retromount --presentation flatThe Retromount process stays in the foreground while the filesystem is mounted. In another terminal, inspect it and then unmount it:
ls /tmp/retromount
fusermount3 -u /tmp/retromountThe ls command should show LICENSE.bin. On a FUSE 2 system, use
fusermount -u /tmp/retromount instead. After unmounting, the foreground
Retromount process exits.
- FUSE mounting is Linux only
- the mounted filesystem is read-only
- output reflects the same structure shown by
phase3-previewandinspect - performance optimisations are planned in future phases
During development, invoke commands with cargo run --locked -- as shown
below. If you run an installed retromount binary, omit that prefix. The
current CLI parser is order-sensitive; when combining options, keep them in
the order shown.
cargo run --locked -- mount <input> <mountpoint> \
[--presentation <name-or-yaml-file>] [--plugin-dir <dir>]mount runs the pipeline for a regular file, ZIP archive, or directory, then
serves the resulting VFS at the existing mountpoint until it is unmounted. The
default presentation is grouped.
cargo run --locked -- inspect <path> [--json] \
[--presentation <name-or-yaml-file>] [--plugin-dir <dir>]inspect runs the pipeline without mounting and reports the input objects,
decoding and normalization results, and output VFS. --json emits the pipeline
trace as JSON instead of the text report.
For mount and inspect, --presentation accepts the built-in names
duckstation, flat, grouped, and opl, or the path to a versioned .yaml
or .yml presentation file. The older --view spelling remains a compatibility
alias. --plugin-dir loads runtime encoder plugins from the supplied directory.
There are no CLI --platform or --media options; those hints are available
only through configuration.
cargo run --locked -- phase3-preview <path> [--plugin-dir <dir>]phase3-preview runs the default grouped presentation and prints only the
proposed VFS tree. It accepts a regular file, ZIP archive, or directory. The
development-era command name is transitional, and this command does not
currently accept --presentation or --json.
Running retromount with no arguments reads retromount.yaml from the current
directory. For each configured view it builds the pipeline and logs the
resulting VFS tree at info level (RUST_LOG=info). This path does not mount the
tree at the configured mount path; use the explicit retromount mount
command to create a FUSE mount.
Create retromount.yaml:
- name: ps1
source: /roms/ps1/Ridge Racer.cue
mount: /mnt/retromount/ps1
platform: ps1
presentation: grouped
- name: ps2-opl
source: /roms/ps2/Test Game.chd
mount: /mnt/retromount/ps2
platform: ps2
media: dvd
presentation: opl
- name: snes
source: /roms/snes
mount: /mnt/retromount/snes
platform: snes
- name: megadrive
source: /roms/megadrive
mount: /mnt/retromount/megadrive
platform: megadrive
presentation: flatFields:
| Field | Description |
|---|---|
name |
Logical view name used in log output |
source |
Source directory, archive, or disc image |
mount |
Required path that is currently logged but not mounted by this path |
platform |
Normalization hint (e.g. ps1, ps2, snes, megadrive) |
media |
Optional cd/dvd hint for ISO input in OPL or DuckStation |
presentation |
Built-in name or YAML file path (default: grouped) |
encoder |
Accepted by the configuration parser but currently unused |
Platform names are case-insensitive and accept friendly aliases.
Each configured view selects a presentation specification, which defines
filesystem structure and artifact requirements. Capability resolution then
selects the available encoders needed to materialize those artifacts; the
configured encoder field does not override that selection.
If not specified:
presentationselects a built-in name or versioned YAML file and defaults togrouped- the legacy
presenterfield remains a compatibility alias
This allows different views to present the same underlying data in different layouts without duplicating files.
Retromount uses one processing model for inspection, previews, and mounts. Input formats are interpreted before output choices are made, with a normalized content model separating source details from consumer-specific filesystem layouts.
Input discovery and enumeration
│
▼
Identification and format decoding
│
▼
Normalized content model
│
▼
Presentation compilation and planning
│
▼
Artifact resolution and materialization
│
▼
Read-only virtual filesystem (VFS)
│
└──► Optional Linux FUSE exposure
The input path first selects a source enumerator. A regular file produces one object, a directory is walked recursively, and a ZIP archive is opened as a container whose non-directory entries become objects. Each object retains the metadata and byte access needed by later pipeline stages. At this stage ZIP is a discovery concern: it determines what input objects are available, not what those objects mean.
Each object is then identified and passed to a decoder that understands its source format. CUE, CHD, and ISO semantics are handled here, including disc layout and any safe track-aware or contiguous views of the media. Ordinary ROM, text, and byte content follows the same identify-and-decode boundary. Discovery therefore answers "what content is available?", while decoding answers "what does this content represent?"
Decoders preserve source-format structure, but downstream presentation does not consume decoder-specific results directly. Normalization creates a shared, presentation-agnostic model of games, ROMs, discs, text, and bytes. It applies semantic information such as platform and disc order, groups related discs into games, and prevents files consumed by compound inputs from also appearing as independent content.
This boundary lets the same interpreted content feed different consumer views without reparsing inputs or embedding filenames, directory layouts, or output formats in the core model.
A command or configured view selects a built-in or versioned YAML presentation before the run is composed. The presentation specification itself is applied after normalization. It declares which normalized content to select, how to arrange and name it, and which output artifacts and formats are required. Current built-in consumer presentations may also provide platform or optical media hints when the run is composed.
The presentation compiler combines that specification with naming and conflict policy to produce a concrete plan of directories, files, multi-file artifact sets, and generated items such as multi-disc playlists. The plan describes the desired result and its artifact requirements; it does not choose or implement an encoder.
For every planned artifact, the host compares its required content type, format, and features with the capabilities advertised by built-in and runtime-plugin encoders. Resolution selects an encoder deterministically; that encoder then materializes the requested representation. Materialization can preserve a source-backed file, generate inline content, or provide a reader-backed view, so it does not necessarily copy data into a separate output file.
The materialized entries become a read-only VFS tree. Inspection and preview commands report that tree directly. On Linux, the mount path indexes the same tree as filesystem nodes and gives it to a thin FUSE adapter, which handles directory traversal and delegates file reads to the existing VFS reader layer. FUSE does not repeat discovery, decoding, presentation, or encoding decisions.
For source navigation, orchestration lives in src/engine/pipeline.rs; input
enumeration and decoding in src/input; normalization, semantic content, and
VFS primitives in src/core; presentation planning, capability resolution, and
materialization in src/output; and FUSE integration in src/mount.
Discovery enumerates all regular files and ZIP entries; it does not
automatically exclude .DS_Store, Thumbs.db, __MACOSX/, or other names.
Presentation file rules can constrain normalized games with source_formats
and excluded_source_formats. The compiler derives each game part's format
from its source extension (chd, iso, cue, bin, or zip), and every part
must satisfy the rule. These constraints choose which presentation rule emits
an artifact; they do not filter discovery or convert the source. The built-in
duckstation presentation uses them to pass existing CHDs through natively
while requesting CUE/BIN artifacts for other supported PS1 sources.
- Mountable virtual filesystem (FUSE)
- Multiple consumer views
- Presentation policies (naming, conflict resolution)
- Explicit presentation configuration
- Declarative
PresentationSpecmodel - Generic presentation compiler
- Flat and grouped layout parity
- Multi-disc playlists and preserved relative paths
- Spec-native
phase3-preview,inspect,mount, and plugin execution - Legacy presenter implementations retired
This project is licensed under the GNU General Public License v3.0 only.