From c7fbaa71a3998eea03677e1b54d6536f659449ef Mon Sep 17 00:00:00 2001 From: Craig Carnell <1188869+cscd98@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:18:44 +0000 Subject: [PATCH 1/2] Move from Externals/libretro to libretro-common --- Externals/libretro-common/include/boolean.h | 39 +++++ .../include}/libretro.h | 17 +++ .../include}/libretro_d3d.h | 0 .../include}/libretro_d3d11.h | 0 .../include}/libretro_d3d12.h | 0 .../include}/libretro_vulkan.h | 0 .../include/retro_common_api.h | 139 ++++++++++++++++++ .../include/retro_environment.h | 111 ++++++++++++++ Externals/libretro-common/include/vfs/vfs.h | 118 +++++++++++++++ .../include/vfs/vfs_implementation.h | 84 +++++++++++ .../include/vfs/vfs_implementation_cdrom.h | 52 +++++++ .../include/vfs/vfs_implementation_saf.h | 103 +++++++++++++ Source/Core/AudioCommon/CMakeLists.txt | 2 +- Source/Core/Common/CMakeLists.txt | 2 +- Source/Core/DolphinLibretro/CMakeLists.txt | 6 +- 15 files changed, 668 insertions(+), 5 deletions(-) create mode 100644 Externals/libretro-common/include/boolean.h rename Externals/{Libretro/Include => libretro-common/include}/libretro.h (99%) rename Externals/{Libretro/Include => libretro-common/include}/libretro_d3d.h (100%) rename Externals/{Libretro/Include => libretro-common/include}/libretro_d3d11.h (100%) rename Externals/{Libretro/Include => libretro-common/include}/libretro_d3d12.h (100%) rename Externals/{Libretro/Include => libretro-common/include}/libretro_vulkan.h (100%) create mode 100644 Externals/libretro-common/include/retro_common_api.h create mode 100644 Externals/libretro-common/include/retro_environment.h create mode 100644 Externals/libretro-common/include/vfs/vfs.h create mode 100644 Externals/libretro-common/include/vfs/vfs_implementation.h create mode 100644 Externals/libretro-common/include/vfs/vfs_implementation_cdrom.h create mode 100644 Externals/libretro-common/include/vfs/vfs_implementation_saf.h diff --git a/Externals/libretro-common/include/boolean.h b/Externals/libretro-common/include/boolean.h new file mode 100644 index 000000000000..9d0d7c129ea8 --- /dev/null +++ b/Externals/libretro-common/include/boolean.h @@ -0,0 +1,39 @@ +/* Copyright (C) 2010-2020 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (boolean.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef __LIBRETRO_SDK_BOOLEAN_H +#define __LIBRETRO_SDK_BOOLEAN_H + +#ifndef __cplusplus + +#if defined(_MSC_VER) && _MSC_VER < 1800 && !defined(SN_TARGET_PS3) +/* Hack applied for MSVC when compiling in C89 mode as it isn't C99 compliant. */ +#define bool unsigned char +#define true 1 +#define false 0 +#else +#include +#endif + +#endif + +#endif diff --git a/Externals/Libretro/Include/libretro.h b/Externals/libretro-common/include/libretro.h similarity index 99% rename from Externals/Libretro/Include/libretro.h rename to Externals/libretro-common/include/libretro.h index 32b732e6d2c4..441ec9bad8d0 100644 --- a/Externals/Libretro/Include/libretro.h +++ b/Externals/libretro-common/include/libretro.h @@ -2920,6 +2920,19 @@ typedef int (RETRO_CALLCONV *retro_vfs_rename_t)(const char *old_path, const cha */ typedef int (RETRO_CALLCONV *retro_vfs_stat_t)(const char *path, int32_t *size); +/** + * Gets information about the given file (64-bit size). + * + * @param path The path to the file to query. + * @param[out] size The reported size of the file in bytes. + * May be \c NULL, in which case this value is ignored. + * @return A bitmask of \c RETRO_VFS_STAT flags, + * or 0 if \c path doesn't refer to a valid file. + * @see RETRO_VFS_STAT + * @since VFS API v4 + */ +typedef int (RETRO_CALLCONV *retro_vfs_stat_64_t)(const char *path, int64_t *size); + /** * Creates a directory at the given path. * @@ -3075,6 +3088,10 @@ struct retro_vfs_interface /** @copydoc retro_vfs_closedir_t */ retro_vfs_closedir_t closedir; + + /* VFS API v4 */ + /** @copydoc retro_vfs_stat_64_t */ + retro_vfs_stat_64_t stat_64; }; /** diff --git a/Externals/Libretro/Include/libretro_d3d.h b/Externals/libretro-common/include/libretro_d3d.h similarity index 100% rename from Externals/Libretro/Include/libretro_d3d.h rename to Externals/libretro-common/include/libretro_d3d.h diff --git a/Externals/Libretro/Include/libretro_d3d11.h b/Externals/libretro-common/include/libretro_d3d11.h similarity index 100% rename from Externals/Libretro/Include/libretro_d3d11.h rename to Externals/libretro-common/include/libretro_d3d11.h diff --git a/Externals/Libretro/Include/libretro_d3d12.h b/Externals/libretro-common/include/libretro_d3d12.h similarity index 100% rename from Externals/Libretro/Include/libretro_d3d12.h rename to Externals/libretro-common/include/libretro_d3d12.h diff --git a/Externals/Libretro/Include/libretro_vulkan.h b/Externals/libretro-common/include/libretro_vulkan.h similarity index 100% rename from Externals/Libretro/Include/libretro_vulkan.h rename to Externals/libretro-common/include/libretro_vulkan.h diff --git a/Externals/libretro-common/include/retro_common_api.h b/Externals/libretro-common/include/retro_common_api.h new file mode 100644 index 000000000000..9f43113c456d --- /dev/null +++ b/Externals/libretro-common/include/retro_common_api.h @@ -0,0 +1,139 @@ +/* Copyright (C) 2010-2020 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (retro_common_api.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef _LIBRETRO_COMMON_RETRO_COMMON_API_H +#define _LIBRETRO_COMMON_RETRO_COMMON_API_H + +/* +This file is designed to normalize the libretro-common compiling environment +for public API headers. This should be leaner than a normal compiling environment, +since it gets #included into other project's sources. +*/ + +/* ------------------------------------ */ + +/* +Ordinarily we want to put #ifdef __cplusplus extern "C" in C library +headers to enable them to get used by c++ sources. +However, we want to support building this library as C++ as well, so a +special technique is called for. +*/ + +#define RETRO_BEGIN_DECLS +#define RETRO_END_DECLS + +#ifdef __cplusplus + +#ifdef CXX_BUILD +/* build wants everything to be built as c++, so no extern "C" */ +#else +#undef RETRO_BEGIN_DECLS +#undef RETRO_END_DECLS +#define RETRO_BEGIN_DECLS extern "C" { +#define RETRO_END_DECLS } +#endif + +#else + +/* header is included by a C source file, so no extern "C" */ + +#endif + +/* +IMO, this non-standard ssize_t should not be used. +However, it's a good example of how to handle something like this. +*/ +#ifdef _MSC_VER +#ifndef HAVE_SSIZE_T +#define HAVE_SSIZE_T +#if defined(_WIN64) +typedef __int64 ssize_t; +#elif defined(_WIN32) +typedef int ssize_t; +#endif +#endif +#elif defined(__MACH__) +#include +#endif + +#ifdef _MSC_VER +#if _MSC_VER >= 1800 +#include +#else +#ifndef PRId64 +#define PRId64 "I64d" +#define PRIu64 "I64u" +#define PRIuPTR "Iu" +#endif +#endif +#else +/* C++11 says this one isn't needed, but apparently (some versions of) mingw require it anyways */ +/* https://stackoverflow.com/questions/8132399/how-to-printf-uint64-t-fails-with-spurious-trailing-in-format */ +/* https://github.com/libretro/RetroArch/issues/6009 */ +#ifndef __STDC_FORMAT_MACROS +#define __STDC_FORMAT_MACROS 1 +#endif +#include +#endif +#ifndef PRId64 +#error "inttypes.h is being screwy" +#endif +#define STRING_REP_INT64 "%" PRId64 +#define STRING_REP_UINT64 "%" PRIu64 +#define STRING_REP_USIZE "%" PRIuPTR + +/* Wrap a declaration in RETRO_DEPRECATED() to produce a compiler warning when +it's used. This is intended for developer machines, so it won't work on ancient +or obscure compilers */ +#if defined(_MSC_VER) +#if _MSC_VER >= 1400 /* Visual C 2005 or later */ +#define RETRO_DEPRECATED(decl) __declspec(deprecated) decl +#endif +#elif defined(__GNUC__) +#if __GNUC__ >= 3 /* GCC 3 or later */ +#define RETRO_DEPRECATED(decl) decl __attribute__((deprecated)) +#endif +#elif defined(__clang__) +#if __clang_major__ >= 3 /* clang 3 or later */ +#define RETRO_DEPRECATED(decl) decl __attribute__((deprecated)) +#endif +#endif +#ifndef RETRO_DEPRECATED /* Unsupported compilers */ +#define RETRO_DEPRECATED(decl) decl +#endif + +/* +I would like to see retro_inline.h moved in here; possibly boolean too. + +rationale: these are used in public APIs, and it is easier to find problems +and write code that works the first time portably when they are included uniformly +than to do the analysis from scratch each time you think you need it, for each feature. + +Moreover it helps force you to make hard decisions: if you EVER bring in boolean.h, +then you should pay the price everywhere, so you can see how much grief it will cause. + +Of course, another school of thought is that you should do as little damage as possible +in as few places as possible... +*/ + +/* _LIBRETRO_COMMON_RETRO_COMMON_API_H */ +#endif diff --git a/Externals/libretro-common/include/retro_environment.h b/Externals/libretro-common/include/retro_environment.h new file mode 100644 index 000000000000..2130f851d1bf --- /dev/null +++ b/Externals/libretro-common/include/retro_environment.h @@ -0,0 +1,111 @@ +/* Copyright (C) 2010-2020 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (retro_environment.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef __LIBRETRO_SDK_ENVIRONMENT_H +#define __LIBRETRO_SDK_ENVIRONMENT_H + +/* +This file is designed to create a normalized environment for compiling +libretro-common's private implementations, or any other sources which might +enjoy use of it's environment (RetroArch for instance). +This should be an elaborately crafted environment so that sources don't +need to be full of platform-specific workarounds. +*/ + +#if defined (__cplusplus) +/* The expected values would be + * 199711L, for ISO/IEC 14882:1998 or 14882:2003 + */ + +#elif defined(__STDC__) +/* This is standard C. */ + +#if (__STDC__ == 1) +/* The implementation is ISO-conforming. */ +#define __STDC_ISO__ +#else +/* The implementation is not ISO-conforming. */ +#endif + +#if defined(__STDC_VERSION__) +#if (__STDC_VERSION__ >= 201112L) +/* This is C11. */ +#define __STDC_C11__ +#elif (__STDC_VERSION__ >= 199901L) +/* This is C99. */ +#define __STDC_C99__ +#elif (__STDC_VERSION__ >= 199409L) +/* This is C89 with amendment 1. */ +#define __STDC_C89__ +#define __STDC_C89_AMENDMENT_1__ +#else +/* This is C89 without amendment 1. */ +#define __STDC_C89__ +#endif +#else /* !defined(__STDC_VERSION__) */ +/* This is C89. __STDC_VERSION__ is not defined. */ +#define __STDC_C89__ +#endif + +#else /* !defined(__STDC__) */ +/* This is not standard C. __STDC__ is not defined. */ +#endif + +#if defined(WIN32) || defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__) +/* Try to find out if we're compiling for WinRT or non-WinRT */ +#if defined(_MSC_VER) && defined(__has_include) +#if __has_include() +#define HAVE_WINAPIFAMILY_H 1 +#else +#define HAVE_WINAPIFAMILY_H 0 +#endif + +/* If _USING_V110_SDK71_ is defined it means we are using the Windows XP toolset. */ +#elif defined(_MSC_VER) && (_MSC_VER >= 1700 && !_USING_V110_SDK71_) /* _MSC_VER == 1700 for Visual Studio 2012 */ +#define HAVE_WINAPIFAMILY_H 1 +#else +#define HAVE_WINAPIFAMILY_H 0 +#endif + +#if HAVE_WINAPIFAMILY_H +#include +#define WINAPI_FAMILY_WINRT (!WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) && WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP)) +#else +#define WINAPI_FAMILY_WINRT 0 +#endif /* HAVE_WINAPIFAMILY_H */ + +#if WINAPI_FAMILY_WINRT +#undef __WINRT__ +#define __WINRT__ 1 +#endif + +/* MSVC obviously has to have some non-standard constants... */ +#if _M_IX86_FP == 1 +#define __SSE__ 1 +#elif _M_IX86_FP == 2 || (defined(_M_AMD64) || defined(_M_X64)) +#define __SSE__ 1 +#define __SSE2__ 1 +#endif + +#endif + +#endif diff --git a/Externals/libretro-common/include/vfs/vfs.h b/Externals/libretro-common/include/vfs/vfs.h new file mode 100644 index 000000000000..5dd83e147e98 --- /dev/null +++ b/Externals/libretro-common/include/vfs/vfs.h @@ -0,0 +1,118 @@ +/* Copyright (C) 2010-2020 The RetroArch team +* +* --------------------------------------------------------------------------------------- +* The following license statement only applies to this file (vfs_implementation.h). +* --------------------------------------------------------------------------------------- +* +* Permission is hereby granted, free of charge, +* to any person obtaining a copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation the rights to +* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +* and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef __LIBRETRO_SDK_VFS_H +#define __LIBRETRO_SDK_VFS_H + +#include +#include +#include + +#ifdef RARCH_INTERNAL +#ifndef VFS_FRONTEND +#define VFS_FRONTEND +#endif +#endif + +RETRO_BEGIN_DECLS + +#ifdef _WIN32 +typedef void* HANDLE; +#endif + +#ifdef HAVE_CDROM +typedef struct +{ + int64_t byte_pos; + char *cue_buf; + size_t cue_len; + unsigned cur_lba; + unsigned last_frame_lba; + unsigned char cur_min; + unsigned char cur_sec; + unsigned char cur_frame; + unsigned char cur_track; + unsigned char last_frame[2352]; + char drive; + bool last_frame_valid; +} vfs_cdrom_t; +#endif + +enum vfs_scheme +{ + VFS_SCHEME_NONE = 0, + VFS_SCHEME_CDROM, + VFS_SCHEME_SAF, + VFS_SCHEME_SMB +}; + +#if !(defined(__WINRT__) && defined(__cplusplus_winrt)) +#ifdef VFS_FRONTEND +struct retro_vfs_file_handle +#else +struct libretro_vfs_implementation_file +#endif +{ +#ifdef HAVE_CDROM + vfs_cdrom_t cdrom; /* int64_t alignment */ +#endif + int64_t size; + uint64_t mappos; + uint64_t mapsize; + FILE *fp; +#ifdef _WIN32 + HANDLE fh; +#endif + char *buf; + char* orig_path; + uint8_t *mapped; + int fd; + unsigned hints; + enum vfs_scheme scheme; +#ifdef HAVE_SMBCLIENT + intptr_t smb_fh; + intptr_t smb_ctx; +#endif +}; +#endif + +/* Replace the following symbol with something appropriate + * to signify the file is being compiled for a front end instead of a core. + * This allows the same code to act as reference implementation + * for VFS and as fallbacks for when the front end does not provide VFS functionality. + */ + +#ifdef VFS_FRONTEND +typedef struct retro_vfs_file_handle libretro_vfs_implementation_file; +#else +typedef struct libretro_vfs_implementation_file libretro_vfs_implementation_file; +#endif + +#ifdef VFS_FRONTEND +typedef struct retro_vfs_dir_handle libretro_vfs_implementation_dir; +#else +typedef struct libretro_vfs_implementation_dir libretro_vfs_implementation_dir; +#endif + +RETRO_END_DECLS + +#endif diff --git a/Externals/libretro-common/include/vfs/vfs_implementation.h b/Externals/libretro-common/include/vfs/vfs_implementation.h new file mode 100644 index 000000000000..56d11a36de02 --- /dev/null +++ b/Externals/libretro-common/include/vfs/vfs_implementation.h @@ -0,0 +1,84 @@ +/* Copyright (C) 2010-2020 The RetroArch team +* +* --------------------------------------------------------------------------------------- +* The following license statement only applies to this file (vfs_implementation.h). +* --------------------------------------------------------------------------------------- +* +* Permission is hereby granted, free of charge, +* to any person obtaining a copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation the rights to +* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +* and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef __LIBRETRO_SDK_VFS_IMPLEMENTATION_H +#define __LIBRETRO_SDK_VFS_IMPLEMENTATION_H + +#include +#include +#include +#include +#include + +RETRO_BEGIN_DECLS + +libretro_vfs_implementation_file *retro_vfs_file_open_impl(const char *path, unsigned mode, unsigned hints); + +int retro_vfs_file_close_impl(libretro_vfs_implementation_file *stream); + +int retro_vfs_file_error_impl(libretro_vfs_implementation_file *stream); + +int64_t retro_vfs_file_size_impl(libretro_vfs_implementation_file *stream); + +int64_t retro_vfs_file_truncate_impl(libretro_vfs_implementation_file *stream, int64_t length); + +int64_t retro_vfs_file_tell_impl(libretro_vfs_implementation_file *stream); + +int64_t retro_vfs_file_seek_impl(libretro_vfs_implementation_file *stream, int64_t offset, int seek_position); + +int64_t retro_vfs_file_read_impl(libretro_vfs_implementation_file *stream, void *s, uint64_t len); + +int64_t retro_vfs_file_write_impl(libretro_vfs_implementation_file *stream, const void *s, uint64_t len); + +int retro_vfs_file_flush_impl(libretro_vfs_implementation_file *stream); + +int retro_vfs_file_remove_impl(const char *path); + +int retro_vfs_file_rename_impl(const char *old_path, const char *new_path); + +const char *retro_vfs_file_get_path_impl(libretro_vfs_implementation_file *stream); + +int retro_vfs_stat_impl(const char *path, int32_t *size); + +int retro_vfs_stat_64_impl(const char *path, int64_t *size); + +int retro_vfs_mkdir_impl(const char *dir); + +libretro_vfs_implementation_dir *retro_vfs_opendir_impl(const char *dir, bool include_hidden); + +bool retro_vfs_readdir_impl(libretro_vfs_implementation_dir *dirstream); + +const char *retro_vfs_dirent_get_name_impl(libretro_vfs_implementation_dir *dirstream); + +bool retro_vfs_dirent_is_dir_impl(libretro_vfs_implementation_dir *dirstream); + +int retro_vfs_closedir_impl(libretro_vfs_implementation_dir *dirstream); + +#ifdef __WINRT__ + +void uwp_set_acl(const wchar_t* path, const wchar_t* AccessString); + +#endif + +RETRO_END_DECLS + +#endif diff --git a/Externals/libretro-common/include/vfs/vfs_implementation_cdrom.h b/Externals/libretro-common/include/vfs/vfs_implementation_cdrom.h new file mode 100644 index 000000000000..f809963f4f5b --- /dev/null +++ b/Externals/libretro-common/include/vfs/vfs_implementation_cdrom.h @@ -0,0 +1,52 @@ +/* Copyright (C) 2010-2020 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (vfs_implementation_cdrom.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef __LIBRETRO_SDK_VFS_IMPLEMENTATION_CDROM_H +#define __LIBRETRO_SDK_VFS_IMPLEMENTATION_CDROM_H + +#include +#include + +RETRO_BEGIN_DECLS + +int64_t retro_vfs_file_seek_cdrom(libretro_vfs_implementation_file *stream, int64_t offset, int whence); + +void retro_vfs_file_open_cdrom( + libretro_vfs_implementation_file *stream, + const char *path, unsigned mode, unsigned hints); + +int retro_vfs_file_close_cdrom(libretro_vfs_implementation_file *stream); + +int64_t retro_vfs_file_tell_cdrom(libretro_vfs_implementation_file *stream); + +int64_t retro_vfs_file_read_cdrom(libretro_vfs_implementation_file *stream, + void *s, uint64_t len); + +int retro_vfs_file_error_cdrom(libretro_vfs_implementation_file *stream); + +const cdrom_toc_t* retro_vfs_file_get_cdrom_toc(void); + +const vfs_cdrom_t* retro_vfs_file_get_cdrom_position(const libretro_vfs_implementation_file *stream); + +RETRO_END_DECLS + +#endif diff --git a/Externals/libretro-common/include/vfs/vfs_implementation_saf.h b/Externals/libretro-common/include/vfs/vfs_implementation_saf.h new file mode 100644 index 000000000000..b348d87c7f91 --- /dev/null +++ b/Externals/libretro-common/include/vfs/vfs_implementation_saf.h @@ -0,0 +1,103 @@ +/* Copyright (C) 2010-2020 The RetroArch team +* +* --------------------------------------------------------------------------------------- +* The following license statement only applies to this file (vfs_implementation_saf.h). +* --------------------------------------------------------------------------------------- +* +* Permission is hereby granted, free of charge, +* to any person obtaining a copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation the rights to +* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +* and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef __LIBRETRO_SDK_VFS_IMPLEMENTATION_SAF_H +#define __LIBRETRO_SDK_VFS_IMPLEMENTATION_SAF_H + +#include +#include +#include +#include +#include +#include +#include + +RETRO_BEGIN_DECLS + +typedef struct libretro_vfs_implementation_saf_dir +{ + jobject directory_object; + jstring dirent_name_object; + const char *dirent_name; + bool dirent_is_dir; +} libretro_vfs_implementation_saf_dir; + +struct libretro_vfs_implementation_saf_path_split_result +{ + char *tree; + char *path; +}; + +/* + * Initialize this VFS backend. This must be called prior to using any of the VFS operations provided by this backend. + * get_jni_env should be a function that returns the JNIEnv for the current thread. + * activity_object should be the current Android android.app.Activity. + */ +bool retro_vfs_init_saf(JNIEnv *(*get_jni_env)(void), jobject activity_object); + +/* + * Deinitialize this VFS backend. + */ +bool retro_vfs_deinit_saf(void); + +/* + * Split a serialized "saf://" path string into tree and path components for use with this backend. + * Returns true if successful or false if not. + * The results will be returned in `out` and must be freed by the caller. + */ +bool retro_vfs_path_split_saf(struct libretro_vfs_implementation_saf_path_split_result *out, const char *serialized_path); + +/* + * Join the tree and path components into a single serialized "saf://" path string. + * Returns the serialized path if successful or NULL if not. + * The returned path must be freed by the caller. + */ +char *retro_vfs_path_join_saf(const char *tree, const char *path); + +/* + * Open a file, returning its file descriptor if successful or -1 if not. + * The file descriptor can be operated on using the POSIX file system API (`read()`, `write()`, `lseek()`, `close()`, etc). + * You can also turn the file descriptor into a `FILE *` by calling `fdopen()` on it. + */ +int retro_vfs_file_open_saf(const char *tree, const char *path, unsigned mode); + +int retro_vfs_file_remove_saf(const char *tree, const char *path); + +int retro_vfs_file_rename_saf(const char *old_tree, const char *old_path, const char *new_tree, const char *new_path); + +int retro_vfs_stat_saf(const char *tree, const char *path, int32_t *size); + +int retro_vfs_mkdir_saf(const char *tree, const char *dir); + +libretro_vfs_implementation_saf_dir *retro_vfs_opendir_saf(const char *tree, const char *dir, bool include_hidden); + +bool retro_vfs_readdir_saf(libretro_vfs_implementation_saf_dir *dirstream); + +const char *retro_vfs_dirent_get_name_saf(libretro_vfs_implementation_saf_dir *dirstream); + +bool retro_vfs_dirent_is_dir_saf(libretro_vfs_implementation_saf_dir *dirstream); + +int retro_vfs_closedir_saf(libretro_vfs_implementation_saf_dir *dirstream); + +RETRO_END_DECLS + +#endif diff --git a/Source/Core/AudioCommon/CMakeLists.txt b/Source/Core/AudioCommon/CMakeLists.txt index 2e4904b5c26b..f3d16f8096cf 100644 --- a/Source/Core/AudioCommon/CMakeLists.txt +++ b/Source/Core/AudioCommon/CMakeLists.txt @@ -104,5 +104,5 @@ if(MSVC) endif() if(LIBRETRO) - target_include_directories(audiocommon PRIVATE ${CMAKE_SOURCE_DIR}/Externals/Libretro/Include) + target_include_directories(audiocommon PRIVATE ${CMAKE_SOURCE_DIR}/Externals/libretro-common/include) endif() diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 77bfe106a04c..033a2007df38 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -417,6 +417,6 @@ if(MSVC) endif() if(LIBRETRO) - target_include_directories(common PRIVATE ${CMAKE_SOURCE_DIR}/Externals/Libretro/Include) + target_include_directories(common PRIVATE ${CMAKE_SOURCE_DIR}/Externals/libretro-common/include) target_link_libraries(common PRIVATE dolphin_libretro_videocontexts) endif() diff --git a/Source/Core/DolphinLibretro/CMakeLists.txt b/Source/Core/DolphinLibretro/CMakeLists.txt index 2fb35dddbebc..3668c8721f3d 100644 --- a/Source/Core/DolphinLibretro/CMakeLists.txt +++ b/Source/Core/DolphinLibretro/CMakeLists.txt @@ -15,14 +15,14 @@ if(ENABLE_VULKAN) ) endif() -include_directories(${CMAKE_SOURCE_DIR}/Externals/Libretro/Include) +include_directories(${CMAKE_SOURCE_DIR}/Externals/libretro-common/include) if(ENABLE_VULKAN) include_directories(${CMAKE_SOURCE_DIR}/Externals/Vulkan-Headers/include) include_directories(${CMAKE_SOURCE_DIR}/Externals/VulkanMemoryAllocator/include) endif() -target_include_directories(dolphin_libretro PRIVATE ${CMAKE_SOURCE_DIR}/Externals/Libretro/Include) +target_include_directories(dolphin_libretro PRIVATE ${CMAKE_SOURCE_DIR}/Externals/libretro-common/include) set(LIBRARY_OUTPUT_PATH "${CMAKE_BINARY_DIR}") @@ -61,7 +61,7 @@ else() ) endif() -set(LIBRETRO_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/Externals/Libretro/Include) +set(LIBRETRO_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/Externals/libretro-common/include) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/Common) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/VideoContexts) From e967edccdf9becd14dc8dd646e45187c221a6b4e Mon Sep 17 00:00:00 2001 From: Craig Carnell <1188869+cscd98@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:19:03 +0000 Subject: [PATCH 2/2] libretro: add VFS support --- Source/Core/Common/CommonFuncs.h | 4 +- Source/Core/Common/DirectIOFile.cpp | 110 +++ Source/Core/Common/DirectIOFile.h | 14 + Source/Core/Common/FileUtil.cpp | 105 ++ Source/Core/Common/IOFile.cpp | 91 ++ Source/Core/Common/IOFile.h | 87 +- Source/Core/Core/IOS/FS/HostBackend/File.cpp | 13 + Source/Core/DolphinLibretro/Boot.cpp | 49 +- .../DolphinLibretro/Common/CMakeLists.txt | 6 + Source/Core/DolphinLibretro/Common/File.cpp | 914 ++++++++++++++++++ Source/Core/DolphinLibretro/Common/File.h | 79 ++ .../Core/DolphinLibretro/Common/Options.cpp | 15 + Source/Core/DolphinLibretro/Common/Options.h | 1 + 13 files changed, 1443 insertions(+), 45 deletions(-) create mode 100644 Source/Core/DolphinLibretro/Common/File.cpp create mode 100644 Source/Core/DolphinLibretro/Common/File.h diff --git a/Source/Core/Common/CommonFuncs.h b/Source/Core/Common/CommonFuncs.h index 1b13aa84a2b6..4f8b475f28c4 100644 --- a/Source/Core/Common/CommonFuncs.h +++ b/Source/Core/Common/CommonFuncs.h @@ -30,10 +30,8 @@ #define fseeko _fseeki64 #define ftello _ftelli64 #define atoll _atoi64 -#ifndef stat +#ifndef __MINGW32__ #define stat _stat64 -#endif -#ifndef fstat #define fstat _fstat64 #endif #define fileno _fileno diff --git a/Source/Core/Common/DirectIOFile.cpp b/Source/Core/Common/DirectIOFile.cpp index bfe598dc48e5..d73f58058aba 100644 --- a/Source/Core/Common/DirectIOFile.cpp +++ b/Source/Core/Common/DirectIOFile.cpp @@ -32,6 +32,10 @@ #include "Common/Assert.h" +#ifdef __LIBRETRO__ +#include "DolphinLibretro/Common/File.h" +#endif + namespace File { DirectIOFile::DirectIOFile() = default; @@ -79,6 +83,17 @@ bool DirectIOFile::Open(const std::string& path, AccessMode access_mode, OpenMod if (access_mode == AccessMode::Read && open_mode == OpenMode::Truncate) return false; +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + // used in Duplicate() later + m_path = path; + + return Libretro::File::Open(path, access_mode, open_mode, + m_vfs_handle, m_mode, m_hints, m_current_offset); + } +#endif + #if defined(_WIN32) DWORD desired_access = GENERIC_READ | GENERIC_WRITE; if (access_mode == AccessMode::Read) @@ -168,6 +183,15 @@ bool DirectIOFile::Close() m_current_offset = 0; +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + int result = Libretro::File::Close(m_vfs_handle); + m_vfs_handle = nullptr; + return result == 0; + } +#endif + #if defined(_WIN32) return CloseHandle(std::exchange(m_handle, INVALID_HANDLE_VALUE)) != 0; #else @@ -177,6 +201,11 @@ bool DirectIOFile::Close() bool DirectIOFile::IsOpen() const { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return m_vfs_handle != nullptr; +#endif + #if defined(_WIN32) return m_handle != INVALID_HANDLE_VALUE; #else @@ -216,6 +245,17 @@ static bool OverlappedTransfer(HANDLE handle, u64 offset, auto* data_ptr, u64 si bool DirectIOFile::OffsetRead(u64 offset, u8* out_ptr, u64 size) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (Libretro::File::Seek(m_vfs_handle, offset, SEEK_SET) < 0) + return false; + + int64_t bytes_read = Libretro::File::ReadBytes(m_vfs_handle, out_ptr, size); + return bytes_read == static_cast(size); + } +#endif + #if defined(_WIN32) return OverlappedTransfer(m_handle, offset, out_ptr, size); #else @@ -225,6 +265,17 @@ bool DirectIOFile::OffsetRead(u64 offset, u8* out_ptr, u64 size) bool DirectIOFile::OffsetWrite(u64 offset, const u8* in_ptr, u64 size) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (Libretro::File::Seek(m_vfs_handle, offset, SEEK_SET) < 0) + return false; + + int64_t bytes_written = Libretro::File::WriteBytes(m_vfs_handle, in_ptr, size); + return bytes_written == static_cast(size); + } +#endif + #if defined(_WIN32) return OverlappedTransfer(m_handle, offset, in_ptr, size); #else @@ -234,6 +285,11 @@ bool DirectIOFile::OffsetWrite(u64 offset, const u8* in_ptr, u64 size) u64 DirectIOFile::GetSize() const { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::GetSize(m_vfs_handle); +#endif + #if defined(_WIN32) LARGE_INTEGER result{}; if (GetFileSizeEx(m_handle, &result) != 0) @@ -275,6 +331,11 @@ bool DirectIOFile::Seek(s64 offset, SeekOrigin origin) bool DirectIOFile::Flush() { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::Flush(m_vfs_handle) == 0; +#endif + #if defined(_WIN32) return FlushFileBuffers(m_handle) != 0; #else @@ -284,6 +345,16 @@ bool DirectIOFile::Flush() void DirectIOFile::Swap(DirectIOFile& other) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + std::swap(m_vfs_handle, other.m_vfs_handle); + std::swap(m_path, other.m_path); + std::swap(m_mode, other.m_mode); + std::swap(m_hints, other.m_hints); + } +#endif + #if defined(_WIN32) std::swap(m_handle, other.m_handle); #else @@ -299,6 +370,30 @@ DirectIOFile DirectIOFile::Duplicate() const if (!IsOpen()) return result; +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + // We need to reopen the file, since VFS handles cannot be duplicated. + result.m_vfs_handle = Libretro::File::Open( + m_path, m_mode, m_hints); + + if (result.m_vfs_handle) + { + result.m_path = m_path; + result.m_mode = m_mode; + result.m_hints = m_hints; + + // Restore offset if possible + Libretro::File::Seek( + result.m_vfs_handle, + m_current_offset, + SEEK_SET); + result.m_current_offset = m_current_offset; + } + return result; + } +#endif + #if defined(_WIN32) const auto current_process = GetCurrentProcess(); if (DuplicateHandle(current_process, m_handle, current_process, &result.m_handle, 0, FALSE, @@ -319,6 +414,11 @@ DirectIOFile DirectIOFile::Duplicate() const bool Resize(DirectIOFile& file, u64 size) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::Truncate(file.m_vfs_handle, size) == 0; +#endif + #if defined(_WIN32) // This operation is not "atomic", but it's the only thing we're using the file pointer for. // Concurrent `Resize` would need some external synchronization to prevent race regardless. @@ -333,6 +433,11 @@ bool Resize(DirectIOFile& file, u64 size) bool Rename(DirectIOFile& file, const std::string& source_path [[maybe_unused]], const std::string& destination_path) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return file.IsOpen() && Libretro::File::Rename(source_path, destination_path); +#endif + #if defined(_WIN32) const auto dest_name = UTF8ToWString(destination_path); const auto dest_name_byte_size = DWORD(dest_name.size() * sizeof(WCHAR)); @@ -354,6 +459,11 @@ bool Rename(DirectIOFile& file, const std::string& source_path [[maybe_unused]], bool Delete(DirectIOFile& file, const std::string& filename) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return file.IsOpen() && Libretro::File::Delete(filename); +#endif + #if defined(_WIN32) FILE_DISPOSITION_INFO info{.DeleteFile = TRUE}; return SetFileInformationByHandle(file.GetHandle(), FileDispositionInfo, &info, sizeof(info)) != diff --git a/Source/Core/Common/DirectIOFile.h b/Source/Core/Common/DirectIOFile.h index 79a5e68a2c1b..fefa6767f0ed 100644 --- a/Source/Core/Common/DirectIOFile.h +++ b/Source/Core/Common/DirectIOFile.h @@ -111,6 +111,11 @@ class DirectIOFile final auto GetHandle() const { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + ERROR_LOG_FMT(COMMON, "VFS: Attempt to use std::FILE in DirectIO which should not happen in VFS mode"); +#endif + #if defined(_WIN32) return m_handle; #else @@ -133,6 +138,15 @@ class DirectIOFile final #endif u64 m_current_offset{}; + +#ifdef __LIBRETRO__ + retro_vfs_file_handle* m_vfs_handle = nullptr; + std::string m_path; + unsigned m_mode{}; + unsigned m_hints{}; + + friend bool Resize(DirectIOFile& file, u64 size); +#endif }; // These take an open file handle to avoid failures from other processes trying to open our files. diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 896318439da6..6b831d030d7c 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -65,6 +65,10 @@ #include #endif +#ifdef __LIBRETRO__ +#include "DolphinLibretro/Common/File.h" +#endif + namespace fs = std::filesystem; namespace File @@ -97,6 +101,13 @@ FileInfo::FileInfo(const std::string& path) : FileInfo(path.c_str()) FileInfo::FileInfo(const char* path) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + Libretro::File::FileInfo(m_status, m_size, m_exists, path); + return; + } +#endif #ifdef ANDROID if (IsPathAndroidContent(path)) { @@ -143,18 +154,31 @@ u64 FileInfo::GetSize() const // Returns true if the path exists bool Exists(const std::string& path) { +#ifdef __LIBRETRO__ + if(Libretro::File::HasVFS()) + return Libretro::File::Exists(path); +#endif + return FileInfo(path).Exists(); } // Returns true if the path exists and is a directory bool IsDirectory(const std::string& path) { +#ifdef __LIBRETRO__ + if(Libretro::File::HasVFS()) + return Libretro::File::IsDirectory(path); +#endif return FileInfo(path).IsDirectory(); } // Returns true if the path exists and is a file bool IsFile(const std::string& path) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::IsFile(path); +#endif return FileInfo(path).IsFile(); } @@ -164,6 +188,11 @@ bool Delete(const std::string& filename, IfAbsentBehavior behavior) { DEBUG_LOG_FMT(COMMON, "{}: file {}", __func__, filename); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::Delete(filename); +#endif + #ifdef ANDROID if (filename.starts_with("content://")) { @@ -208,6 +237,11 @@ bool CreateDir(const std::string& path) { DEBUG_LOG_FMT(COMMON, "{}: directory {}", __func__, path); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::CreateDir(path); +#endif + std::error_code error; auto native_path = StringToPath(path); bool success = fs::create_directory(native_path, error); @@ -224,6 +258,11 @@ bool CreateDirs(std::string_view path) { DEBUG_LOG_FMT(COMMON, "{}: directory {}", __func__, path); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::CreateDirs(path); +#endif + std::error_code error; auto native_path = StringToPath(path); bool success = fs::create_directories(native_path, error); @@ -240,6 +279,11 @@ bool CreateFullPath(std::string_view fullPath) { DEBUG_LOG_FMT(COMMON, "{}: path {}", __func__, fullPath); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::CreateFullPath(fullPath); +#endif + std::error_code error; auto native_path = StringToPath(fullPath).parent_path(); bool success = fs::create_directories(native_path, error); @@ -257,6 +301,11 @@ bool DeleteDir(const std::string& filename, IfAbsentBehavior behavior) { DEBUG_LOG_FMT(COMMON, "{}: directory {}", __func__, filename); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::DeleteDir(filename); +#endif + auto native_path = StringToPath(filename); std::error_code error; auto status = fs::status(native_path, error); @@ -290,6 +339,10 @@ bool DeleteDir(const std::string& filename, IfAbsentBehavior behavior) // renames file srcFilename to destFilename, returns true on success bool Rename(const std::string& srcFilename, const std::string& destFilename) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::Rename(srcFilename, destFilename); +#endif DEBUG_LOG_FMT(COMMON, "{}: {} --> {}", __func__, srcFilename, destFilename); std::error_code error; std::filesystem::rename(StringToPath(srcFilename), StringToPath(destFilename), error); @@ -344,6 +397,11 @@ bool CopyRegularFile(std::string_view source_path, std::string_view destination_ { DEBUG_LOG_FMT(COMMON, "{}: {} --> {}", __func__, source_path, destination_path); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::CopyRegularFile(source_path, destination_path); +#endif + auto src_path = StringToPath(source_path); auto dst_path = StringToPath(destination_path); std::error_code error; @@ -359,12 +417,23 @@ bool CopyRegularFile(std::string_view source_path, std::string_view destination_ // Returns the size of a file (or returns 0 if the path isn't a file that exists) u64 GetSize(const std::string& path) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::GetSize(path); +#endif return FileInfo(path).GetSize(); } // Overloaded GetSize, accepts FILE* u64 GetSize(FILE* f) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + ERROR_LOG_FMT(COMMON, "GetSize: call using FILE when VFS is in use. This should not happen"); + return 0; + } +#endif // can't use off_t here because it can be 32-bit const u64 pos = ftello(f); if (fseeko(f, 0, SEEK_END) != 0) @@ -388,6 +457,10 @@ bool CreateEmptyFile(const std::string& filename) { DEBUG_LOG_FMT(COMMON, "CreateEmptyFile: {}", filename); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::CreateEmptyFile(filename); +#endif if (!File::IOFile(filename, "wb")) { ERROR_LOG_FMT(COMMON, "CreateEmptyFile: failed {}: {}", filename, Common::LastStrerrorString()); @@ -440,6 +513,11 @@ FSTEntry ScanDirectoryTree(std::string directory, bool recursive) { DEBUG_LOG_FMT(COMMON, "{}: directory {}", __func__, directory); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::ScanDirectoryTree(directory, recursive); +#endif + #ifdef ANDROID if (IsPathAndroidContent(directory)) return ScanDirectoryTreeAndroidContent(directory, recursive); @@ -531,6 +609,11 @@ bool DeleteDirRecursively(const std::string& directory) { DEBUG_LOG_FMT(COMMON, "{}: {}", __func__, directory); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::DeleteDir(directory, true); +#endif + std::error_code error; const std::uintmax_t num_removed = std::filesystem::remove_all(StringToPath(directory), error); const bool success = num_removed != static_cast(-1) && !error; @@ -544,6 +627,11 @@ bool Copy(std::string_view source_path, std::string_view dest_path, bool overwri DEBUG_LOG_FMT(COMMON, "{}: {} --> {} ({})", __func__, source_path, dest_path, overwrite_existing ? "overwrite" : "preserve"); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::CopyRegularFile(source_path, dest_path, overwrite_existing); +#endif + auto src_path = StringToPath(source_path); auto dst_path = StringToPath(dest_path); std::error_code error; @@ -567,6 +655,11 @@ bool Copy(std::string_view source_path, std::string_view dest_path, bool overwri static bool MoveWithOverwrite(const std::filesystem::path& src, const std::filesystem::path& dst, std::error_code& error) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::MoveWithOverwrite(src.string(), dst.string()); +#endif + fs::rename(src, dst, error); if (!error) return true; @@ -675,6 +768,10 @@ std::string CreateTempDir() std::string GetTempFilenameForAtomicWrite(std::string path) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return path + ".xxx"; +#endif std::error_code error; auto absolute_path = fs::absolute(StringToPath(path), error); if (!error) @@ -1050,11 +1147,19 @@ std::string GetThemeDir(const std::string& theme_name) bool WriteStringToFile(const std::string& filename, std::string_view str) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::WriteStringToFile(filename, str); +#endif return File::IOFile(filename, "wb").WriteBytes(str.data(), str.size()); } bool ReadFileToString(const std::string& filename, std::string& str) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return Libretro::File::ReadFileToString(filename, str); +#endif File::IOFile file(filename, "rb"); if (!file) diff --git a/Source/Core/Common/IOFile.cpp b/Source/Core/Common/IOFile.cpp index b52854fe5bdc..8d59974de31b 100644 --- a/Source/Core/Common/IOFile.cpp +++ b/Source/Core/Common/IOFile.cpp @@ -25,18 +25,34 @@ #include "Common/CommonTypes.h" #include "Common/FileUtil.h" +#ifdef __LIBRETRO__ +#include "DolphinLibretro/Common/File.h" +#endif + namespace File { IOFile::IOFile() : m_file(nullptr), m_good(true) +#ifdef __LIBRETRO__ +, m_vfs_handle{nullptr} +#endif { } IOFile::IOFile(std::FILE* file) : m_file(file), m_good(true) { +#ifdef __LIBRETRO__ + m_vfs_handle = nullptr; + + if (Libretro::File::HasVFS()) + ERROR_LOG_FMT(COMMON, "VFS: Attempt to use IOFile::IOFile with a std::FILE which should not happen"); +#endif } IOFile::IOFile(const std::string& filename, const char openmode[], SharedAccess sh) : m_file(nullptr), m_good(true) +#ifdef __LIBRETRO__ +, m_vfs_handle{nullptr} +#endif { Open(filename, openmode, sh); } @@ -47,6 +63,9 @@ IOFile::~IOFile() } IOFile::IOFile(IOFile&& other) noexcept : m_file(nullptr), m_good(true) +#ifdef __LIBRETRO__ +, m_vfs_handle{nullptr} +#endif { Swap(other); } @@ -59,6 +78,10 @@ IOFile& IOFile::operator=(IOFile&& other) noexcept void IOFile::Swap(IOFile& other) noexcept { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + std::swap(m_vfs_handle, other.m_vfs_handle); +#endif std::swap(m_file, other.m_file); std::swap(m_good, other.m_good); } @@ -68,6 +91,15 @@ bool IOFile::Open(const std::string& filename, const char openmode[], { Close(); +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + m_vfs_handle = Libretro::File::Open(filename, openmode); + m_good = m_vfs_handle != nullptr; + return m_good; + } +#endif + #ifdef _WIN32 if (sh == SharedAccess::Default) { @@ -94,6 +126,17 @@ bool IOFile::Open(const std::string& filename, const char openmode[], bool IOFile::Close() { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (!IsOpen() || 0 != Libretro::File::Close(m_vfs_handle)) + m_good = false; + + m_vfs_handle = nullptr; + return m_good; + } +#endif + if (!IsOpen() || 0 != std::fclose(m_file)) m_good = false; @@ -110,6 +153,16 @@ void IOFile::SetHandle(std::FILE* file) u64 IOFile::GetSize() const { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (IsOpen()) + return Libretro::File::GetSize(m_vfs_handle); + else + return 0; + } +#endif + if (IsOpen()) return File::GetSize(m_file); else @@ -134,6 +187,16 @@ bool IOFile::Seek(s64 offset, SeekOrigin origin) return false; } +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (!IsOpen() || 0 != Libretro::File::Seek(m_vfs_handle, offset, fseek_origin)) + m_good = false; + + return m_good; + } +#endif + if (!IsOpen() || 0 != fseeko(m_file, offset, fseek_origin)) m_good = false; @@ -142,6 +205,15 @@ bool IOFile::Seek(s64 offset, SeekOrigin origin) u64 IOFile::Tell() const { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (IsOpen()) + return Libretro::File::Tell(m_vfs_handle); + else + return UINT64_MAX; + } +#endif if (IsOpen()) return ftello(m_file); else @@ -150,6 +222,15 @@ u64 IOFile::Tell() const bool IOFile::Flush() { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (!IsOpen() || 0 != Libretro::File::Flush(m_vfs_handle)) + m_good = false; + + return m_good; + } +#endif if (!IsOpen() || 0 != std::fflush(m_file)) m_good = false; @@ -158,6 +239,16 @@ bool IOFile::Flush() bool IOFile::Resize(u64 size) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (!IsOpen() || 0 != Libretro::File::Resize(m_vfs_handle, size)) + m_good = false; + + return m_good; + } +#endif + #ifdef _WIN32 if (!IsOpen() || 0 != _chsize_s(_fileno(m_file), size)) #else diff --git a/Source/Core/Common/IOFile.h b/Source/Core/Common/IOFile.h index 187cbee4cfb4..5daac171d352 100644 --- a/Source/Core/Common/IOFile.h +++ b/Source/Core/Common/IOFile.h @@ -11,6 +11,11 @@ #include "Common/CommonTypes.h" +#ifdef __LIBRETRO__ +#include "Common/Logging/Log.h" +#include "DolphinLibretro/Common/File.h" +#endif + namespace File { enum class SeekOrigin @@ -55,6 +60,30 @@ class IOFile requires(std::is_trivially_copyable_v) bool ReadArray(T* elements, size_t count, size_t* num_read = nullptr) { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (!IsOpen()) + { + m_good = false; + return m_good; + } + + int64_t bytes_read = Libretro::File::ReadBytes(m_vfs_handle, + elements, + static_cast(count * sizeof(T))); + + size_t read_count = (bytes_read >= 0) ? static_cast(bytes_read) / sizeof(T) : 0; + + if (read_count != count) + m_good = false; + + if (num_read) + *num_read = read_count; + + return m_good; + } +#endif size_t read_count = 0; if (!IsOpen() || count != (read_count = std::fread(elements, sizeof(T), count, m_file))) m_good = false; @@ -69,6 +98,35 @@ class IOFile requires(std::is_trivially_copyable_v) bool WriteArray(const T* elements, size_t count) { + #ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + if (!IsOpen()) + { + m_good = false; + return m_good; + } + + const uint64_t bytes_to_write = static_cast(count * sizeof(T)); + int64_t bytes_written = Libretro::File::WriteBytes(m_vfs_handle, elements, bytes_to_write); + + if (bytes_written < 0) + { + m_good = false; + return m_good; + } + + size_t write_count = static_cast(bytes_written) / sizeof(T); + if (write_count != count) + { + m_good = false; + return m_good; + } + + return m_good; + } + #endif + if (!IsOpen() || count != std::fwrite(elements, sizeof(T), count, m_file)) m_good = false; @@ -96,11 +154,28 @@ class IOFile bool WriteString(std::string_view str) { return WriteBytes(str.data(), str.size()); } - bool IsOpen() const { return nullptr != m_file; } + bool IsOpen() const + { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return nullptr != m_vfs_handle; +#endif + return nullptr != m_file; + } // m_good is set to false when a read, write or other function fails bool IsGood() const { return m_good; } explicit operator bool() const { return IsGood() && IsOpen(); } - std::FILE* GetHandle() { return m_file; } + std::FILE* GetHandle() + { +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + ERROR_LOG_FMT(COMMON, "VFS: Attempt to use std::FILE which should not happen in VFS mode"); +#endif + return m_file; + } +#ifdef __LIBRETRO__ + retro_vfs_file_handle* GetVFSHandle() { return m_vfs_handle; } +#endif void SetHandle(std::FILE* file); bool Seek(s64 offset, SeekOrigin origin); @@ -113,6 +188,10 @@ class IOFile void ClearError() { m_good = true; +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + return; +#endif if (IsOpen()) std::clearerr(m_file); } @@ -120,6 +199,10 @@ class IOFile private: std::FILE* m_file; bool m_good; + +#ifdef __LIBRETRO__ + retro_vfs_file_handle* m_vfs_handle = nullptr; +#endif }; } // namespace File diff --git a/Source/Core/Core/IOS/FS/HostBackend/File.cpp b/Source/Core/Core/IOS/FS/HostBackend/File.cpp index 17e42e0aec43..4a10970372eb 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/File.cpp +++ b/Source/Core/Core/IOS/FS/HostBackend/File.cpp @@ -134,6 +134,19 @@ Result HostFileSystem::ReadBytesFromFile(Fd fd, u8* ptr, u32 count) // File might be opened twice, need to seek before we read handle->host_file->Seek(handle->file_offset, File::SeekOrigin::Begin); + +#ifdef __LIBRETRO__ + if (Libretro::File::HasVFS()) + { + const u32 actually_read_vfs = Libretro::File::ReadBytes(handle->host_file->GetVFSHandle(), ptr, count); + + if (actually_read_vfs != count) + return ResultCode::AccessDenied; + + handle->file_offset += actually_read_vfs; + return actually_read_vfs; + } +#endif const u32 actually_read = static_cast(fread(ptr, 1, count, handle->host_file->GetHandle())); if (actually_read != count && ferror(handle->host_file->GetHandle())) diff --git a/Source/Core/DolphinLibretro/Boot.cpp b/Source/Core/DolphinLibretro/Boot.cpp index b2a04b049690..9dbff5449654 100644 --- a/Source/Core/DolphinLibretro/Boot.cpp +++ b/Source/Core/DolphinLibretro/Boot.cpp @@ -22,6 +22,7 @@ #include "DolphinLibretro/Audio.h" #include "DolphinLibretro/Input.h" #include "DolphinLibretro/Log.h" +#include "DolphinLibretro/Common/File.h" #include "DolphinLibretro/Common/Options.h" #include "DolphinLibretro/Video.h" #include "DolphinLibretro/VideoContexts/ContextStatus.h" @@ -43,8 +44,6 @@ extern retro_environment_t environ_cb; // Disk swapping static void InitDiskControlInterface(); -static std::string NormalizePath(const std::string& path); -static std::string DenormalizePath(const std::string& path); static unsigned disk_index = 0; static bool eject_state; static std::vector disk_paths; @@ -77,6 +76,10 @@ bool retro_load_game(const struct retro_game_info* game) else sys_dir = "dolphin-emu" DIR_SEP "Sys"; + using namespace Libretro::Options; + Libretro::Options::Init(); + Libretro::File::Init(); + #ifdef ANDROID static bool sysdir_set = false; @@ -108,10 +111,6 @@ bool retro_load_game(const struct retro_game_info* game) INFO_LOG_FMT(COMMON, "User Directory set to '{}'", user_dir); INFO_LOG_FMT(COMMON, "System Directory set to '{}'", sys_dir); - using namespace Libretro::Options; - - Libretro::Options::Init(); - // Main.Core Config::SetBase(Config::MAIN_CPU_CORE, static_cast( @@ -382,7 +381,7 @@ bool retro_load_game(const struct retro_game_info* game) NOTICE_LOG_FMT(VIDEO, "Using GFX backend: {}", Config::Get(Config::MAIN_GFX_BACKEND)); std::vector normalized_game_paths; - normalized_game_paths.push_back(Libretro::NormalizePath(game->path)); + normalized_game_paths.push_back(Libretro::File::NormalizePath(game->path)); std::string folder_path_str; std::string filename_str; std::string extension; @@ -394,7 +393,7 @@ bool retro_load_game(const struct retro_game_info* game) #ifdef _WIN32 // If SplitPath only gave us "D:", rebuild the real directory from the full path - if (folder_path_str.size() == 2 && folder_path_str[1] == ':') + if (!Libretro::File::HasVFS() && folder_path_str.size() == 2 && folder_path_str[1] == ':') { // take everything up to the last backslash size_t last_slash = normalized_game_paths.front().find_last_of("\\/"); @@ -415,7 +414,7 @@ bool retro_load_game(const struct retro_game_info* game) } for (auto& normalized_game_path : normalized_game_paths) - Libretro::disk_paths.push_back(Libretro::DenormalizePath(normalized_game_path)); + Libretro::disk_paths.push_back(Libretro::File::DenormalizePath(normalized_game_path)); Libretro::Input::Init(wsi); @@ -469,36 +468,6 @@ void retro_unload_game(void) namespace Libretro { // Disk swapping - -// Dolphin expects to be able to use "/" (DIR_SEP) everywhere. -// RetroArch uses the OS separator. -// Convert between them when switching between systems. -std::string NormalizePath(const std::string& path) -{ - std::string newPath = path; -#ifdef _MSC_VER - constexpr fs::path::value_type os_separator = fs::path::preferred_separator; - static_assert(os_separator == DIR_SEP_CHR || os_separator == '\\', "Unsupported path separator"); - if (os_separator != DIR_SEP_CHR) - std::replace(newPath.begin(), newPath.end(), '\\', DIR_SEP_CHR); -#endif - - return newPath; -} - -std::string DenormalizePath(const std::string& path) -{ - std::string newPath = path; -#ifdef _MSC_VER - constexpr fs::path::value_type os_separator = fs::path::preferred_separator; - static_assert(os_separator == DIR_SEP_CHR || os_separator == '\\', "Unsupported path separator"); - if (os_separator != DIR_SEP_CHR) - std::replace(newPath.begin(), newPath.end(), DIR_SEP_CHR, '\\'); -#endif - - return newPath; -} - static bool retro_set_eject_state(bool ejected) { if (eject_state == ejected) @@ -512,7 +481,7 @@ static bool retro_set_eject_state(bool ejected) { Core::RunOnCPUThread(Core::System::GetInstance(), [] { Core::CPUThreadGuard guard{Core::System::GetInstance()}; - const std::string path = NormalizePath(disk_paths[disk_index]); + const std::string path = Libretro::File::NormalizePath(disk_paths[disk_index]); Core::System::GetInstance().GetDVDInterface().ChangeDisc(guard, path); }, true); // wait_for_completion = true } diff --git a/Source/Core/DolphinLibretro/Common/CMakeLists.txt b/Source/Core/DolphinLibretro/Common/CMakeLists.txt index c07360a96c79..982cd7aa335c 100644 --- a/Source/Core/DolphinLibretro/Common/CMakeLists.txt +++ b/Source/Core/DolphinLibretro/Common/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(dolphin_libretro_common STATIC + File.cpp Globals.cpp Options.cpp ) @@ -8,5 +9,10 @@ target_include_directories(dolphin_libretro_common PUBLIC ${LIBRETRO_INCLUDE_DIR} ) +# needed for VFS when overriding Source/Core/Common/IOFile.h +target_include_directories(common PUBLIC + ${LIBRETRO_INCLUDE_DIR} +) + # needed for windows cmake compile target_link_libraries(dolphin_libretro_common fmt) diff --git a/Source/Core/DolphinLibretro/Common/File.cpp b/Source/Core/DolphinLibretro/Common/File.cpp new file mode 100644 index 000000000000..4b8ee358c088 --- /dev/null +++ b/Source/Core/DolphinLibretro/Common/File.cpp @@ -0,0 +1,914 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "File.h" +#include "Globals.h" +#include "Common/DirectIOFile.h" +#include "Common/IOFile.h" +#include "Common/Logging/Log.h" +#include "Common/CommonPaths.h" + +#include +#include "DolphinLibretro/Common/Options.h" +#include "DolphinLibretro/Log.h" + +#ifdef _WIN32 +#undef stat +#endif + +#define LIBRETRO_VFS_INTERFACE_VERSION 4 + +namespace Libretro +{ +namespace File +{ +retro_vfs_interface* vfs_interface = nullptr; +static bool initialized = false; +static bool is_enabled = true; + +void Init() +{ + is_enabled = Libretro::GetOption(Options::sysconf::ENABLE_LIBRETRO_VFS, /*def=*/true); + + if (!is_enabled) + return; + + if (initialized) + return; + + retro_vfs_interface_info vfs_info{}; + vfs_info.required_interface_version = LIBRETRO_VFS_INTERFACE_VERSION; + vfs_info.iface = nullptr; + + if (Libretro::environ_cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &vfs_info)) + { + if (vfs_info.iface != nullptr) + { + vfs_interface = vfs_info.iface; + initialized = true; + } + } + + if (!vfs_interface) + { + WARN_LOG_FMT(BOOT, "LibRetro VFS V4 not available, using standard file I/O"); + } +} + +bool HasVFS() +{ + return is_enabled && initialized && vfs_interface; +} + +// Dolphin expects to be able to use "/" (DIR_SEP) everywhere. +// RetroArch uses the OS separator. +// Convert between them when switching between systems. +std::string NormalizePath(const std::string& path) +{ + if (!vfs_interface) + { + std::string normalized = path; +#ifdef _WIN32 + // Convert backslashes to forward slashes for Dolphin compatibility + std::replace(normalized.begin(), normalized.end(), '\\', '/'); +#endif + return normalized; + } + std::string newPath = path; +#ifdef _MSC_VER + namespace fs = std::filesystem; + constexpr fs::path::value_type os_separator = fs::path::preferred_separator; + static_assert(os_separator == DIR_SEP_CHR || os_separator == '\\', + "Unsupported path separator"); + if (os_separator != DIR_SEP_CHR) + std::replace(newPath.begin(), newPath.end(), '\\', DIR_SEP_CHR); +#endif + return newPath; +} + +std::string DenormalizePath(const std::string& path) +{ + if (!vfs_interface) + { + return path; + } + std::string newPath = path; +#ifdef _MSC_VER + namespace fs = std::filesystem; + constexpr fs::path::value_type os_separator = fs::path::preferred_separator; + static_assert(os_separator == DIR_SEP_CHR || os_separator == '\\', + "Unsupported path separator"); + if (os_separator != DIR_SEP_CHR) + std::replace(newPath.begin(), newPath.end(), DIR_SEP_CHR, '\\'); +#endif + return newPath; +} + +// ============================================================================ +// File / DirectIOFile VFS Implementation +// ============================================================================ +bool Exists(const std::string& path) +{ + int64_t size = -1; + int flags = vfs_interface->stat_64(path.c_str(), &size); + + bool valid = (flags & RETRO_VFS_STAT_IS_VALID) != 0; + + DEBUG_LOG_FMT(COMMON, "Exists {}: {}", path, valid); + + return valid; +} + +void FileInfo(std::filesystem::file_status& status, uintmax_t& size, bool& exists, + const char* path) +{ + DEBUG_LOG_FMT(COMMON, "FileInfo {}", path); + + using namespace std; + namespace fs = std::filesystem; + + // Defaults: not found + exists = false; + size = 0; + status.type(fs::file_type::none); + + if (!vfs_interface) + return; + + int64_t tmp_size = 0; + int flags = vfs_interface->stat_64(path, &tmp_size); + + if (!(flags & RETRO_VFS_STAT_IS_VALID)) + return; + + exists = true; + + if (flags & RETRO_VFS_STAT_IS_DIRECTORY) + status.type(fs::file_type::directory); + else + { + status.type(fs::file_type::regular); + size = static_cast(tmp_size); + } +} + +bool IsDirectory(const std::string& path) +{ + if (!vfs_interface) + return false; + + int64_t size = -1; + int flags = vfs_interface->stat_64(path.c_str(), &size); + + return (flags & RETRO_VFS_STAT_IS_VALID) && (flags & RETRO_VFS_STAT_IS_DIRECTORY); +} + +bool IsFile(const std::string& path) +{ + if (!vfs_interface) + return false; + + int64_t size = -1; + int flags = vfs_interface->stat_64(path.c_str(), &size); + + return (flags & RETRO_VFS_STAT_IS_VALID) && !(flags & RETRO_VFS_STAT_IS_DIRECTORY); +} + +u64 GetSize(const std::string& path) +{ + DEBUG_LOG_FMT(COMMON, "GetSize {}", path); + + if (!vfs_interface) + return 0; + + int64_t size = 0; + + int flags = vfs_interface->stat_64(path.c_str(), &size); + + if (flags & RETRO_VFS_STAT_IS_VALID) + { + DEBUG_LOG_FMT(COMMON, "GetSize returning {}", static_cast(size)); + return static_cast(size); + } + + return 0; +} + +u64 GetSize(::retro_vfs_file_handle* handle) +{ + DEBUG_LOG_FMT(COMMON, "GetSize (VFS) handle"); + + if (!vfs_interface || !handle) + return 0; + + int64_t sz = vfs_interface->size(handle); + return sz >= 0 ? static_cast(sz) : 0; +} + +bool Delete(const std::string& path) +{ + DEBUG_LOG_FMT(COMMON, "Delete {}", path); + + if (!vfs_interface) + return false; + + return vfs_interface->remove(path.c_str()) == 0; +} + +bool CreateDir(const std::string& path) +{ + DEBUG_LOG_FMT(COMMON, "CreateDir {}", path); + + if (!vfs_interface) + return false; + + return vfs_interface->mkdir(path.c_str()) == 0; +} + +bool DeleteDir(const std::string& path, bool recursive) +{ + DEBUG_LOG_FMT(COMMON, "DeleteDir {} recursive={}", path, recursive); + + if (!vfs_interface) + return false; + + if (!recursive) + { + retro_vfs_dir_handle* dir = vfs_interface->opendir(path.c_str(), false); + if (dir) + { + if (vfs_interface->closedir) + vfs_interface->closedir(dir); + if (vfs_interface->remove) + return vfs_interface->remove(path.c_str()) == 0; + } + return false; + } + + // Recursive delete + retro_vfs_dir_handle* dir = vfs_interface->opendir(path.c_str(), false); + if (!dir) + return false; + + bool ok = true; + + while (vfs_interface->readdir(dir)) + { + const char* name = vfs_interface->dirent_get_name(dir); + if (!name || strcmp(name, ".") == 0 || strcmp(name, "..") == 0) + continue; + + std::string child_path = path + "/" + name; + if (vfs_interface->dirent_is_dir(dir)) + { + if (!DeleteDir(child_path, true)) + ok = false; + } + else + { + if (vfs_interface->remove(child_path.c_str()) != 0) + ok = false; + } + } + + vfs_interface->closedir(dir); + + if (vfs_interface->remove(path.c_str()) != 0) + ok = false; + + return ok; +} + +bool Rename(const std::string& src, const std::string& dst) +{ + DEBUG_LOG_FMT(COMMON, "Rename {} {}", src, dst); + + if (!vfs_interface) + return false; + + // IOS_FS: Failed to rename temporary FST file otherwise + if (Exists(dst)) + { + if(!Delete(dst)) + WARN_LOG_FMT(COMMON, "Rename failed src: {} dest: {}", src, dst); + } + + return vfs_interface->rename(src.c_str(), dst.c_str()) == 0; +} + +bool CopyRegularFile(std::string_view src, + std::string_view dst, + bool overwrite_existing) +{ + DEBUG_LOG_FMT(COMMON, "CopyRegularFile {} {} overwrite={}", src, dst, overwrite_existing); + + if (!vfs_interface) + return false; + + std::string src_str(src); + std::string dst_str(dst); + + // Open source for reading + retro_vfs_file_handle* in = vfs_interface->open(src_str.c_str(), + RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (!in) + return false; + + // If not overwriting, check existence first + if (!overwrite_existing && Exists(dst_str)) + { + vfs_interface->close(in); + return false; + } + + // Open destination for writing (will truncate if exists) + retro_vfs_file_handle* out = vfs_interface->open(dst_str.c_str(), + RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + + if (!out) + { + vfs_interface->close(in); + return false; + } + + // Copy in chunks + static constexpr size_t CHUNK = 64 * 1024; + std::vector buf(CHUNK); + bool ok = true; + + for (;;) + { + int64_t r = vfs_interface->read(in, buf.data(), buf.size()); + if (r < 0) { ok = false; break; } + if (r == 0) break; // EOF + int64_t w = vfs_interface->write(out, buf.data(), static_cast(r)); + if (w != r) { ok = false; break; } + } + + vfs_interface->close(out); + vfs_interface->close(in); + return ok; +} + +bool CreateEmptyFile(const std::string& path) +{ + DEBUG_LOG_FMT(COMMON, "CreateEmptyFile {}", path); + + if (!vfs_interface) + return false; + + retro_vfs_file_handle* f = vfs_interface->open(path.c_str(), + RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + + if (!f) return false; + + return vfs_interface->close(f) == 0; +} + +bool WriteStringToFile(const std::string& filename, std::string_view str) +{ + DEBUG_LOG_FMT(COMMON, "WriteStringToFile {}: {}", filename, str); + + if (!vfs_interface) + return false; + + retro_vfs_file_handle* f = vfs_interface->open(filename.c_str(), + RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + + if (!f) + return false; + + bool ok = (vfs_interface->write(f, str.data(), str.size()) == (int64_t)str.size()); + vfs_interface->close(f); + return ok; +} + +bool ReadFileToString(const std::string& filename, std::string& str) +{ + DEBUG_LOG_FMT(COMMON, "ReadFileToString {}: {}", filename, str); + + if (!vfs_interface) + return false; + + retro_vfs_file_handle* f = vfs_interface->open(filename.c_str(), + RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); + + if (!f) + return false; + + int64_t size = vfs_interface->size(f); + if (size <= 0) + { + vfs_interface->close(f); + return false; + } + + str.resize(size); + int64_t read = vfs_interface->read(f, str.data(), size); + vfs_interface->close(f); + + return read == size; +} + +bool CreateDirs(std::string_view path) +{ + DEBUG_LOG_FMT(COMMON, "CreateDirs {}", path); + + if (!vfs_interface) + return false; + + if (path.empty()) + return false; + + std::string accum; + size_t i = 0; + + if (path[0] == '/') + accum = "/"; + + while (i < path.size()) + { + size_t slash = path.find('/', i); + std::string token = (slash == std::string::npos) ? + std::string(path.substr(i)) : + std::string(path.substr(i, slash - i)); + + if (!token.empty() && token != ".") + { + if (!accum.empty() && accum.back() != '/') + accum.push_back('/'); + accum += token; + + int rc = vfs_interface->mkdir(accum.c_str()); + if (rc != 0 && rc != -2) // -2 means already exists + return false; + } + + if (slash == std::string::npos) + break; + + i = slash + 1; + } + + return true; +} + +bool CreateFullPath(std::string_view fullPath) +{ + DEBUG_LOG_FMT(COMMON, "CreateFullPath {}", fullPath); + + if (fullPath.empty()) + return false; + + size_t last = fullPath.rfind('/'); + if (last == std::string::npos) + return CreateDirs("."); + + return CreateDirs(fullPath.substr(0, last)); +} + +::File::FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive) +{ + DEBUG_LOG_FMT(COMMON, "ScanDirectoryTree {} {}", directory, recursive); + + ::File::FSTEntry parent_entry; + parent_entry.physicalName = directory; + parent_entry.isDirectory = IsDirectory(directory); + parent_entry.size = 0; + + if (!vfs_interface) + return parent_entry; + + retro_vfs_dir_handle* dir = vfs_interface->opendir(directory.c_str(), false); + if (!dir) + return parent_entry; + + auto calc_dir_size = [](::File::FSTEntry& entry) { + entry.size += entry.children.size(); + for (auto& child : entry.children) + if (child.isDirectory) + entry.size += child.size; + }; + + if (recursive) + { + std::stack dir_stack; + dir_stack.push(directory); + + while (!dir_stack.empty()) + { + std::string current_path = dir_stack.top(); + dir_stack.pop(); + + DEBUG_LOG_FMT(COMMON, "ScanDirectoryTree (recursive) {}", current_path); + + if (current_path.empty()) + continue; + + retro_vfs_dir_handle* d = vfs_interface->opendir(current_path.c_str(), false); + if (!d) + continue; + + // Find the entry object corresponding to current_path + std::function<::File::FSTEntry*(::File::FSTEntry&)> findEntry = + [&](::File::FSTEntry& e) -> ::File::FSTEntry* { + if (e.physicalName == current_path) + return &e; + for (auto& child : e.children) + { + if (auto* found = findEntry(child)) + return found; + } + return nullptr; + }; + + ::File::FSTEntry* parent = findEntry(parent_entry); + if (!parent) + { + vfs_interface->closedir(d); + continue; + } + + while (vfs_interface->readdir(d)) + { + const char* name = vfs_interface->dirent_get_name(d); + if (!name || strcmp(name, ".") == 0 || strcmp(name, "..") == 0) + continue; + + ::File::FSTEntry entry; + entry.virtualName = name; + entry.physicalName = current_path + "/" + name; + entry.isDirectory = vfs_interface->dirent_is_dir(d); + entry.size = 0; + + parent->children.push_back(entry); + + if (entry.isDirectory) + dir_stack.push(entry.physicalName); + } + + vfs_interface->closedir(d); + calc_dir_size(*parent); + } + } + else + { + while (vfs_interface->readdir(dir)) + { + const char* name = vfs_interface->dirent_get_name(dir); + if (!name || strcmp(name, ".") == 0 || strcmp(name, "..") == 0) + continue; + + ::File::FSTEntry entry; + entry.virtualName = name; + entry.physicalName = directory + "/" + name; + entry.isDirectory = vfs_interface->dirent_is_dir(dir); + entry.size = 0; + + parent_entry.children.push_back(entry); + } + calc_dir_size(parent_entry); + } + + vfs_interface->closedir(dir); + + return parent_entry; +} + +// Move a file or directory to a new location, overwriting if it exists. +bool MoveWithOverwrite(const std::string& src, const std::string& dst) +{ + if (!vfs_interface) + return false; + + // Try direct rename first + if (vfs_interface->rename(src.c_str(), dst.c_str()) == 0) + return true; + + // rename failed, try fallbacks + + // Check if src is a directory + int64_t size = -1; + int flags = vfs_interface->stat_64(src.c_str(), &size); + bool is_dir = (flags & RETRO_VFS_STAT_IS_VALID) && (flags & RETRO_VFS_STAT_IS_DIRECTORY); + + if (!is_dir) + { + // src is a file: copy + delete + if (!CopyRegularFile(src, dst, true)) // overwrite_existing = true + return false; + if (vfs_interface->remove(src.c_str()) != 0) + return false; + return true; + } + + // src is a directory: recurse into it + retro_vfs_dir_handle* dir = vfs_interface->opendir(src.c_str(), false); + if (!dir) + return false; + + bool ok = true; + + while (vfs_interface->readdir(dir)) + { + const char* name = vfs_interface->dirent_get_name(dir); + if (!name || strcmp(name, ".") == 0 || strcmp(name, "..") == 0) + continue; + + std::string child_src = src + "/" + name; + std::string child_dst = dst + "/" + name; + + if (!MoveWithOverwrite(child_src, child_dst)) + { + ok = false; + break; + } + } + + vfs_interface->closedir(dir); + + // Remove the source directory itself + if (ok) + { + if (vfs_interface->remove(src.c_str()) != 0) + ok = false; + } + + return ok; +} + +// ============================================================================ +// IOFile VFS Implementation +// ============================================================================ +::retro_vfs_file_handle* Open(const std::string& filename, const char openmode[]) +{ + DEBUG_LOG_FMT(COMMON, "IOFile::Open {} {}", filename, openmode); + + if (!vfs_interface) + return nullptr; + + unsigned mode = 0; + unsigned hints = RETRO_VFS_FILE_ACCESS_HINT_NONE; + + bool read = false; + bool write = false; + bool append = false; + bool plus = false; + + for (const char* p = openmode; *p; ++p) + { + switch (*p) + { + case 'r': read = true; break; + case 'w': write = true; break; + case 'a': append = true; break; + case '+': plus = true; break; + case 'b': break; + } + } + if (read && !write && !plus) + { + // "r" or "rb" - read only + mode = RETRO_VFS_FILE_ACCESS_READ; + } + else if (write && !read && !plus && !append) + { + // "w" or "wb" - write only, truncate + mode = RETRO_VFS_FILE_ACCESS_WRITE; + } + else if (append && !plus) + { + // "a" or "ab" - append only + mode = RETRO_VFS_FILE_ACCESS_WRITE; + } + else if (read && plus) + { + // "r+" or "r+b" - read and write, file must exist + mode = RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING; + } + else if (write && plus) + { + // "w+" or "w+b" - read and write, truncate + mode = RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE; + } + else if (append && plus) + { + // "a+" or "a+b" - read and append + mode = RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE; + } + else + { + // Default to read + mode = RETRO_VFS_FILE_ACCESS_READ; + } + + ::retro_vfs_file_handle* handle = vfs_interface->open(filename.c_str(), mode, hints); + + if (handle) + { + // Handle write mode - truncate to 0 + if (write && !plus && !append) + { + vfs_interface->truncate(handle, 0); + } + + // Handle append mode - seek to end + if (append) + { + vfs_interface->seek(handle, 0, RETRO_VFS_SEEK_POSITION_END); + } + } + + return handle; +} + +// helper: return int status, 0 = success, nonzero = error +int Close(::retro_vfs_file_handle* handle) +{ + DEBUG_LOG_FMT(COMMON, "IOFileClose"); + + if (!vfs_interface || !handle) + return -1; + + return vfs_interface->close(handle); +} + +s64 Seek(::retro_vfs_file_handle* handle, s64 offset, ::File::SeekOrigin origin) +{ + int whence = RETRO_VFS_SEEK_POSITION_START; + + switch (origin) + { + case ::File::SeekOrigin::Current: + whence = static_cast(::File::SeekOrigin::Current); + break; + case ::File::SeekOrigin::End: + whence = static_cast(::File::SeekOrigin::End); + break; + case ::File::SeekOrigin::Begin: + default: + whence = static_cast(::File::SeekOrigin::Begin); + break; + } + + return Libretro::File::Seek(handle, offset, whence); +} + +s64 Seek(::retro_vfs_file_handle* handle, s64 offset, int origin) +{ + DEBUG_LOG_FMT(COMMON, "IOFileSeek {} {}", offset, origin); + + if (!vfs_interface || !handle) + return -1; + + int whence = RETRO_VFS_SEEK_POSITION_START; + + if (origin == SEEK_CUR) + whence = RETRO_VFS_SEEK_POSITION_CURRENT; + else if (origin == SEEK_END) + whence = RETRO_VFS_SEEK_POSITION_END; + + return vfs_interface->seek(handle, offset, whence); +} + +s64 Tell(::retro_vfs_file_handle* handle) +{ + DEBUG_LOG_FMT(COMMON, "IOFileTell"); + + if (!vfs_interface || !handle) + return UINT64_MAX; + + int64_t pos = vfs_interface->tell(handle); + return pos >= 0 ? static_cast(pos) : UINT64_MAX; +} + +int Flush(::retro_vfs_file_handle* handle) +{ + DEBUG_LOG_FMT(COMMON, "IOFileFlush"); + + if (!vfs_interface || !handle) + return -1; + + return vfs_interface->flush(handle); +} + +s64 Resize(::retro_vfs_file_handle* handle, u64 size) +{ + DEBUG_LOG_FMT(COMMON, "IOFileResize {}", size); + + if (!vfs_interface || !handle) + return -1; + + return vfs_interface->truncate(handle, size); +} + +u64 ReadBytes(::retro_vfs_file_handle* handle, void* data, size_t length) +{ + DEBUG_LOG_FMT(COMMON, "IOFileReadBytes {}", length); + + if (!vfs_interface || !handle) + return 0; + + int64_t result = vfs_interface->read(handle, data, length); + return result >= 0 ? static_cast(result) : 0; +} + +u64 WriteBytes(::retro_vfs_file_handle* handle, const void* data, size_t length) +{ + DEBUG_LOG_FMT(COMMON, "IOFileWriteBytes {}", length); + + if (!vfs_interface || !handle) + return 0; + + int64_t result = vfs_interface->write(handle, data, length); + return result >= 0 ? static_cast(result) : 0; +} + +// Helper for DirectIOFile::Open +bool Open(const std::string& path, + ::File::AccessMode access_mode, + ::File::OpenMode open_mode, + retro_vfs_file_handle*& out_handle, + unsigned& out_mode, + unsigned& out_hints, + u64& out_offset) +{ + unsigned mode = 0; + + if (access_mode == ::File::AccessMode::Read) + mode = RETRO_VFS_FILE_ACCESS_READ; + else if (access_mode == ::File::AccessMode::Write) + mode = RETRO_VFS_FILE_ACCESS_WRITE; + else // ReadAndWrite + mode = RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING; + + unsigned hints = RETRO_VFS_FILE_ACCESS_HINT_NONE; + + if (open_mode == ::File::OpenMode::Truncate) + { + // VFS doesn't have explicit truncate - we'll handle it after opening + mode |= RETRO_VFS_FILE_ACCESS_WRITE; + } + else if (open_mode == ::File::OpenMode::Create) + { + if (Exists(path)) + return false; + } + + retro_vfs_file_handle* handle = vfs_interface->open(path.c_str(), mode, hints); + if (!handle) + return false; + + if (open_mode == ::File::OpenMode::Truncate) + { + if (vfs_interface->truncate(handle, 0) != 0) + { + vfs_interface->close(handle); + return false; + } + } + + out_handle = handle; + out_mode = mode; + out_hints = hints; + out_offset = 0; + + return true; +} + +::retro_vfs_file_handle* Open(const std::string& filename, unsigned mode, unsigned hints) +{ + DEBUG_LOG_FMT(COMMON, "IOFile::Open {} {} {}", filename, mode, hints); + + if (!vfs_interface) + return nullptr; + + ::retro_vfs_file_handle* handle = vfs_interface->open(filename.c_str(), mode, hints); + + return handle; +} + +s64 Truncate(retro_vfs_file_handle* vfs_handle, const u64 size) +{ + if (!vfs_interface || !vfs_handle) + return 0; + + return vfs_interface->truncate(vfs_handle, size); +} + +std::string GetPath(::retro_vfs_file_handle* handle) +{ + if (!vfs_interface || !handle) + return std::string(); + + return Libretro::File::vfs_interface->get_path(handle); +} + +} // namespace File +} // namespace Libretro diff --git a/Source/Core/DolphinLibretro/Common/File.h b/Source/Core/DolphinLibretro/Common/File.h new file mode 100644 index 000000000000..ee712d146e77 --- /dev/null +++ b/Source/Core/DolphinLibretro/Common/File.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include +#include +#include "Common/CommonTypes.h" +#include "Common/FileUtil.h" +#include +#include +#include + +namespace File +{ +enum class SeekOrigin; +enum class AccessMode; +enum class OpenMode; +} + +namespace Libretro +{ +namespace File +{ +extern retro_vfs_interface* vfs_interface; + +// Generic methods +void Init(); +bool HasVFS(); +std::string NormalizePath(const std::string& path); +std::string DenormalizePath(const std::string& path); + +// FileUtil +bool Exists(const std::string& path); +void FileInfo(std::filesystem::file_status& status, uintmax_t& size, bool& exists, + const char* path); +bool IsDirectory(const std::string& path); +bool IsFile(const std::string& path); +u64 GetSize(const std::string& path); +u64 GetSize(::retro_vfs_file_handle* handle); + +bool Delete(const std::string& path); +bool CreateDir(const std::string& path); +bool DeleteDir(const std::string& path, bool recursive = false); +bool Rename(const std::string& src, const std::string& dst); +bool CopyRegularFile(std::string_view src, std::string_view dst, bool overwrite_existing = true); +bool CreateEmptyFile(const std::string& path); +bool WriteStringToFile(const std::string& filename, std::string_view str); +bool ReadFileToString(const std::string& filename, std::string& str); +bool CreateDirs(std::string_view path); +bool CreateFullPath(std::string_view fullPath); +::File::FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive); +bool MoveWithOverwrite(const std::string& src, const std::string& dst); + +// IOFile +::retro_vfs_file_handle* Open(const std::string& filename, const char openmode[]); +int Close(::retro_vfs_file_handle* handle); +s64 Seek(::retro_vfs_file_handle* handle, s64 offset, ::File::SeekOrigin origin); +s64 Seek(::retro_vfs_file_handle* handle, s64 offset, int origin); +s64 Tell(::retro_vfs_file_handle* handle); +s64 Resize(::retro_vfs_file_handle* handle, u64 size); +u64 ReadBytes(::retro_vfs_file_handle* handle, void* data, size_t length); +u64 WriteBytes(::retro_vfs_file_handle* handle, const void* data, size_t length); +int Flush(::retro_vfs_file_handle* handle); + +// DirectIOFile +bool Open(const std::string& path, ::File::AccessMode access_mode, ::File::OpenMode open_mode, + retro_vfs_file_handle*& out_handle, + unsigned& out_mode, + unsigned& out_hints, + u64& out_offset); +::retro_vfs_file_handle* Open(const std::string& filename, unsigned mode, unsigned hints); + +s64 Truncate(retro_vfs_file_handle* vfs_handle, const u64 size); + +// IOS::HLE::FS +std::string GetPath(::retro_vfs_file_handle* handle); + +} // namespace File +} // namespace Libretro diff --git a/Source/Core/DolphinLibretro/Common/Options.cpp b/Source/Core/DolphinLibretro/Common/Options.cpp index fc1f0f1224b7..e3c9b74d4a55 100644 --- a/Source/Core/DolphinLibretro/Common/Options.cpp +++ b/Source/Core/DolphinLibretro/Common/Options.cpp @@ -530,6 +530,21 @@ static struct retro_core_option_v2_definition option_defs[] = { "disabled" }, + { + Libretro::Options::sysconf::ENABLE_LIBRETRO_VFS, + "System Configuration > Enable Virtual File System (VFS)", + "Enable Virtual File System (VFS)", + "Enable VFS for SAF/SMB etc.", + nullptr, + CATEGORY_SYSCONF, + { + { "disabled", nullptr }, + { "enabled", nullptr }, + { nullptr, nullptr } + }, + "disabled" + }, + // ========== Graphics.Hardware ========== /*{ Libretro::Options::gfx_hardware::VSYNC, diff --git a/Source/Core/DolphinLibretro/Common/Options.h b/Source/Core/DolphinLibretro/Common/Options.h index 52373dc5ab5d..355d50fcc774 100644 --- a/Source/Core/DolphinLibretro/Common/Options.h +++ b/Source/Core/DolphinLibretro/Common/Options.h @@ -168,6 +168,7 @@ namespace sysconf { constexpr const char ENABLE_RUMBLE[] = "dolphin_enable_rumble"; constexpr const char WIIMOTE_CONTINUOUS_SCANNING[] = "dolphin_wiimote_continuous_scanning"; constexpr const char ALT_GC_PORTS_ON_WII[] = "dolphin_alt_gc_ports_on_wii"; + constexpr const char ENABLE_LIBRETRO_VFS[] = "dolphin_libretro_vfs_enabled"; } // namespace sysconf // ======================================================