diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f4214968d..24a709a2be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1463,6 +1463,8 @@ else() # library src/platform/libretro/audio.h src/platform/libretro/clock.cpp src/platform/libretro/clock.h + src/platform/libretro/filesystem_libretro.cpp + src/platform/libretro/filesystem_libretro.h src/platform/libretro/input_buttons.cpp src/platform/libretro/ui.cpp src/platform/libretro/ui.h diff --git a/src/filesystem_root.cpp b/src/filesystem_root.cpp index 4cb334590c..8f569a3506 100644 --- a/src/filesystem_root.cpp +++ b/src/filesystem_root.cpp @@ -23,6 +23,10 @@ # include "platform/android/filesystem_saf.h" #endif +#ifdef USE_LIBRETRO +# include "platform/libretro/filesystem_libretro.h" +#endif + constexpr const std::string_view root_ns = "root://"; RootFilesystem::RootFilesystem() : Filesystem("", FilesystemView()) { @@ -32,6 +36,10 @@ RootFilesystem::RootFilesystem() : Filesystem("", FilesystemView()) { fs_list.push_back(std::make_pair("content", std::make_unique("", FilesystemView()))); #endif +#ifdef USE_LIBRETRO + fs_list.push_back(std::make_pair("libretro", std::make_unique("", FilesystemView()))); +#endif + // IMPORTANT: This must be the last filesystem in the list, do not push anything to fs_list afterwards! fs_list.push_back(std::make_pair("file", std::make_unique("", FilesystemView()))); @@ -106,12 +114,19 @@ const Filesystem& RootFilesystem::FilesystemForPath(std::string_view path) const assert(!fs_list.empty()); std::string_view ns; + +#ifdef USE_LIBRETRO + if (LibretroFilesystem::vfs.required_interface_version >= EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION) { + ns = "libretro"; + } +#else // Check if the path contains a namespace auto ns_pos = path.find("://"); if (ns_pos != std::string::npos) { ns = path.substr(0, ns_pos); path = path.substr(ns_pos + 3); } +#endif if (ns.empty()) { // No namespace returns the last fs which is the NativeFilesystem diff --git a/src/platform/libretro/filesystem_libretro.cpp b/src/platform/libretro/filesystem_libretro.cpp new file mode 100644 index 0000000000..5532fd066b --- /dev/null +++ b/src/platform/libretro/filesystem_libretro.cpp @@ -0,0 +1,239 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player 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 Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player 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 EasyRPG Player. If not, see . + */ + +#include "filesystem_libretro.h" +#include "filesystem_stream.h" +#include "output.h" + +struct retro_vfs_interface_info LibretroFilesystem::vfs; + +LibretroFilesystem::LibretroFilesystem(std::string base_path, FilesystemView parent_fs) : Filesystem(std::move(base_path), parent_fs) { +} + +bool LibretroFilesystem::IsFile(std::string_view path) const { + int flags = vfs.iface->stat(ToString(path).c_str(), nullptr); + return flags & RETRO_VFS_STAT_IS_VALID && !(flags & RETRO_VFS_STAT_IS_DIRECTORY); +} + +bool LibretroFilesystem::IsDirectory(std::string_view dir, bool) const { + int flags = vfs.iface->stat(ToString(dir).c_str(), nullptr); + return flags & RETRO_VFS_STAT_IS_VALID && flags & RETRO_VFS_STAT_IS_DIRECTORY; +} + +bool LibretroFilesystem::Exists(std::string_view filename) const { + int flags = vfs.iface->stat(ToString(filename).c_str(), nullptr); + return flags & RETRO_VFS_STAT_IS_VALID; +} + +int64_t LibretroFilesystem::GetFilesize(std::string_view path) const { + int32_t size; + int flags = vfs.iface->stat(ToString(path).c_str(), &size); + return flags & RETRO_VFS_STAT_IS_VALID ? size : -1; +} + +// To prevent leaking of the file handle if an exception is thrown within CreateInputStreambuffer/CreateOutputStreambuffer +class LibretroFileGuard { +public: + LibretroFileGuard(struct retro_vfs_file_handle* handle) noexcept : handle(handle) { + } + + ~LibretroFileGuard() { + if (handle != nullptr) { + LibretroFilesystem::vfs.iface->close(handle); + } + } + + void forget() noexcept { + handle = nullptr; + } + +private: + struct retro_vfs_file_handle* handle; +}; + +class LibretroStreamBufIn : public std::streambuf { +public: + LibretroStreamBufIn(struct retro_vfs_file_handle* handle) : std::streambuf(), handle(handle) { + setg(buffer_start, buffer_start, buffer_start); + } + + ~LibretroStreamBufIn() override { + LibretroFilesystem::vfs.iface->close(handle); + } + + int underflow() override { + int64_t res = LibretroFilesystem::vfs.iface->read(handle, buffer.data(), buffer.size()); + if (res == 0) { + return traits_type::eof(); + } else if (res < 0) { + Output::Debug("underflow failed: {}", strerror(errno)); + return traits_type::eof(); + } + setg(buffer_start, buffer_start, buffer_start + res); + return traits_type::to_int_type(*gptr()); + } + + std::streambuf::pos_type seekoff(std::streambuf::off_type offset, std::ios_base::seekdir dir, std::ios_base::openmode mode) override { + if (dir == std::ios_base::cur) { + offset += static_cast(gptr() - egptr()); + } + int cdir = Filesystem_Stream::CppSeekdirToCSeekdir(dir); + auto res = LibretroFilesystem::vfs.iface->seek(handle, offset, cdir == SEEK_CUR ? RETRO_VFS_SEEK_POSITION_CURRENT : cdir == SEEK_END ? RETRO_VFS_SEEK_POSITION_END : RETRO_VFS_SEEK_POSITION_START); + setg(buffer_start, buffer_end, buffer_end); + return res; + } + + std::streambuf::pos_type seekpos(std::streambuf::pos_type pos, std::ios_base::openmode mode) override { + return seekoff(pos, std::ios_base::beg, mode); + } + +private: + struct retro_vfs_file_handle* handle; + std::array buffer; + char* buffer_start = &buffer.front(); + char* buffer_end = &buffer.back(); +}; + +std::streambuf* LibretroFilesystem::CreateInputStreambuffer(std::string_view path, std::ios_base::openmode) const { + struct retro_vfs_file_handle* handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (handle == nullptr) { + return nullptr; + } + LibretroFileGuard guard(handle); + LibretroStreamBufIn* stream = new LibretroStreamBufIn(handle); + guard.forget(); + return stream; +} + +class LibretroStreamBufOut : public std::streambuf { +public: + LibretroStreamBufOut(struct retro_vfs_file_handle* handle) : std::streambuf(), handle(handle) { + setp(buffer_start, buffer_end); + } + + ~LibretroStreamBufOut() override { + sync(); + LibretroFilesystem::vfs.iface->close(handle); + } + + int overflow(int c = EOF) override { + if (sync() < 0) { + return traits_type::eof(); + } + if (c != EOF) { + char a = static_cast(c); + int64_t res = LibretroFilesystem::vfs.iface->write(handle, &a, 1); + if (res < 1) { + return traits_type::eof(); + } + } + + return c; + } + + int sync() override { + auto len = pptr() - pbase(); + if (len == 0) { + return 0; + } + int64_t res = LibretroFilesystem::vfs.iface->write(handle, pbase(), len); + setp(buffer_start, buffer_end); + if (res < len) { + return -1; + } + return 0; + } + +private: + struct retro_vfs_file_handle* handle; + std::array buffer; + char* buffer_start = &buffer.front(); + char* buffer_end = &buffer.back(); +}; + +std::streambuf* LibretroFilesystem::CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode mode) const { + struct retro_vfs_file_handle* handle; + if ((mode & std::ios_base::app) == std::ios_base::app && Exists(path)) { + handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (handle == nullptr) { + return nullptr; + } + if (vfs.iface->seek(handle, 0, RETRO_VFS_SEEK_POSITION_END) == -1) { + vfs.iface->close(handle); + return nullptr; + } + } else { + handle = vfs.iface->open(ToString(path).c_str(), RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (handle == nullptr) { + return nullptr; + } + } + LibretroFileGuard guard(handle); + LibretroStreamBufOut* stream = new LibretroStreamBufOut(handle); + guard.forget(); + return stream; +} + +// To prevent leaking of the directory handle if an exception is thrown within GetDirectoryContent +class LibretroDirGuard { +public: + LibretroDirGuard(struct retro_vfs_dir_handle* handle) noexcept : handle(handle) { + } + + ~LibretroDirGuard() { + LibretroFilesystem::vfs.iface->closedir(handle); + } + +private: + struct retro_vfs_dir_handle* handle; +}; + +bool LibretroFilesystem::GetDirectoryContent(std::string_view path, std::vector& entries) const { + std::string p = ToString(path); + + struct retro_vfs_dir_handle* dir = vfs.iface->opendir(p.c_str(), true); + if (dir == nullptr) { + Output::Debug("Error opening dir {}", p); + return false; + } + LibretroDirGuard guard(dir); + + while (vfs.iface->readdir(dir)) { + const char* name = vfs.iface->dirent_get_name(dir); + if (name == nullptr) { + continue; + } + bool is_directory = vfs.iface->dirent_is_dir(dir); + entries.emplace_back( + name, + is_directory ? DirectoryTree::FileType::Directory : DirectoryTree::FileType::Regular); + } + + return true; +} + +bool LibretroFilesystem::MakeDirectory(std::string_view path, bool) const { + return vfs.iface->mkdir(ToString(path).c_str()) != -1; +} + +bool LibretroFilesystem::IsFeatureSupported(Feature f) const { + return f == Filesystem::Feature::Write; +} + +std::string LibretroFilesystem::Describe() const { + return fmt::format("[libretro] {}", GetPath()); +} diff --git a/src/platform/libretro/filesystem_libretro.h b/src/platform/libretro/filesystem_libretro.h new file mode 100644 index 0000000000..60ee7da0c7 --- /dev/null +++ b/src/platform/libretro/filesystem_libretro.h @@ -0,0 +1,56 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player 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 Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player 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 EasyRPG Player. If not, see . + */ + +#ifndef EP_FILESYSTEM_LIBRETRO_H +#define EP_FILESYSTEM_LIBRETRO_H + +#include "filesystem.h" +#include "libretro.h" + +#define EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION 3U + +/** + * A wrapper around the libretro virtual filesystem interface + */ +class LibretroFilesystem : public Filesystem { +public: + /** + * Initializes a libretro filesystem + */ + explicit LibretroFilesystem(std::string base_path, FilesystemView parent_fs); + + static struct retro_vfs_interface_info vfs; + +protected: + /** + * Implementation of abstract methods + */ + /** @{ */ + bool IsFile(std::string_view path) const override; + bool IsDirectory(std::string_view path, bool follow_symlinks) const override; + bool Exists(std::string_view path) const override; + int64_t GetFilesize(std::string_view path) const override; + std::streambuf* CreateInputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override; + std::streambuf* CreateOutputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override; + bool GetDirectoryContent(std::string_view path, std::vector& entries) const override; + bool MakeDirectory(std::string_view path, bool follow_symlinks) const override; + bool IsFeatureSupported(Feature f) const override; + std::string Describe() const override; + /** @} */ +}; + +#endif diff --git a/src/platform/libretro/ui.cpp b/src/platform/libretro/ui.cpp index e05087ed97..d06ca1f311 100644 --- a/src/platform/libretro/ui.cpp +++ b/src/platform/libretro/ui.cpp @@ -18,6 +18,7 @@ // Headers #include "ui.h" #include "clock.h" +#include "filesystem_libretro.h" #include "bitmap.h" #include "color.h" #include "filefinder.h" @@ -316,6 +317,12 @@ RETRO_API void retro_set_environment(retro_environment_t cb) { { nullptr, nullptr } }; cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables); + + struct retro_vfs_interface_info vfs; + vfs.required_interface_version = EP_FILESYSTEM_LIBRETRO_REQUIRED_INTERFACE_VERSION; + if (cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &vfs)) { + LibretroFilesystem::vfs = vfs; + } } RETRO_API void retro_set_video_refresh(retro_video_refresh_t cb) {