A native Linux virtual filesystem for game mods. Eidos reproduces what Mod
Organizer 2's usvfs does on Windows - a clean, per-launch merged view of your
mods over a game's files - but built from native Linux primitives instead of
Windows API hooking, so games run under Proton/Wine without the usual
Wine-shoehorned mod-manager pain.
Status: early but real. The thesis is validated; the resolver and the read-write FUSE daemon are implemented, hardened, and covered by unit and real-mount integration tests. What remains is Proton launch integration, import-time casing, and performance validation on a heavy load order. See Roadmap.
MO2 is two things bolted together:
- A Qt mod-manager UI + plugin system. Cross-platform, ports easily.
usvfs- the feature that actually matters: a per-process virtual filesystem that merges your mods over the game directory without ever touching the real game files. It works by injecting a DLL and hooking the Windows NT syscall layer.
Part 2 is intrinsically Windows. It cannot be "ported" - the mechanism has no Linux equivalent. That is why every Linux option today is a compromise:
| Tool | Approach | Compromise |
|---|---|---|
| MO2 + Wine installers | run MO2 itself inside the Wine prefix | usvfs runs under Wine: slow, fragile, painful setup |
| Limo | native, symlink/hardlink deploy | writes links into the game dir, no per-process isolation |
| RadTux | native daemon + dll shim, symlinks on launch | still MO2-under-Wine, symlink write-back caveats |
The key insight: on Linux the game already runs in a box - Wine/Proton. So we do not hook anything Windows-side. We virtualize underneath Wine, at the Linux filesystem level. Present a merged view at the directory Wine reads from, and the game sees the merge for free, with no Windows-side injection at all.
Eidos reproduces the four properties that make usvfs valuable:
- Merged view - game data + N mod layers, priority ordered (last-enabled mod wins on conflict).
- Copy-on-write - the running game's writes (saves, regenerated configs) land in an Overwrite layer, never corrupting a mod source.
- Zero-touch - the real game directory is never modified.
- Per-process scope - the merge only exists for the launched game, inside a private mount namespace; the rest of the system sees the pristine directory.
+------------ per-launch user+mount namespace ------------+
Steam --> | eidos launch wrapper (`eidos %command%`) |
| | |
| v |
Proton <--| merged view <-- Eidos FUSE union (passthrough) |
sees ONE | ^ ^ ^ |
directory | overwrite mod N..1 game data |
+---------------------------------------------------------+
(the rest of the system sees only the pristine game directory)
Engine choice: a FUSE union filesystem in Rust, built around kernel passthrough (Linux 6.9+) so resolved reads bypass userspace and run at near-native speed, while we keep full control of the semantics OverlayFS cannot express (exact Windows-style case-insensitivity, precise write redirection, no lowerdir scaling wall). See docs/architecture.md for the full rationale, including why FUSE over OverlayFS for completeness and long-term stability.
crates/eidos the unified CLI front end (games / init / play)
crates/eidos-gui the iced GUI (Colony parchment look)
crates/eidos-core the layer-resolution engine (pure, unit-tested)
crates/eidos-fuse the read-write FUSE union daemon
crates/eidos-games supported-game catalog + Steam install detection
crates/eidos-launch per-launch namespace wrapper: run a game through the view
crates/eidos-instance instance model: global/portable, profiles, per-mod meta.ini, manifest, load order
crates/eidos-plugins ESP/ESM/ESL plugin load order (via esplugin) + plugins.txt
crates/eidos-conflicts per-file conflict analysis (winners / losers, per-mod state)
crates/eidos-install mod installer: 7-Zip extract + Simple wrapper-strip + meta.ini
crates/eidos-fomod FOMOD scripted-installer parser + condition/flag engine
crates/eidos-gamefeatures BSA/archive invalidation + per-profile INIs/saves at launch
crates/eidos-gamedef declarative per-game descriptor (one row per game; MO2 schema)
crates/eidos-ini shared low-level INI primitives (newline / section / key / edit)
crates/eidos-nexus Nexus Mods: v1 API client, nxm:// downloads, update checks
docs/architecture.md the design and the tradeoffs behind it
scripts/poc-overlay.sh runnable proof that the "virtualize under Wine" thesis
holds with native primitives, no root required
eidos games # supported games installed here (like MO2's list)
eidos init skyrimse # create a modding instance
# ...drop each mod as a folder into ~/.local/share/eidos/skyrimse/mods/...
eidos play skyrimse # show what would be mounted
eidos play skyrimse -- <command> # run <command> with the mods mounted over the gameplay mounts the instance's mods over the game's own Data directory (via a
bind-stash, so the daemon still reads the pristine files) inside a private
namespace, then runs the command through that view. Writes (saves, regenerated
configs) land in the instance's overwrite/ layer; the game install and every
mod source stay byte-for-byte pristine.
To launch the game itself through Eidos, set its Steam launch option to:
eidos play skyrimse -- %command%
Prefix it with WINEDLLOVERRIDES="d3dcompiler_47=n" if Proton needs native
d3dcompiler for shader compilation; Eidos merges that with any DLL overrides a
mod ships (ENB/ReShade/.asi loaders).
cargo run -p eidos-guiAn MO2-style first-launch wizard in the Colony parchment / burgundy look: welcome -> instance type (portable / global) -> game -> name & location -> summary -> create -> main screen.
The two-pane main window is built too: a profile picker (switch, or create a new one by copying the current), a mod list you filter, select, reorder and right-click for actions (enable/disable, send to top/bottom, open in explorer, visit on Nexus, reinstall, rename, remove, information), and Data / Plugins / Conflicts / Overwrite / Downloads tabs plus a Run button. The Plugins tab is the ESP/ESM/ESL load order (toggle each plugin); the Conflicts tab explains the per-file winners and losers; "Information..." opens a per-mod dialog (general, conflicts, filetree, notes).
To launch the game through the GUI, set the game's Steam launch option to its
absolute path (Steam doesn't see ~/.cargo/bin on PATH), forcing native
d3dcompiler so Proton shader compilation works:
WINEDLLOVERRIDES="d3dcompiler_47=n" ~/.cargo/bin/eidos-gui %command%
Eidos opens on the game's instance; click Run to launch it through the merged view. (The Run button shows this exact line, with the running binary's real path, if you press it outside Steam.)
No game required. It proves union + copy-on-write + zero-touch + per-namespace scope using only unprivileged OverlayFS in a user namespace (Linux >= 5.11):
./scripts/poc-overlay.shcargo test # eidos-core resolver unit tests
cargo build -p eidos-fuse # the read-only union daemonThe first --layer wins on conflict; the last is your pristine game data. The
mount needs only /dev/fuse and fusermount3 (no overlayfs, no Wine):
eidos-fuse --layer mod_b --layer mod_a --layer game_data /mnt/point
# ... read through /mnt/point ...
fusermount3 -u /mnt/point- Validate the under-Wine virtualization thesis (PoC)
- Layer-resolution engine + tests (
eidos-core) - FUSE union daemon (
eidos-fuse) - live-verified: merge priority, fall-through, case-insensitive reads, no root / no overlayfs / no Wine - Copy-up / Overwrite layer (writes) - live-verified: new files, edits to game files, and edits to mod files all land in Overwrite; the game install and every mod source stay pristine
- Deletes (whiteouts), rename, and statfs - live-verified: deleting a game or mod file hides it (via a whiteout in Overwrite) while the source stays pristine; rename works (saves), and df reports real free space
- Supported-game catalog + Steam install detection (
eidos-games) - live-verified: finds installed games across all libraries (incl. other drives) with their data dir + Proton prefix - Per-launch user+mount namespace wrapper (
eidos-launch) - live-verified: runs a command through a private union view; the host sees no mount, the game install and mod sources stay pristine, writes land in Overwrite - Instance management + unified
eidosCLI (games/init/play) - live-verified end to end: detect a game, create an instance, mount its mods over the game's Data dir, run a command through the view - Steam launch-option integration (
eidos %command%) with a real Proton game - GUI first-launch wizard (
eidos-gui, iced) - MO2-style screens (welcome -> portable/global -> game -> name -> summary -> main), Colony parchment theme - GUI main window (
eidos-gui) - two-pane MO2 layout: a profile picker, a mod list (enable/disable, reorder, per-mod conflict flags), and Data / Plugins / Conflicts / Overwrite / Downloads tabs with a Run button - Per-mod
meta.ini, byte-compatible with MO2 (eidos-instance) - existing MO2 instances round-trip unchanged (version / Nexus id / category / endorsed), with anupdate_availablecheck ready for a future Nexus crate - Self-describing instance manifest
eidos-instance.ini(eidos-instance) - records game id + schema version, so portable instances need no path-guessing - Profiles (
eidos-instance) - per-profile enabled set + load order over one sharedmods/pool, with a GUI picker (switch / new-by-copy) - ESP/ESM/ESL plugin load order (
eidos-plugins) - esplugin-parsed headers + MO2-parity ordering (masters first, master-before-dependent) and FormID indexes (FElight /FDmedium), written asplugins.txt/loadorder.txtinto the Proton prefix right before launch; surfaced in the Plugins tab - Per-file conflict detection (
eidos-conflicts) - one pass over the enabled layers builds a winners/losers tree + per-mod state (Overwrites / Overwritten / Mixed / Redundant), shown in the Conflicts tab and as per-mod flags in the mod list - Mod installer (
eidos-install+eidos-fomod) - the MO2 Simple installer (7-Zip extract, wrapper-strip via a Gamebryo ModDataChecker) plus the FOMOD scripted installer (UTF-16ModuleConfig.xmlparse + condition/flag engine), driven from a CLI (eidos install) and the GUI Install button + an interactive FOMOD wizard; writes a MO2-compatiblemeta.ini - Nexus Mods integration (
eidos-nexus) - connect with a personal API key (eidos nexus key), register thenxm://handler (eidos nxm --register) so the site's "Mod Manager Download" button downloads straight into the instance'sdownloads/(with MO2-format.metasidecars), check installed mods for updates (eidos nexus update, MO2's rate-limit-friendly updated-this-month strategy), and install from the GUI Downloads tab in one click (Simple or FOMOD wizard) - Tools through the VFS (
eidos tool) - run xEdit/FNIS/BodySlide through the same merged view, inside the game's Proton prefix, with no Steam%command%: a protontricks-style Proton resolver (config.vdfCompatToolMapping-> compatibilitytools.d / official Protons ->STEAM_COMPAT_*env), a per-instance tool list (MO2ExecutablesListparity, script extender seeded by default), and a run-target picker next to the GUI Run button. Tool output lands in Overwrite, so the next launch picks it up - the generate-then-play loop. (MO2 itself rebuilds its mapping per run, so per-launch mounts are exact parity.) - Per-game Bethesda features (
eidos-gamefeatures+eidos-gamedef) - all keyed off one declarativeGameDefrow per game (MO2'sIPluginGameschema): BSA/archive invalidation so loose mod files override the vanilla BSAs (without it BSA-packed mods are silently ignored); per-profile INIs seeded from the prefix then deployed/captured around launch; and per-profile saves via a namespace bind-mount of the profile's saves over the prefix (the Linux-native equivalent of MO2's usvfs save mapping, no prefix changes). The INI writing shares oneeidos-iniprimitive (MO2's single-QSettingsidea), keeping MO2meta.iniround-trips byte-for-byte - FUSE passthrough + rootless perf tuning (1 MiB readahead / max_write).
Passthrough negotiates
FUSE_PASSTHROUGHand engages when the daemon runs privileged (setcap cap_sys_admin+ep, taken via a bare mount namespace): the kernel then serves reads/mmap straight from the real backing file, which is what lets Windows SKSE-plugin DLLs image-map natively. Rootless it falls back to the daemon's own reads (correct, but DLLs may not load - kernel passthrough needs CAP_SYS_ADMIN in the initial user namespace) - Harden the daemon for real use - inode reference-counting +
forget, offset-stablereaddir(snapshot per directory handle), per-handlepread/pwrite(no re-resolve per syscall, lock released before I/O), case-insensitive whiteouts, opaque directories, POSIX errnos (rmdirENOTEMPTY,renameNOREPLACE/EXCHANGE),setattr(mode / timestamps), xattr passthrough (WineDOSATTRIB), symlinks,fsyncdurability. Covered by a real-mount integration suite that runs in a private namespace, including a writableMAP_SHAREDmmap round-trip, withsetattrguarded so the kernel's post-unlink attribute flush cannot resurrect a deleted file. (writeback_cacheis off - it broke loading Windows DLLs from the mount; passthrough serves DLL image-mapping instead.) - Runs a real heavily-modded Skyrim SE (110 mods) end-to-end under Proton
- all ~50 SKSE plugin DLLs (CommonLibSSE-NG included) load and run via the
mount, each writing its config into the Overwrite layer. Needed two
MO2/usvfs parity fixes: launch with CWD = game root (CommonLibSSE-NG
opens its address library by a CWD-relative path) and NTFS-like sorted
readdir(the Creation Engine's loose-file indexer assumes it) - Native DLL provisioning for Proton (
eidos-gamefeatures::native_dll) - no Proton flavour ships Microsoft's natived3dcompiler_47.dll(they symlink Wine's builtin HLSL stub, which Community Shaders / ENB / ReShade reject), so Eidos scans enabled mods' DLL import tables (theobjectcrate) and, when one imports it, deploys the bundled genuine MS redistributable into the prefix'ssystem32/syswow64and forces it native - unlinking Proton's builtin symlink first, backing up any displaced file, idempotent and best-effort - Modding-tool prerequisites (
eidos prereqs) - tools run in the game's shared Proton prefix through the merged view, so one prereq set covers them all. Tier 1 (bundled, zero network): the DirectX helpers BodySlide / DynDOLOD / CAO need (d3dx9_43/d3dx11_43/d3dcompiler_43), declared per-tool intools.iniand provisioned at launch. Tier 2 (consented download): the .NET / vcrun verbs Synthesis / Pandora / FNIS need, installed via the system winetricks pointed straight at Proton's own wine (bypassing the protontricks + Proton-GE mismatch), behind an expliciteidos prereqs <id> --install/ GUI "Tool Setup" button, recorded in a per-instance sentinel - GUI to MO2 daily-driver parity (
eidos-gui) - a mod-list filter box, click-to-select, and a right-click action menu (enable/disable, send to top/bottom, open in explorer, visit on Nexus, reinstall, rename, remove, information); an interactive Plugins tab (toggle ESP/ESM, persisted toplugins.txt); a per-mod information dialog (general / conflicts / filetree / editable notes); a Version column; and wired Nexus / Change Game / Settings / Tool Setup toolbar buttons - Casing normalization at mod-import time
- Mod-list separators + categories, and LOOT-based plugin auto-sort
The manager layer above the VFS is complete per the MO2 + usvfs study that drove
this work (docs/master-pieces.md, all 6 master pieces
done): the mod installer (Simple + FOMOD wizard), plugins, conflicts, profiles,
meta.ini, the instance manifest, per-game Bethesda features (BSA invalidation +
per-profile INIs/saves off a declarative GameDef), and tools through the VFS
(eidos tool), plus Nexus integration beyond it (eidos-nexus: nxm:// downloads,
update checks, the GUI Downloads tab). Since then: the GUI brought up to MO2
daily-driver parity, native DLL provisioning so Proton graphics mods (Community
Shaders) and tools (BodySlide) just work, and the eidos prereqs tool-prerequisite
system. Next up: mod-list separators / categories and LOOT plugin sorting.
ModOrganizer2/usvfs- the Windows semantics we reproducecontainers/fuse-overlayfs- overlay semantics in FUSE, reference for the engine- Limo, RadTux, MO2 Linux installer - the existing compromises
GPL-3.0. See LICENSE.