mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-22 17:46:08 -04:00
feat(common): add VFS cache for Android Scoped Storage bypass
- Implement thread-safe singleton VfsCache class for O(1) file metadata lookup - Add game asset priority caching with extended expiration (5 min vs 60s) - Include directory listing cache for reduced SAF overhead - Support LRU-based smart eviction preserving game assets - Increase cache limits to 100K entries for high-memory devices (12GB Thor) Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -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
|
||||
|
||||
348
src/common/vfs_cache.cpp
Normal file
348
src/common/vfs_cache.cpp
Normal file
@@ -0,0 +1,348 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#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<VfsCacheEntry> 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<VfsCacheEntry> 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<u32>::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<std::string>& 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<std::vector<std::string>> 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<std::pair<std::string, std::pair<u32, std::chrono::steady_clock::time_point>>>
|
||||
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<std::string>& paths,
|
||||
std::function<std::optional<std::pair<u64, u64>>(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<std::chrono::seconds>(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<u64>(elapsed) > expiration;
|
||||
}
|
||||
|
||||
bool VfsCache::IsDirListingExpired(const VfsDirCacheEntry& entry) const {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(now - entry.last_access).count();
|
||||
return static_cast<u64>(elapsed) > dir_listing_expiration_seconds_;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
117
src/common/vfs_cache.h
Normal file
117
src/common/vfs_cache.h
Normal file
@@ -0,0 +1,117 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> 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<VfsCacheEntry> Lookup(const std::string& path);
|
||||
std::optional<VfsCacheEntry> 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<std::string>& entries,
|
||||
u64 timestamp);
|
||||
std::optional<std::vector<std::string>> 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<std::string>& paths,
|
||||
std::function<std::optional<std::pair<u64, u64>>(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<std::string, VfsCacheEntry> cache_;
|
||||
std::unordered_map<std::string, VfsDirCacheEntry> 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
|
||||
Reference in New Issue
Block a user