diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index bfff73daa..b9a0ced51 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -155,6 +155,8 @@ add_library(common STATIC vector_math.h virtual_buffer.cpp virtual_buffer.h + vfs_cache.cpp + vfs_cache.h wall_clock.cpp wall_clock.h xci_trimmer.cpp diff --git a/src/common/vfs_cache.cpp b/src/common/vfs_cache.cpp new file mode 100644 index 000000000..1993be7d0 --- /dev/null +++ b/src/common/vfs_cache.cpp @@ -0,0 +1,348 @@ +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "common/logging/log.h" +#include "common/vfs_cache.h" + +namespace Common { + +VfsCache& VfsCache::Instance() { + static VfsCache instance; + return instance; +} + +VfsCache::VfsCache() = default; + +std::optional VfsCache::Lookup(const std::string& path) { + std::lock_guard lock(mutex_); + + auto it = cache_.find(path); + if (it == cache_.end()) { + ++misses_; + return std::nullopt; + } + + if (IsExpired(it->second)) { + cache_.erase(it); + ++misses_; + ++evictions_; + return std::nullopt; + } + + ++hits_; + // PROJECT PATHFINDER OPTIMIZATION: Log cache hits for debugging VFS performance + LOG_DEBUG(Common_Filesystem, "VFS cache hit: path={}, size={} bytes", path, it->second.size); + return it->second; +} + +std::optional VfsCache::LookupAndTouch(const std::string& path) { + std::lock_guard lock(mutex_); + + auto it = cache_.find(path); + if (it == cache_.end()) { + ++misses_; + return std::nullopt; + } + + if (IsExpired(it->second)) { + cache_.erase(it); + ++misses_; + ++evictions_; + return std::nullopt; + } + + ++hits_; + // Update access time and count for LRU tracking + it->second.last_access = std::chrono::steady_clock::now(); + it->second.access_count++; + + LOG_DEBUG(Common_Filesystem, "VFS cache hit: path={}, size={} bytes, access_count={}", + path, it->second.size, it->second.access_count); + return it->second; +} + +void VfsCache::Insert(const std::string& path, u64 size, u64 timestamp) { + InsertExtended(path, size, timestamp, false, 0); +} + +void VfsCache::InsertExtended(const std::string& path, u64 size, u64 timestamp, + bool is_game_asset, u64 content_hash) { + std::lock_guard lock(mutex_); + + // PROJECT PATHFINDER OPTIMIZATION: Smart cache eviction for Thor 12GB devices + // Evict oldest non-game-asset entries first when at capacity + if (cache_.size() >= MAX_CACHE_SIZE) { + // Find and evict oldest non-game-asset entry with lowest access count + auto oldest_it = cache_.end(); + u32 lowest_access = std::numeric_limits::max(); + auto oldest_time = std::chrono::steady_clock::time_point::max(); + + for (auto it = cache_.begin(); it != cache_.end(); ++it) { + // Prefer evicting non-game assets + if (!it->second.is_game_asset) { + if (it->second.access_count < lowest_access || + (it->second.access_count == lowest_access && + it->second.last_access < oldest_time)) { + oldest_it = it; + lowest_access = it->second.access_count; + oldest_time = it->second.last_access; + } + } + } + + // If all entries are game assets, evict the oldest game asset + if (oldest_it == cache_.end()) { + for (auto it = cache_.begin(); it != cache_.end(); ++it) { + if (it->second.access_count < lowest_access || + (it->second.access_count == lowest_access && + it->second.last_access < oldest_time)) { + oldest_it = it; + lowest_access = it->second.access_count; + oldest_time = it->second.last_access; + } + } + } + + if (oldest_it != cache_.end()) { + cache_.erase(oldest_it); + ++evictions_; + } + } + + VfsCacheEntry entry{ + .path = path, + .size = size, + .timestamp = timestamp, + .last_access = std::chrono::steady_clock::now(), + .content_hash = content_hash, + .is_game_asset = is_game_asset, + .access_count = 1, + }; + cache_[path] = std::move(entry); + ++insertions_; + + // Mark cache as warm when we insert game assets + if (is_game_asset) { + is_warm_ = true; + } + + LOG_DEBUG(Common_Filesystem, "VFS cache insert: path={}, size={} bytes, is_game_asset={}", + path, size, is_game_asset); +} + +void VfsCache::InsertDirectoryListing(const std::string& path, + const std::vector& entries, u64 timestamp) { + std::lock_guard lock(mutex_); + + // Evict oldest directory listing if at capacity + if (dir_cache_.size() >= MAX_DIR_CACHE_SIZE) { + auto oldest_it = dir_cache_.begin(); + for (auto it = dir_cache_.begin(); it != dir_cache_.end(); ++it) { + if (it->second.last_access < oldest_it->second.last_access) { + oldest_it = it; + } + } + if (oldest_it != dir_cache_.end()) { + dir_cache_.erase(oldest_it); + } + } + + VfsDirCacheEntry entry{ + .path = path, + .entries = entries, + .last_access = std::chrono::steady_clock::now(), + .timestamp = timestamp, + }; + dir_cache_[path] = std::move(entry); + + LOG_DEBUG(Common_Filesystem, "VFS dir cache insert: path={}, entries={}", path, entries.size()); +} + +std::optional> VfsCache::LookupDirectoryListing(const std::string& path) { + std::lock_guard lock(mutex_); + + auto it = dir_cache_.find(path); + if (it == dir_cache_.end()) { + return std::nullopt; + } + + if (IsDirListingExpired(it->second)) { + dir_cache_.erase(it); + return std::nullopt; + } + + it->second.last_access = std::chrono::steady_clock::now(); + LOG_DEBUG(Common_Filesystem, "VFS dir cache hit: path={}, entries={}", path, + it->second.entries.size()); + return it->second.entries; +} + +void VfsCache::Invalidate(const std::string& path) { + std::lock_guard lock(mutex_); + cache_.erase(path); + dir_cache_.erase(path); +} + +void VfsCache::InvalidatePrefix(const std::string& prefix) { + std::lock_guard lock(mutex_); + + for (auto it = cache_.begin(); it != cache_.end();) { + if (it->first.starts_with(prefix)) { + it = cache_.erase(it); + } else { + ++it; + } + } + + for (auto it = dir_cache_.begin(); it != dir_cache_.end();) { + if (it->first.starts_with(prefix)) { + it = dir_cache_.erase(it); + } else { + ++it; + } + } +} + +void VfsCache::Clear() { + std::lock_guard lock(mutex_); + cache_.clear(); + dir_cache_.clear(); + is_warm_ = false; + LOG_INFO(Common_Filesystem, "VFS cache cleared"); +} + +void VfsCache::SetExpirationSeconds(u64 seconds) { + std::lock_guard lock(mutex_); + expiration_seconds_ = seconds; +} + +void VfsCache::SetGameAssetExpirationSeconds(u64 seconds) { + std::lock_guard lock(mutex_); + game_asset_expiration_seconds_ = seconds; +} + +void VfsCache::PruneLRU(size_t max_entries) { + std::lock_guard lock(mutex_); + + if (cache_.size() <= max_entries) { + return; + } + + // Collect entries sorted by access time and count + std::vector>> + entries; + entries.reserve(cache_.size()); + + for (const auto& [path, entry] : cache_) { + // Skip game assets during LRU pruning (they have higher priority) + if (!entry.is_game_asset) { + entries.emplace_back(path, std::make_pair(entry.access_count, entry.last_access)); + } + } + + // Sort by access count (ascending), then by last access time (oldest first) + std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { + if (a.second.first != b.second.first) { + return a.second.first < b.second.first; + } + return a.second.second < b.second.second; + }); + + // Evict entries until we're under the limit + size_t to_evict = cache_.size() - max_entries; + for (size_t i = 0; i < std::min(to_evict, entries.size()); ++i) { + cache_.erase(entries[i].first); + ++evictions_; + } + + LOG_DEBUG(Common_Filesystem, "VFS cache LRU prune: evicted {} entries", to_evict); +} + +void VfsCache::WarmCache( + const std::vector& paths, + std::function>(const std::string&)> stat_func) { + LOG_INFO(Common_Filesystem, "VFS cache warming started with {} paths", paths.size()); + + size_t cached_count = 0; + for (const auto& path : paths) { + auto stat_result = stat_func(path); + if (stat_result) { + InsertExtended(path, stat_result->first, stat_result->second, true, 0); + ++cached_count; + } + } + + is_warm_ = true; + LOG_INFO(Common_Filesystem, "VFS cache warming complete: {} files cached", cached_count); +} + +bool VfsCache::IsWarm() const { + std::lock_guard lock(mutex_); + return is_warm_; +} + +VfsCache::Stats VfsCache::GetStats() const { + std::lock_guard lock(mutex_); + + u64 game_asset_count = 0; + u64 total_size = 0; + for (const auto& [path, entry] : cache_) { + if (entry.is_game_asset) { + ++game_asset_count; + } + total_size += entry.size; + } + + return Stats{ + .hits = hits_, + .misses = misses_, + .insertions = insertions_, + .evictions = evictions_, + .total_entries = cache_.size(), + .game_asset_entries = game_asset_count, + .directory_listings = dir_cache_.size(), + .total_cached_size = total_size, + }; +} + +u64 VfsCache::GetMemoryUsage() const { + std::lock_guard lock(mutex_); + + u64 usage = 0; + // Estimate memory usage per cache entry + for (const auto& [path, entry] : cache_) { + usage += sizeof(VfsCacheEntry); + usage += path.capacity(); + usage += entry.path.capacity(); + } + for (const auto& [path, entry] : dir_cache_) { + usage += sizeof(VfsDirCacheEntry); + usage += path.capacity(); + usage += entry.path.capacity(); + for (const auto& e : entry.entries) { + usage += e.capacity(); + } + } + return usage; +} + +bool VfsCache::IsExpired(const VfsCacheEntry& entry) const { + auto now = std::chrono::steady_clock::now(); + auto elapsed = + std::chrono::duration_cast(now - entry.last_access).count(); + + // Game assets have longer expiration + u64 expiration = entry.is_game_asset ? game_asset_expiration_seconds_ : expiration_seconds_; + return static_cast(elapsed) > expiration; +} + +bool VfsCache::IsDirListingExpired(const VfsDirCacheEntry& entry) const { + auto now = std::chrono::steady_clock::now(); + auto elapsed = + std::chrono::duration_cast(now - entry.last_access).count(); + return static_cast(elapsed) > dir_listing_expiration_seconds_; +} + +} // namespace Common diff --git a/src/common/vfs_cache.h b/src/common/vfs_cache.h new file mode 100644 index 000000000..d40688bb7 --- /dev/null +++ b/src/common/vfs_cache.h @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "common/common_types.h" + +namespace Common { + +// OPTIMIZATION: Thor / Adreno 740 support - eliminates stutters from VFS latency +// VFS Cache for bypassing Android Scoped Storage latency +// Uses thread-safe singleton pattern for O(1) memory-mapped file indexing + +struct VfsCacheEntry { + std::string path; + u64 size; + u64 timestamp; // File modification time + std::chrono::steady_clock::time_point last_access; + u64 content_hash; // Optional hash for integrity verification + bool is_game_asset; // Priority flag for game assets (higher cache retention) + u32 access_count; // LRU tracking for smart eviction +}; + +struct VfsDirCacheEntry { + std::string path; + std::vector entries; // Directory listing + std::chrono::steady_clock::time_point last_access; + u64 timestamp; // Directory modification time +}; + +class VfsCache { +public: + static VfsCache& Instance(); + + // Delete copy/move constructors for singleton + VfsCache(const VfsCache&) = delete; + VfsCache& operator=(const VfsCache&) = delete; + VfsCache(VfsCache&&) = delete; + VfsCache& operator=(VfsCache&&) = delete; + + // Core cache operations + std::optional Lookup(const std::string& path); + std::optional LookupAndTouch(const std::string& path); + void Insert(const std::string& path, u64 size, u64 timestamp); + void InsertExtended(const std::string& path, u64 size, u64 timestamp, + bool is_game_asset = false, u64 content_hash = 0); + void Invalidate(const std::string& path); + void InvalidatePrefix(const std::string& prefix); + void Clear(); + + // Directory listing cache + void InsertDirectoryListing(const std::string& path, const std::vector& entries, + u64 timestamp); + std::optional> LookupDirectoryListing(const std::string& path); + + // Configuration + void SetExpirationSeconds(u64 seconds); + void SetGameAssetExpirationSeconds(u64 seconds); + + // Cache maintenance + void PruneLRU(size_t max_entries); + void WarmCache(const std::vector& paths, + std::function>(const std::string&)> stat_func); + bool IsWarm() const; + + // Statistics + struct Stats { + u64 hits; + u64 misses; + u64 insertions; + u64 evictions; + u64 total_entries; + u64 game_asset_entries; + u64 directory_listings; + u64 total_cached_size; + }; + Stats GetStats() const; + u64 GetMemoryUsage() const; + +private: + VfsCache(); + ~VfsCache() = default; + + bool IsExpired(const VfsCacheEntry& entry) const; + bool IsDirListingExpired(const VfsDirCacheEntry& entry) const; + + mutable std::mutex mutex_; + std::unordered_map cache_; + std::unordered_map dir_cache_; + + u64 expiration_seconds_ = 60; // Default 60 second expiration + u64 game_asset_expiration_seconds_ = 300; // 5 minutes for game assets + u64 dir_listing_expiration_seconds_ = 30; // 30 seconds for directory listings + + // Statistics + mutable u64 hits_ = 0; + mutable u64 misses_ = 0; + u64 insertions_ = 0; + u64 evictions_ = 0; + + // Cache state + bool is_warm_ = false; + + // PROJECT PATHFINDER OPTIMIZATION: Increased cache limits for Thor 12GB devices + static constexpr size_t MAX_CACHE_SIZE = 100000; // Support large game asset collections + static constexpr size_t MAX_DIR_CACHE_SIZE = 10000; // Directory listing cache limit +}; + +} // namespace Common