Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Storage, Disks, and VFS

Three storage-related libretro surfaces live in this chapter because they have different ownership models:

  • Disk control is core-owned media state. The core decides what “the current disk” means and answers frontend queries.
  • Subsystems describe load-time multi-ROM contracts. Registered once during environment setup.
  • VFS is frontend-owned file access. The frontend exposes a filesystem interface and the core uses RAII handles to read and write.

Use normal Rust std::fs only when the core intentionally bypasses frontend VFS policy (for example, debug-only paths). Frontend-mediated paths should always go through VfsInterface.

Disk Control

Implement these Core trait methods when the core has swappable disk images (CD-ROM emulators, multi-disc games):

impl Core for MyCore {
    fn disk_image_count(&mut self) -> u32 {
        self.disks.len() as u32
    }

    fn disk_image_index(&mut self) -> DiskIndex {
        DiskIndex::new(self.current_disk)
    }

    fn disk_set_image_index(&mut self, index: DiskIndex) -> bool {
        let i = index.get();
        if (i as usize) < self.disks.len() {
            self.current_disk = i;
            true
        } else {
            false
        }
    }

    fn disk_tray_state(&mut self) -> DiskTrayState {
        if self.tray_open { DiskTrayState::Ejected } else { DiskTrayState::Closed }
    }

    fn disk_set_tray_state(&mut self, state: DiskTrayState) -> bool {
        self.tray_open = matches!(state, DiskTrayState::Ejected);
        true
    }

    fn disk_replace_image_index(
        &mut self,
        index: DiskIndex,
        game: Option<GameInfo<'_>>,
    ) -> bool {
        let i = index.get() as usize;
        match game {
            Some(info) => self.disks[i] = Disk::from_game_info(info),
            None => self.disks.remove(i),
        };
        true
    }

    fn disk_add_image_index(&mut self) -> bool {
        self.disks.push(Disk::empty());
        true
    }

    fn disk_image_path(&mut self, index: DiskIndex) -> Option<String> {
        self.disks.get(index.get() as usize)?.path.clone()
    }

    fn disk_image_label(&mut self, index: DiskIndex) -> Option<String> {
        self.disks.get(index.get() as usize)?.label.clone()
    }
}

Register the disk control interface during environment setup. There are two versions:

fn on_set_environment(&mut self, env: &mut Environment<'_>) {
    if let Some(version) = env.disk_control_interface_version() {
        if version.supports_extended() {
            let _ = env.set_disk_control_ext_interface();
        } else {
            let _ = env.set_disk_control_interface();
        }
    } else {
        let _ = env.set_disk_control_interface();
    }
}

The extended (v1) interface enables disk_image_path, disk_image_label, disk_set_initial_image, disk_add_image_index, and replace operations. The v0 interface only supports basic tray/index management.

Subsystems

A subsystem describes how a core can load multiple related ROMs as a single “game” — for example, a Super Game Boy core that needs both an SNES ROM and a Game Boy ROM. Register during environment setup:

fn on_set_environment(&mut self, env: &mut Environment<'_>) {
    let sgb = SubsystemInfo::new("Super Game Boy", "sgb", SubsystemId::from(0))
        .with_roms([
            SubsystemRomInfo::new("SNES ROM", "smc|sfc")
                .with_required(true)
                .with_memory([
                    SubsystemMemoryInfo::new("srm", SubsystemMemoryType::from(0x101)),
                ]),
            SubsystemRomInfo::new("Game Boy ROM", "gb|gbc")
                .with_required(true)
                .with_need_fullpath(true)
                .with_memory([
                    SubsystemMemoryInfo::new("sav", SubsystemMemoryType::from(0x101)),
                    SubsystemMemoryInfo::new("rtc", SubsystemMemoryType::from(0x102)),
                ]),
        ]);

    let _ = env.set_subsystem_info(&[sgb]);
}
TypePurpose
SubsystemInfoOne subsystem entry — description, identifier, numeric id, and the ROMs it loads.
SubsystemRomInfoOne ROM slot within a subsystem — extensions, fullpath requirements, memory associations.
SubsystemMemoryInfoA save/RTC/SRAM file associated with a ROM slot.
SubsystemMemoryType::from(u32)Frontend-defined memory category (e.g. 0x101 for cartridge SRAM, 0x102 for RTC).

The wrapper retains all descriptor strings, so the slice you pass to set_subsystem_info can be built from temporaries.

VFS

VfsInterface is acquired during setup with the version the core needs:

fn on_set_environment(&mut self, env: &mut Environment<'_>) {
    self.vfs = env.vfs_interface(VfsInterfaceVersion::new(3));
}

Files

VfsFile is RAII — drop closes the file, or call close() explicitly to inspect the result:

let Some(vfs) = self.vfs else { return; };

let access = VfsFileAccessFlags::from(VfsFileAccess::Read);
let hints = VfsFileAccessHints::empty();
let Some(mut file) = vfs.open_file("/path/to/save.srm", access, hints) else {
    return;
};

let mut buffer = vec![0u8; 4096];
while let Some(read) = file.read(&mut buffer) {
    if read == 0 { break; }
    self.save_data.extend_from_slice(&buffer[..read]);
}

let _ = file.seek(0, VfsSeekPosition::Start);
let _ = file.tell();
let _ = file.flush();
// Drop closes the file; or:
let _ok = file.close();

VfsFileAccess variants: Read, Write, UpdateExisting. Combine with | to get multi-mode flags. VfsSeekPosition is Start, Current, or End.

Writing follows the same shape:

let access = VfsFileAccessFlags::from(VfsFileAccess::Write)
    | VfsFileAccess::UpdateExisting;
let hints = VfsFileAccessHints::empty();
let Some(mut file) = vfs.open_file("/path/save.srm", access, hints) else {
    return;
};
let _ = file.write(&self.save_data);
let _ = file.flush();

Directories

VfsDirectory iterates entries via read_next:

let Some(mut dir) = vfs.open_dir("/saves", /* include_hidden */ false) else {
    return;
};
while dir.read_next() {
    if let Some(name) = dir.entry_name() {
        let is_dir = dir.entry_is_dir();
        self.entries.push((name, is_dir));
    }
}

Other operations

let exists = vfs.stat("/saves/slot1.srm");           // Option<VfsMetadata>
let ok = vfs.create_dir("/saves");
let ok = vfs.rename("/old.srm", "/new.srm");
let ok = vfs.remove_file("/scratch.bin");

VfsMetadata::flags is a VfsStatFlags bitmask (Valid, Directory, CharacterSpecial); size is the file size when known.

VfsFile and VfsDirectory are !Send and !Sync — they hold raw pointers into the frontend callback table and must stay on the thread that called run.

Reference