mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	Implement RomFS cache and async reads. (#7089)
* Implement RomFS cache and async reads. * Suggestions and fix compilation. * Apply suggestions
This commit is contained in:
		
							parent
							
								
									79ea06b226
								
							
						
					
					
						commit
						4284893044
					
				
					 10 changed files with 404 additions and 22 deletions
				
			
		|  | @ -86,6 +86,20 @@ public: | |||
|      */ | ||||
|     virtual void Flush() const = 0; | ||||
| 
 | ||||
|     /**
 | ||||
|      * Whether the backend supports cached reads. | ||||
|      */ | ||||
|     virtual bool AllowsCachedReads() const { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /**
 | ||||
|      * Whether the cache is ready for a specified offset and length. | ||||
|      */ | ||||
|     virtual bool CacheReady(std::size_t file_offset, std::size_t length) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
| protected: | ||||
|     std::unique_ptr<DelayGenerator> delay_generator; | ||||
| 
 | ||||
|  |  | |||
|  | @ -131,6 +131,14 @@ public: | |||
|     } | ||||
|     void Flush() const override {} | ||||
| 
 | ||||
|     bool AllowsCachedReads() const override { | ||||
|         return romfs_file->AllowsCachedReads(); | ||||
|     } | ||||
| 
 | ||||
|     bool CacheReady(std::size_t file_offset, std::size_t length) override { | ||||
|         return romfs_file->CacheReady(file_offset, length); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     std::shared_ptr<RomFSReader> romfs_file; | ||||
| 
 | ||||
|  |  | |||
|  | @ -53,6 +53,14 @@ public: | |||
| 
 | ||||
|     bool DumpRomFS(const std::string& target_path); | ||||
| 
 | ||||
|     bool AllowsCachedReads() const override { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     bool CacheReady(std::size_t file_offset, std::size_t length) override { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     struct File; | ||||
|     struct Directory { | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| #include <algorithm> | ||||
| #include <vector> | ||||
| #include <cryptopp/aes.h> | ||||
| #include <cryptopp/modes.h> | ||||
| #include "common/archives.h" | ||||
|  | @ -9,17 +10,102 @@ SERIALIZE_EXPORT_IMPL(FileSys::DirectRomFSReader) | |||
| namespace FileSys { | ||||
| 
 | ||||
| std::size_t DirectRomFSReader::ReadFile(std::size_t offset, std::size_t length, u8* buffer) { | ||||
|     length = std::min(length, static_cast<std::size_t>(data_size) - offset); | ||||
|     if (length == 0) | ||||
|         return 0; // Crypto++ does not like zero size buffer
 | ||||
|     file.Seek(file_offset + offset, SEEK_SET); | ||||
|     std::size_t read_length = std::min(length, static_cast<std::size_t>(data_size) - offset); | ||||
|     read_length = file.ReadBytes(buffer, read_length); | ||||
|     if (is_encrypted) { | ||||
|         CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption d(key.data(), key.size(), ctr.data()); | ||||
|         d.Seek(crypto_offset + offset); | ||||
|         d.ProcessData(buffer, buffer, read_length); | ||||
| 
 | ||||
|     const auto segments = BreakupRead(offset, length); | ||||
|     size_t read_progress = 0; | ||||
| 
 | ||||
|     // Skip cache if the read is too big
 | ||||
|     if (segments.size() == 1 && segments[0].second > cache_line_size) { | ||||
|         length = file.ReadAtBytes(buffer, length, file_offset + offset); | ||||
|         if (is_encrypted) { | ||||
|             CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption d(key.data(), key.size(), ctr.data()); | ||||
|             d.Seek(crypto_offset + offset); | ||||
|             d.ProcessData(buffer, buffer, length); | ||||
|         } | ||||
|         // LOG_INFO(Service_FS, "Cache SKIP: offset={}, length={}", offset, length);
 | ||||
|         return length; | ||||
|     } | ||||
|     return read_length; | ||||
| 
 | ||||
|     // TODO(PabloMK7): Make cache thread safe, read the comment in CacheReady function.
 | ||||
|     // std::unique_lock<std::shared_mutex> read_guard(cache_mutex);
 | ||||
|     for (const auto& seg : segments) { | ||||
|         size_t read_size = cache_line_size; | ||||
|         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 disk and cache the data
 | ||||
|             read_size = file.ReadAtBytes(cache_entry.second.data(), read_size, file_offset + page); | ||||
|             if (is_encrypted && read_size) { | ||||
|                 CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption d(key.data(), key.size(), ctr.data()); | ||||
|                 d.Seek(crypto_offset + page); | ||||
|                 d.ProcessData(cache_entry.second.data(), cache_entry.second.data(), read_size); | ||||
|             } | ||||
|             // LOG_INFO(Service_FS, "Cache MISS: page={}, length={}, into={}", page, seg.second,
 | ||||
|             //          (seg.first - page));
 | ||||
|         } else { | ||||
|             // LOG_INFO(Service_FS, "Cache HIT: page={}, length={}, into={}", page, seg.second,
 | ||||
|             //          (seg.first - page));
 | ||||
|         } | ||||
|         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 DirectRomFSReader::AllowsCachedReads() const { | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| bool DirectRomFSReader::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 { | ||||
|         // TODO(PabloMK7): Since the LRU cache is not thread safe, a lock must be used.
 | ||||
|         // However, this completely breaks the point of using a cache, because
 | ||||
|         // smaller reads may be blocked by bigger reads. For now, always return
 | ||||
|         // data being in cache to prevent the need of a lock, and only read data
 | ||||
|         // asynchronously if it is too big to use the cache.
 | ||||
|         /*
 | ||||
|         std::shared_lock<std::shared_mutex> read_guard(cache_mutex); | ||||
|         for (auto it = segments.begin(); it != segments.end(); it++) { | ||||
|             if (!cache.contains(OffsetToPage(it->first))) | ||||
|                 return false; | ||||
|         } | ||||
|         */ | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| std::vector<std::pair<std::size_t, std::size_t>> DirectRomFSReader::BreakupRead( | ||||
|     std::size_t offset, std::size_t length) { | ||||
| 
 | ||||
|     std::vector<std::pair<std::size_t, std::size_t>> ret; | ||||
| 
 | ||||
|     // Reads bigger than the cache line size will probably never hit again
 | ||||
|     if (length > cache_line_size) { | ||||
|         ret.push_back(std::make_pair(offset, length)); | ||||
|         return ret; | ||||
|     } | ||||
| 
 | ||||
|     size_t curr_offset = offset; | ||||
|     while (length) { | ||||
|         size_t next_page = OffsetToPage(curr_offset + cache_line_size); | ||||
|         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
 | ||||
|  |  | |||
|  | @ -1,11 +1,14 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <array> | ||||
| #include <shared_mutex> | ||||
| #include <boost/serialization/array.hpp> | ||||
| #include <boost/serialization/base_object.hpp> | ||||
| #include <boost/serialization/export.hpp> | ||||
| #include "common/alignment.h" | ||||
| #include "common/common_types.h" | ||||
| #include "common/file_util.h" | ||||
| #include "common/static_lru_cache.h" | ||||
| 
 | ||||
| namespace FileSys { | ||||
| 
 | ||||
|  | @ -18,6 +21,8 @@ public: | |||
| 
 | ||||
|     virtual std::size_t GetSize() const = 0; | ||||
|     virtual std::size_t ReadFile(std::size_t offset, std::size_t length, u8* buffer) = 0; | ||||
|     virtual bool AllowsCachedReads() const = 0; | ||||
|     virtual bool CacheReady(std::size_t file_offset, std::size_t length) = 0; | ||||
| 
 | ||||
| private: | ||||
|     template <class Archive> | ||||
|  | @ -48,6 +53,10 @@ public: | |||
| 
 | ||||
|     std::size_t ReadFile(std::size_t offset, std::size_t length, u8* buffer) override; | ||||
| 
 | ||||
|     bool AllowsCachedReads() const override; | ||||
| 
 | ||||
|     bool CacheReady(std::size_t file_offset, std::size_t length) override; | ||||
| 
 | ||||
| private: | ||||
|     bool is_encrypted; | ||||
|     FileUtil::IOFile file; | ||||
|  | @ -57,8 +66,23 @@ private: | |||
|     u64 crypto_offset; | ||||
|     u64 data_size; | ||||
| 
 | ||||
|     // Total cache size: 128KB
 | ||||
|     static constexpr size_t cache_line_size = (1 << 13); // About 8KB
 | ||||
|     static constexpr size_t cache_line_count = 16; | ||||
| 
 | ||||
|     Common::StaticLRUCache<std::size_t, std::array<u8, cache_line_size>, cache_line_count> cache; | ||||
|     // TODO(PabloMK7): Make cache thread safe, read the comment in CacheReady function.
 | ||||
|     // std::shared_mutex cache_mutex;
 | ||||
| 
 | ||||
|     DirectRomFSReader() = default; | ||||
| 
 | ||||
|     std::size_t OffsetToPage(std::size_t offset) { | ||||
|         return Common::AlignDown<std::size_t>(offset, cache_line_size); | ||||
|     } | ||||
| 
 | ||||
|     std::vector<std::pair<std::size_t, std::size_t>> BreakupRead(std::size_t offset, | ||||
|                                                                  std::size_t length); | ||||
| 
 | ||||
|     template <class Archive> | ||||
|     void serialize(Archive& ar, const unsigned int) { | ||||
|         ar& boost::serialization::base_object<RomFSReader>(*this); | ||||
|  |  | |||
|  | @ -57,7 +57,6 @@ void File::Read(Kernel::HLERequestContext& ctx) { | |||
|     IPC::RequestParser rp(ctx); | ||||
|     u64 offset = rp.Pop<u64>(); | ||||
|     u32 length = rp.Pop<u32>(); | ||||
|     auto& buffer = rp.PopMappedBuffer(); | ||||
|     LOG_TRACE(Service_FS, "Read {}: offset=0x{:x} length=0x{:08X}", GetName(), offset, length); | ||||
| 
 | ||||
|     const FileSessionSlot* file = GetSessionData(ctx.Session()); | ||||
|  | @ -76,22 +75,94 @@ void File::Read(Kernel::HLERequestContext& ctx) { | |||
|                   offset, length, backend->GetSize()); | ||||
|     } | ||||
| 
 | ||||
|     IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); | ||||
|     // Conventional reading if the backend does not support cache.
 | ||||
|     if (!backend->AllowsCachedReads()) { | ||||
|         auto& buffer = rp.PopMappedBuffer(); | ||||
|         IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); | ||||
|         std::unique_ptr<u8*> data = std::make_unique<u8*>(static_cast<u8*>(operator new(length))); | ||||
|         const auto read = backend->Read(offset, length, *data); | ||||
|         if (read.Failed()) { | ||||
|             rb.Push(read.Code()); | ||||
|             rb.Push<u32>(0); | ||||
|         } else { | ||||
|             buffer.Write(*data, 0, *read); | ||||
|             rb.Push(RESULT_SUCCESS); | ||||
|             rb.Push<u32>(static_cast<u32>(*read)); | ||||
|         } | ||||
|         rb.PushMappedBuffer(buffer); | ||||
| 
 | ||||
|     std::vector<u8> data(length); | ||||
|     ResultVal<std::size_t> read = backend->Read(offset, data.size(), data.data()); | ||||
|     if (read.Failed()) { | ||||
|         rb.Push(read.Code()); | ||||
|         rb.Push<u32>(0); | ||||
|     } else { | ||||
|         buffer.Write(data.data(), 0, *read); | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u32>(static_cast<u32>(*read)); | ||||
|         std::chrono::nanoseconds read_timeout_ns{backend->GetReadDelayNs(length)}; | ||||
|         ctx.SleepClientThread("file::read", read_timeout_ns, nullptr); | ||||
|         return; | ||||
|     } | ||||
|     rb.PushMappedBuffer(buffer); | ||||
| 
 | ||||
|     std::chrono::nanoseconds read_timeout_ns{backend->GetReadDelayNs(length)}; | ||||
|     ctx.SleepClientThread("file::read", read_timeout_ns, nullptr); | ||||
|     struct AsyncData { | ||||
|         // Input
 | ||||
|         u32 length; | ||||
|         u64 offset; | ||||
|         std::chrono::steady_clock::time_point pre_timer; | ||||
|         bool cache_ready; | ||||
| 
 | ||||
|         // Output
 | ||||
|         ResultCode ret{0}; | ||||
|         Kernel::MappedBuffer* buffer; | ||||
|         std::unique_ptr<u8*> data; | ||||
|         size_t read_size; | ||||
|     }; | ||||
| 
 | ||||
|     auto async_data = std::make_shared<AsyncData>(); | ||||
|     async_data->buffer = &rp.PopMappedBuffer(); | ||||
|     async_data->length = length; | ||||
|     async_data->offset = offset; | ||||
|     async_data->cache_ready = backend->CacheReady(offset, length); | ||||
|     if (!async_data->cache_ready) { | ||||
|         async_data->pre_timer = std::chrono::steady_clock::now(); | ||||
|     } | ||||
| 
 | ||||
|     // LOG_DEBUG(Service_FS, "cache={}, offset={}, length={}", cache_ready, offset, length);
 | ||||
|     ctx.RunAsync( | ||||
|         [this, async_data](Kernel::HLERequestContext& ctx) { | ||||
|             async_data->data = | ||||
|                 std::make_unique<u8*>(static_cast<u8*>(operator new(async_data->length))); | ||||
|             const auto read = | ||||
|                 backend->Read(async_data->offset, async_data->length, *async_data->data); | ||||
|             if (read.Failed()) { | ||||
|                 async_data->ret = read.Code(); | ||||
|                 async_data->read_size = 0; | ||||
|             } else { | ||||
|                 async_data->ret = RESULT_SUCCESS; | ||||
|                 async_data->read_size = *read; | ||||
|             } | ||||
| 
 | ||||
|             const auto read_delay = static_cast<s64>(backend->GetReadDelayNs(async_data->length)); | ||||
|             if (!async_data->cache_ready) { | ||||
|                 const auto time_took = std::chrono::duration_cast<std::chrono::nanoseconds>( | ||||
|                                            std::chrono::steady_clock::now() - async_data->pre_timer) | ||||
|                                            .count(); | ||||
|                 /*
 | ||||
|                 if (time_took > read_delay) { | ||||
|                     LOG_DEBUG(Service_FS, "Took longer! length={}, time_took={}, read_delay={}", | ||||
|                               async_data->length, time_took, read_delay); | ||||
|                 } | ||||
|                 */ | ||||
|                 return static_cast<s64>((read_delay > time_took) ? (read_delay - time_took) : 0); | ||||
|             } else { | ||||
|                 return static_cast<s64>(read_delay); | ||||
|             } | ||||
|         }, | ||||
|         [async_data](Kernel::HLERequestContext& ctx) { | ||||
|             IPC::RequestBuilder rb(ctx, 0x0802, 2, 2); | ||||
|             if (async_data->ret.IsError()) { | ||||
|                 rb.Push(async_data->ret); | ||||
|                 rb.Push<u32>(0); | ||||
|             } else { | ||||
|                 async_data->buffer->Write(*async_data->data, 0, async_data->read_size); | ||||
|                 rb.Push(RESULT_SUCCESS); | ||||
|                 rb.Push<u32>(static_cast<u32>(async_data->read_size)); | ||||
|             } | ||||
|             rb.PushMappedBuffer(*async_data->buffer); | ||||
|         }, | ||||
|         !async_data->cache_ready); | ||||
| } | ||||
| 
 | ||||
| void File::Write(Kernel::HLERequestContext& ctx) { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue