diff --git a/Makefile.common b/Makefile.common
index 73152664bbc..3a6b582979e 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -590,6 +590,9 @@ ifeq ($(HAVE_LIBRETRODB), 1)
OBJ += menu/menu_explore.o \
tasks/task_menu_explore.o
endif
+ ifeq ($(HAVE_VFS), 1)
+ OBJ += menu/menu_vfs_browser.o
+ endif
endif
ifeq ($(HAVE_BUILTINBEARSSL), 1)
diff --git a/menu/menu_vfs_browser.c b/menu/menu_vfs_browser.c
new file mode 100644
index 00000000000..587b3b539cc
--- /dev/null
+++ b/menu/menu_vfs_browser.c
@@ -0,0 +1,410 @@
+/* RetroArch - A frontend for libretro.
+ * Copyright (C) 2011-2026 - The RetroArch Team
+ *
+ * RetroArch is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with RetroArch.
+ * If not, see .
+ */
+
+#if defined(HAVE_VFS) && !defined(__3DS__) && !defined(GEKKO) && !defined(PS2) && !defined(WIIU) && !defined(__DJGPP__) && !defined(WEBOS)
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "menu_vfs_browser.h"
+#include "menu_driver.h"
+#include "../retroarch.h"
+#include "../verbosity.h"
+
+/* VFS Browser state */
+typedef struct
+{
+ libretro_vfs_implementation_dir *dir;
+ char current_path[PATH_MAX_LENGTH];
+ char **entry_names;
+ bool *entry_is_dir;
+ uint64_t *entry_sizes;
+ size_t entry_count;
+ size_t entry_capacity;
+ bool initialized;
+ enum vfs_scheme scheme;
+} vfs_browser_state_t;
+
+static vfs_browser_state_t g_vfs_browser = {0};
+
+/**
+ * Internal function to read directory entries using VFS
+ */
+static void vfs_browser_read_dir(void)
+{
+ libretro_vfs_implementation_dir *vfs_dir;
+ size_t i;
+ const char *name;
+ bool is_dir;
+ uint64_t size;
+ char full_path[PATH_MAX_LENGTH];
+ size_t new_capacity;
+ char **new_names;
+ bool *new_is_dir;
+ uint64_t *new_sizes;
+
+ /* Clean up old entries */
+ if (g_vfs_browser.entry_names)
+ {
+ for (i = 0; i < g_vfs_browser.entry_count; i++)
+ {
+ if (g_vfs_browser.entry_names[i])
+ free(g_vfs_browser.entry_names[i]);
+ }
+ free(g_vfs_browser.entry_names);
+ g_vfs_browser.entry_names = NULL;
+ }
+
+ if (g_vfs_browser.entry_is_dir)
+ {
+ free(g_vfs_browser.entry_is_dir);
+ g_vfs_browser.entry_is_dir = NULL;
+ }
+
+ if (g_vfs_browser.entry_sizes)
+ {
+ free(g_vfs_browser.entry_sizes);
+ g_vfs_browser.entry_sizes = NULL;
+ }
+
+ g_vfs_browser.entry_count = 0;
+ g_vfs_browser.entry_capacity = 64;
+
+ /* Allocate arrays */
+ g_vfs_browser.entry_names = (char**)calloc(g_vfs_browser.entry_capacity, sizeof(char*));
+ g_vfs_browser.entry_is_dir = (bool*)calloc(g_vfs_browser.entry_capacity, sizeof(bool));
+ g_vfs_browser.entry_sizes = (uint64_t*)calloc(g_vfs_browser.entry_capacity, sizeof(uint64_t));
+
+ if (!g_vfs_browser.entry_names || !g_vfs_browser.entry_is_dir || !g_vfs_browser.entry_sizes)
+ {
+ RARCH_ERR("[VFS Browser] Failed to allocate entry arrays\n");
+ return;
+ }
+
+ /* Open directory using VFS */
+ vfs_dir = retro_vfs_opendir_impl(g_vfs_browser.current_path, true);
+ if (!vfs_dir)
+ {
+ RARCH_ERR("[VFS Browser] Failed to open directory: %s\n", g_vfs_browser.current_path);
+ return;
+ }
+
+ g_vfs_browser.dir = vfs_dir;
+
+ /* Read entries */
+ while (retro_vfs_readdir_impl(vfs_dir))
+ {
+ name = retro_vfs_dirent_get_name_impl(vfs_dir);
+ is_dir = retro_vfs_dirent_is_dir_impl(vfs_dir);
+ size = 0;
+
+ if (!name)
+ continue;
+
+ /* Skip . and .. */
+ if (string_is_equal(name, ".") || string_is_equal(name, ".."))
+ continue;
+
+ /* Expand capacity if needed */
+ if (g_vfs_browser.entry_count >= g_vfs_browser.entry_capacity)
+ {
+ new_capacity = g_vfs_browser.entry_capacity * 2;
+ new_names = (char**)realloc(g_vfs_browser.entry_names,
+ new_capacity * sizeof(char*));
+ new_is_dir = (bool*)realloc(g_vfs_browser.entry_is_dir,
+ new_capacity * sizeof(bool));
+ new_sizes = (uint64_t*)realloc(g_vfs_browser.entry_sizes,
+ new_capacity * sizeof(uint64_t));
+
+ if (!new_names || !new_is_dir || !new_sizes)
+ {
+ RARCH_ERR("[VFS Browser] Failed to expand entry arrays\n");
+ break;
+ }
+
+ g_vfs_browser.entry_names = new_names;
+ g_vfs_browser.entry_is_dir = new_is_dir;
+ g_vfs_browser.entry_sizes = new_sizes;
+ g_vfs_browser.entry_capacity = new_capacity;
+ }
+
+ /* Store entry info */
+ g_vfs_browser.entry_names[g_vfs_browser.entry_count] = strdup(name);
+ g_vfs_browser.entry_is_dir[g_vfs_browser.entry_count] = is_dir;
+
+ /* Get file size if not a directory */
+ if (!is_dir)
+ {
+ fill_pathname_join(full_path, g_vfs_browser.current_path, name,
+ sizeof(full_path));
+ size = retro_vfs_stat_impl(full_path, NULL);
+ }
+
+ g_vfs_browser.entry_sizes[g_vfs_browser.entry_count] = size;
+ g_vfs_browser.entry_count++;
+ }
+
+ retro_vfs_closedir_impl(vfs_dir);
+ g_vfs_browser.dir = NULL;
+
+ RARCH_LOG("[VFS Browser] Read %zu entries from %s\n",
+ g_vfs_browser.entry_count, g_vfs_browser.current_path);
+}
+
+/**
+ * Initialize the VFS browser
+ */
+bool menu_vfs_browser_init(void)
+{
+ memset(&g_vfs_browser, 0, sizeof(g_vfs_browser));
+ strlcpy(g_vfs_browser.current_path, "/", sizeof(g_vfs_browser.current_path));
+ g_vfs_browser.scheme = VFS_SCHEME_NONE;
+ g_vfs_browser.initialized = true;
+
+ RARCH_LOG("[VFS Browser] Initialized\n");
+ return true;
+}
+
+/**
+ * Deinitialize the VFS browser
+ */
+void menu_vfs_browser_deinit(void)
+{
+ size_t i;
+
+ if (!g_vfs_browser.initialized)
+ return;
+
+ /* Clean up entries */
+ if (g_vfs_browser.entry_names)
+ {
+ for (i = 0; i < g_vfs_browser.entry_count; i++)
+ {
+ if (g_vfs_browser.entry_names[i])
+ free(g_vfs_browser.entry_names[i]);
+ }
+ free(g_vfs_browser.entry_names);
+ }
+
+ if (g_vfs_browser.entry_is_dir)
+ free(g_vfs_browser.entry_is_dir);
+
+ if (g_vfs_browser.entry_sizes)
+ free(g_vfs_browser.entry_sizes);
+
+ memset(&g_vfs_browser, 0, sizeof(g_vfs_browser));
+
+ RARCH_LOG("[VFS Browser] Deinitialized\n");
+}
+
+/**
+ * Open VFS browser at specified path
+ */
+bool menu_vfs_browser_open(const char *path)
+{
+ if (!g_vfs_browser.initialized)
+ {
+ RARCH_ERR("[VFS Browser] Not initialized\n");
+ return false;
+ }
+
+ if (path && !string_is_empty(path))
+ {
+ strlcpy(g_vfs_browser.current_path, path, sizeof(g_vfs_browser.current_path));
+ }
+ else
+ {
+ strlcpy(g_vfs_browser.current_path, "/", sizeof(g_vfs_browser.current_path));
+ }
+
+ vfs_browser_read_dir();
+ return true;
+}
+
+/**
+ * Navigate to parent directory
+ */
+bool menu_vfs_browser_parent(void)
+{
+ char *last_slash;
+
+ if (!g_vfs_browser.initialized)
+ return false;
+
+ /* Don't go above root */
+ if (string_is_equal(g_vfs_browser.current_path, "/"))
+ return false;
+
+ /* Find last slash and truncate */
+ last_slash = strrchr(g_vfs_browser.current_path, '/');
+ if (last_slash && last_slash != g_vfs_browser.current_path)
+ {
+ *last_slash = '\0';
+ if (string_is_empty(g_vfs_browser.current_path))
+ strlcpy(g_vfs_browser.current_path, "/", sizeof(g_vfs_browser.current_path));
+ }
+
+ vfs_browser_read_dir();
+ return true;
+}
+
+/**
+ * Navigate to subdirectory
+ */
+bool menu_vfs_browser_subdir(const char *name)
+{
+ char new_path[PATH_MAX_LENGTH];
+
+ if (!g_vfs_browser.initialized || !name)
+ return false;
+
+ fill_pathname_join(new_path, g_vfs_browser.current_path, name,
+ sizeof(new_path));
+ strlcpy(g_vfs_browser.current_path, new_path, sizeof(g_vfs_browser.current_path));
+
+ vfs_browser_read_dir();
+ return true;
+}
+
+/**
+ * Get current VFS path
+ */
+const char* menu_vfs_browser_get_path(void)
+{
+ return g_vfs_browser.current_path;
+}
+
+/**
+ * Perform file operation
+ */
+bool menu_vfs_browser_operation(unsigned operation, const char *name, const char *new_name)
+{
+ char full_path[PATH_MAX_LENGTH];
+ char new_path[PATH_MAX_LENGTH];
+
+ if (!g_vfs_browser.initialized || !name)
+ return false;
+
+ fill_pathname_join(full_path, g_vfs_browser.current_path, name,
+ sizeof(full_path));
+
+ switch (operation)
+ {
+ case 0: /* Info - just return success, info is already loaded */
+ return true;
+
+ case 1: /* Open - would need to integrate with file viewer */
+ RARCH_LOG("[VFS Browser] Open file: %s\n", full_path);
+ /* TODO: Integrate with file viewer */
+ return true;
+
+ case 2: /* Delete */
+ RARCH_LOG("[VFS Browser] Delete: %s\n", full_path);
+ if (retro_vfs_remove_impl(full_path) == 0)
+ {
+ vfs_browser_read_dir(); /* Refresh */
+ return true;
+ }
+ return false;
+
+ case 3: /* Rename */
+ if (!new_name)
+ return false;
+ fill_pathname_join(new_path, g_vfs_browser.current_path, new_name,
+ sizeof(new_path));
+ RARCH_LOG("[VFS Browser] Rename: %s -> %s\n", full_path, new_path);
+ if (retro_vfs_rename_impl(full_path, new_path) == 0)
+ {
+ vfs_browser_read_dir(); /* Refresh */
+ return true;
+ }
+ return false;
+
+ case 4: /* Create directory */
+ RARCH_LOG("[VFS Browser] Create directory: %s\n", full_path);
+ if (retro_vfs_mkdir_impl(full_path))
+ {
+ vfs_browser_read_dir(); /* Refresh */
+ return true;
+ }
+ return false;
+
+ default:
+ return false;
+ }
+}
+
+/**
+ * Refresh current directory listing
+ */
+void menu_vfs_browser_refresh(void)
+{
+ if (g_vfs_browser.initialized)
+ vfs_browser_read_dir();
+}
+
+/**
+ * Get file count in current directory
+ */
+size_t menu_vfs_browser_get_count(void)
+{
+ return g_vfs_browser.entry_count;
+}
+
+/**
+ * Get entry name at index
+ */
+const char* menu_vfs_browser_get_name(size_t index)
+{
+ if (index >= g_vfs_browser.entry_count)
+ return NULL;
+ return g_vfs_browser.entry_names[index];
+}
+
+/**
+ * Check if entry at index is a directory
+ */
+bool menu_vfs_browser_is_directory(size_t index)
+{
+ if (index >= g_vfs_browser.entry_count)
+ return false;
+ return g_vfs_browser.entry_is_dir[index];
+}
+
+/**
+ * Get entry size
+ */
+uint64_t menu_vfs_browser_get_size(size_t index)
+{
+ if (index >= g_vfs_browser.entry_count)
+ return 0;
+ return g_vfs_browser.entry_sizes[index];
+}
+
+/**
+ * Get VFS scheme for current location
+ */
+enum vfs_scheme menu_vfs_browser_get_scheme(void)
+{
+ return g_vfs_browser.scheme;
+}
+
+#endif /* HAVE_VFS guard */
diff --git a/menu/menu_vfs_browser.h b/menu/menu_vfs_browser.h
new file mode 100644
index 00000000000..1c23d58e20a
--- /dev/null
+++ b/menu/menu_vfs_browser.h
@@ -0,0 +1,117 @@
+/* RetroArch - A frontend for libretro.
+ * Copyright (C) 2011-2026 - The RetroArch Team
+ *
+ * RetroArch is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with RetroArch.
+ * If not, see .
+ */
+
+#if defined(HAVE_VFS) && !defined(__3DS__) && !defined(GEKKO) && !defined(PS2) && !defined(WIIU) && !defined(__DJGPP__) && !defined(WEBOS)
+
+#ifndef __MENU_VFS_BROWSER_H
+#define __MENU_VFS_BROWSER_H
+
+#include
+#include
+#include
+#include
+
+RETRO_BEGIN_DECLS
+
+/**
+ * Initialize the VFS browser
+ * @return true on success, false on failure
+ */
+bool menu_vfs_browser_init(void);
+
+/**
+ * Deinitialize the VFS browser
+ */
+void menu_vfs_browser_deinit(void);
+
+/**
+ * Open VFS browser at specified path
+ * @param path Path to browse (NULL for root)
+ * @return true on success
+ */
+bool menu_vfs_browser_open(const char *path);
+
+/**
+ * Navigate to parent directory
+ * @return true on success
+ */
+bool menu_vfs_browser_parent(void);
+
+/**
+ * Navigate to subdirectory
+ * @param name Directory name
+ * @return true on success
+ */
+bool menu_vfs_browser_subdir(const char *name);
+
+/**
+ * Get current VFS path
+ * @return Current path string
+ */
+const char* menu_vfs_browser_get_path(void);
+
+/**
+ * Perform file operation
+ * @param operation Operation type (0=info, 1=open, 2=delete, 3=rename, 4=mkdir)
+ * @param name File/directory name
+ * @param new_name New name (for rename)
+ * @return true on success
+ */
+bool menu_vfs_browser_operation(unsigned operation, const char *name, const char *new_name);
+
+/**
+ * Refresh current directory listing
+ */
+void menu_vfs_browser_refresh(void);
+
+/**
+ * Get file count in current directory
+ * @return Number of entries
+ */
+size_t menu_vfs_browser_get_count(void);
+
+/**
+ * Get entry name at index
+ * @param index Entry index
+ * @return Entry name or NULL
+ */
+const char* menu_vfs_browser_get_name(size_t index);
+
+/**
+ * Check if entry at index is a directory
+ * @param index Entry index
+ * @return true if directory
+ */
+bool menu_vfs_browser_is_directory(size_t index);
+
+/**
+ * Get entry size
+ * @param index Entry index
+ * @return File size in bytes
+ */
+uint64_t menu_vfs_browser_get_size(size_t index);
+
+/**
+ * Get VFS scheme for current location
+ * @return VFS scheme enum value
+ */
+enum vfs_scheme menu_vfs_browser_get_scheme(void);
+
+RETRO_END_DECLS
+
+#endif /* __MENU_VFS_BROWSER_H */
+
+#endif /* HAVE_VFS guard */
+