Skip to main content

Overview

Oboromi implements a minimal virtual filesystem (VFS) layer to abstract file I/O operations. The current implementation focuses on memory-mapped file access for performance-critical ROM loading and asset streaming.

Architecture

The filesystem layer is designed to support multiple storage backends while providing a unified API:
┌──────────────────┐
│   Game Code      │
└────────┬─────────┘


┌──────────────────┐
│   VFS Layer      │  ◄── Future: Multiple backends
└────────┬─────────┘


┌──────────────────┐
│  Memory-Mapped   │  ◄── Current implementation
│      Files       │
└──────────────────┘

File Interface

The core filesystem type is a memory-mapped file wrapper (defined in core/src/fs/mod.rs):
use memmap2::Mmap;
use std::fs;
use std::path::Path;
use std::ops::Deref;

pub struct File {
    map: Mmap,
}

Opening Files

impl File {
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, std::io::Error>
    where
    {
        let file = fs::File::open(path)?;      
        let map = unsafe { Mmap::map(&file)? };
        Ok(Self { map })
    }
}
Key characteristics:
  • Takes any type implementing AsRef<Path> (strings, Path, PathBuf)
  • Uses memmap2 crate for efficient memory mapping
  • Maps entire file into process address space
  • Returns std::io::Error on failure
The Mmap::map() call is marked unsafe because memory-mapped files can cause undefined behavior if the underlying file is modified or deleted while mapped.

Deref Implementation

impl Deref for File {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        &self.map
    }
}
The Deref trait allows File to be used like a byte slice:
let file = File::open("game.rom")?;

// Direct indexing
let first_byte = file[0];

// Slicing
let header = &file[0..16];

// Length
let size = file.len();

// Iteration
for byte in file.iter() {
    // process byte
}

Usage Examples

Loading a ROM File

use oboromi_core::fs::File;

fn load_rom(path: &str) -> Result<(), std::io::Error> {
    let rom = File::open(path)?;
    
    // Check magic number
    if &rom[0..4] != b"NSP\0" {
        eprintln!("Invalid ROM format");
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "Not a valid NSP file"
        ));
    }
    
    // Read ROM size from header
    let size = u32::from_le_bytes([
        rom[4], rom[5], rom[6], rom[7]
    ]);
    
    println!("ROM size: {} bytes", size);
    Ok(())
}

Parsing File Structures

use std::mem;

#[repr(C, packed)]
struct NcaHeader {
    magic: [u8; 4],
    fixed_key_sig: [u8; 256],
    npdm_sig: [u8; 256],
    // ... more fields ...
}

fn parse_nca_header(file: &File) -> Result<&NcaHeader, &'static str> {
    if file.len() < mem::size_of::<NcaHeader>() {
        return Err("File too small for NCA header");
    }
    
    // SAFETY: We verified the size above
    unsafe {
        let ptr = file.as_ptr() as *const NcaHeader;
        Ok(&*ptr)
    }
}
When casting memory-mapped data to structured types, ensure proper alignment and size validation to avoid undefined behavior.

Memory Mapping Benefits

Zero-Copy

Data is accessed directly from the kernel page cache without copying to userspace buffers.

Lazy Loading

Pages are loaded on-demand as accessed, reducing startup time for large files.

Shared Pages

Multiple processes mapping the same file share physical memory pages.

OS Caching

The OS manages page eviction and prefetching automatically.

Design Rationale

Why Memory Mapping?

  1. Performance: Critical for streaming large game assets (textures, models, audio)
  2. Simplicity: Reduces abstraction overhead - files are just byte slices
  3. Integration: Works seamlessly with zero-copy parsing and deserialization

Current Limitations

The current implementation is read-only. Write operations would require:
  • MmapMut for mutable mappings
  • Synchronization primitives for concurrent access
  • Flush semantics for durability
Missing features for a complete VFS:
  • Directory enumeration
  • File metadata (size, timestamps)
  • Mount point abstraction
  • Archive support (ZIP, NACP)
The API is synchronous. Async support would enable:
  • Non-blocking file opens
  • Concurrent loading of multiple files
  • Better integration with async game loops

Future Expansion

Planned VFS Features

// Future API sketch (not implemented)

pub trait FileSystem {
    fn open(&self, path: &str) -> Result<Box<dyn VFile>>;
    fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>>;
    fn metadata(&self, path: &str) -> Result<Metadata>;
}

pub trait VFile: Read + Seek {
    fn size(&self) -> u64;
    fn mmap(&self) -> Result<Mmap>;
}

// Backend implementations
pub struct HostFs;    // Real filesystem
pub struct RomFs;     // Nintendo RomFS archives
pub struct UnionFs;   // Layered filesystems (mods)

Archive Support

Nintendo Switch games use several archive formats:
  • NSP (Nintendo Submission Package) - PFS0 container
  • XCI (NX Card Image) - HFS0 cartridge image
  • NCA (Nintendo Content Archive) - Encrypted content
  • RomFS - Read-only filesystem
Supporting these requires:
pub struct NspArchive {
    base: File,
    entries: Vec<PfsEntry>,
}

impl FileSystem for NspArchive {
    fn open(&self, path: &str) -> Result<Box<dyn VFile>> {
        // Lookup path in archive entries
        // Return virtual file backed by base file slice
    }
}

Thread Safety

The current File type is not Sync because:
  • Mmap is !Sync by default
  • Multiple threads reading the same mapping is safe
  • But we don’t wrap it in Arc<Mmap> currently
For multi-threaded access:
use std::sync::Arc;

let file = Arc::new(File::open("data.bin")?);
let file_clone = Arc::clone(&file);

std::thread::spawn(move || {
    // Use file_clone
});

Error Handling

File operations can fail in several ways:
match File::open("missing.bin") {
    Ok(file) => { /* use file */ },
    Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
        eprintln!("File does not exist");
    },
    Err(e) => return Err(e),
}

Performance Considerations

Memory Usage

Memory-mapped files don’t consume RAM immediately:
let big_file = File::open("10GB.bin")?;  // Fast - doesn't load file
let first_mb = &big_file[0..1024*1024];  // Only this 1MB is paged in

Page Faults

First access to each 4KB page triggers a minor page fault (~1μs overhead):
// Sequential access - good prefetching
for chunk in file.chunks(4096) {
    process(chunk);  // OS prefetches next pages
}

// Random access - poor locality
for i in (0..file.len()).step_by(1_000_000) {
    let byte = file[i];  // Each access likely a page fault
}

Unmapping

Files are automatically unmapped when File is dropped:
{
    let file = File::open("temp.bin")?;
    // use file
}  // <- file unmapped here, pages freed
For long-running processes, consider manual memory management:
let file = File::open("data.bin")?;
let data = file.to_vec();  // Copy to owned Vec
drop(file);                // Unmap immediately

Integration with Other Systems

ROM Loader

use oboromi_core::fs::File;

pub struct RomLoader {
    nca_files: Vec<File>,
}

impl RomLoader {
    pub fn load(rom_path: &str) -> Result<Self, std::io::Error> {
        let nsp = File::open(rom_path)?;
        // Parse NSP header, extract NCA files
        let nca_files = extract_ncas(&nsp)?;
        Ok(Self { nca_files })
    }
}

Asset Streaming

pub struct AssetManager {
    romfs: File,
    index: HashMap<String, (u64, u64)>,  // path -> (offset, size)
}

impl AssetManager {
    pub fn load_asset(&self, path: &str) -> Option<&[u8]> {
        let (offset, size) = self.index.get(path)?;
        let start = *offset as usize;
        let end = start + *size as usize;
        Some(&self.romfs[start..end])
    }
}

Testing

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    
    #[test]
    fn test_file_open() {
        // Create temporary file
        let mut temp = std::fs::File::create("/tmp/test.bin").unwrap();
        temp.write_all(b"Hello, World!").unwrap();
        drop(temp);
        
        // Open with VFS
        let file = File::open("/tmp/test.bin").unwrap();
        assert_eq!(file.len(), 13);
        assert_eq!(&file[0..5], b"Hello");
        
        // Cleanup
        std::fs::remove_file("/tmp/test.bin").unwrap();
    }
}

Source Files

  • Implementation: core/src/fs/mod.rs:1-26
  • Module: core/src/lib.rs:2

Build docs developers (and LLMs) love