// Copyright 2024 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "artic_cache.h" namespace FileSys { ResultVal ArticCache::Read(s32 file_handle, std::size_t offset, std::size_t length, u8* buffer) { if (length == 0) return size_t(); const auto segments = BreakupRead(offset, length); std::size_t read_progress = 0; // Skip cache if the read is too big if (segments.size() == 1 && segments[0].second > cache_line_size) { if (segments[0].second < big_cache_skip) { std::unique_lock big_read_guard(big_cache_mutex); auto big_cache_entry = big_cache.request(std::make_pair(offset, length)); if (!big_cache_entry.first) { LOG_TRACE(Service_FS, "ArticCache BMISS: offset={}, length={}", offset, length); big_cache_entry.second.clear(); big_cache_entry.second.resize(length); auto res = ReadFromArtic(file_handle, reinterpret_cast(big_cache_entry.second.data()), length, offset); if (res.Failed()) return res; length = res.Unwrap(); } else { LOG_TRACE(Service_FS, "ArticCache BHIT: offset={}, length={}", offset, length); } memcpy(buffer, big_cache_entry.second.data(), length); } else { if (segments[0].second < very_big_cache_skip) { std::unique_lock very_big_read_guard(very_big_cache_mutex); auto very_big_cache_entry = very_big_cache.request(std::make_pair(offset, length)); if (!very_big_cache_entry.first) { LOG_TRACE(Service_FS, "ArticCache VBMISS: offset={}, length={}", offset, length); very_big_cache_entry.second.clear(); very_big_cache_entry.second.resize(length); auto res = ReadFromArtic( file_handle, reinterpret_cast(very_big_cache_entry.second.data()), length, offset); if (res.Failed()) return res; length = res.Unwrap(); } else { LOG_TRACE(Service_FS, "ArticCache VBHIT: offset={}, length={}", offset, length); } memcpy(buffer, very_big_cache_entry.second.data(), length); } else { LOG_TRACE(Service_FS, "ArticCache SKIP: offset={}, length={}", offset, length); auto res = ReadFromArtic(file_handle, buffer, length, offset); if (res.Failed()) return res; length = res.Unwrap(); } } return length; } // TODO(PabloMK7): Make cache thread safe, read the comment in CacheReady function. std::unique_lock read_guard(cache_mutex); for (const auto& seg : segments) { std::size_t read_size = cache_line_size; std::size_t page = OffsetToPage(seg.first); // Check if segment is in cache auto cache_entry = cache.request(page); if (!cache_entry.first) { // If not found, read from artic and cache the data auto res = ReadFromArtic(file_handle, cache_entry.second.data(), read_size, page); if (res.Failed()) return res; read_size = res.Unwrap(); LOG_TRACE(Service_FS, "ArticCache MISS: page={}, length={}, into={}", page, seg.second, (seg.first - page)); } else { LOG_TRACE(Service_FS, "ArticCache HIT: page={}, length={}, into={}", page, seg.second, (seg.first - page)); } std::size_t copy_amount = (read_size > (seg.first - page)) ? std::min((seg.first - page) + seg.second, read_size) - (seg.first - page) : 0; std::memcpy(buffer + read_progress, cache_entry.second.data() + (seg.first - page), copy_amount); read_progress += copy_amount; } return read_progress; } bool ArticCache::CacheReady(std::size_t file_offset, std::size_t length) { auto segments = BreakupRead(file_offset, length); if (segments.size() == 1 && segments[0].second > cache_line_size) { return false; } else { std::shared_lock read_guard(cache_mutex); for (auto it = segments.begin(); it != segments.end(); it++) { if (!cache.contains(OffsetToPage(it->first))) return false; } return true; } } void ArticCache::Clear() { std::unique_lock l1(cache_mutex), l2(big_cache_mutex), l3(very_big_cache_mutex); cache.clear(); big_cache.clear(); very_big_cache.clear(); data_size = std::nullopt; } ResultVal ArticCache::Write(s32 file_handle, std::size_t offset, std::size_t length, const u8* buffer, u32 flags) { // Can probably do better, but write operations are usually done at the end, so it doesn't // matter much Clear(); size_t written_amount = 0; while (written_amount != length) { size_t to_write = std::min(client->GetServerRequestMaxSize() - 0x100, length - written_amount); auto req = client->NewRequest("FSFILE_Write"); req.AddParameterS32(file_handle); req.AddParameterS64(static_cast(offset + written_amount)); req.AddParameterS32(static_cast(to_write)); req.AddParameterS32(static_cast(flags)); req.AddParameterBuffer(buffer + written_amount, to_write); auto resp = client->Send(req); if (!resp.has_value() || !resp->Succeeded()) return Result(-1); auto res = Result(static_cast(resp->GetMethodResult())); if (res.IsError()) return res; auto actually_written_opt = resp->GetResponseS32(0); if (!actually_written_opt.has_value()) return Result(-1); size_t actually_written = static_cast(actually_written_opt.value()); written_amount += actually_written; if (actually_written != to_write) break; } return written_amount; } ResultVal ArticCache::GetSize(s32 file_handle) { std::unique_lock l1(cache_mutex); if (data_size.has_value()) return data_size.value(); auto req = client->NewRequest("FSFILE_GetSize"); req.AddParameterS32(file_handle); auto resp = client->Send(req); if (!resp.has_value() || !resp->Succeeded()) return Result(-1); auto res = Result(static_cast(resp->GetMethodResult())); if (res.IsError()) return res; auto size_buf = resp->GetResponseS64(0); if (!size_buf) { return Result(-1); } data_size = static_cast(*size_buf); return data_size.value(); } ResultVal ArticCache::ReadFromArtic(s32 file_handle, u8* buffer, size_t len, size_t offset) { size_t read_amount = 0; while (read_amount != len) { size_t to_read = std::min(client->GetServerRequestMaxSize() - 0x100, len - read_amount); auto req = client->NewRequest("FSFILE_Read"); req.AddParameterS32(file_handle); req.AddParameterS64(static_cast(offset + read_amount)); req.AddParameterS32(static_cast(to_read)); auto resp = client->Send(req); if (!resp.has_value() || !resp->Succeeded()) return Result(-1); auto res = Result(static_cast(resp->GetMethodResult())); if (res.IsError()) return res; auto read_buff = resp->GetResponseBuffer(0); size_t actually_read = 0; if (read_buff.has_value()) { actually_read = read_buff->second; memcpy(buffer + read_amount, read_buff->first, actually_read); } read_amount += actually_read; if (actually_read != to_read) break; } return read_amount; } std::vector> ArticCache::BreakupRead(std::size_t offset, std::size_t length) { std::vector> ret; // Reads bigger than the cache line size will probably never hit again if (length > max_breakup_size) { ret.push_back(std::make_pair(offset, length)); return ret; } std::size_t curr_offset = offset; while (length) { std::size_t next_page = OffsetToPage(curr_offset + cache_line_size); std::size_t curr_page_len = std::min(length, next_page - curr_offset); ret.push_back(std::make_pair(curr_offset, curr_page_len)); curr_offset = next_page; length -= curr_page_len; } return ret; } } // namespace FileSys