duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

gpu_shader_cache.cpp (10260B)


      1 // SPDX-FileCopyrightText: 2023-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "gpu_shader_cache.h"
      5 #include "gpu_device.h"
      6 
      7 #include "common/error.h"
      8 #include "common/file_system.h"
      9 #include "common/heap_array.h"
     10 #include "common/log.h"
     11 #include "common/md5_digest.h"
     12 #include "common/path.h"
     13 
     14 #include "fmt/format.h"
     15 
     16 #include "compress_helpers.h"
     17 
     18 Log_SetChannel(GPUShaderCache);
     19 
     20 #pragma pack(push, 1)
     21 struct CacheIndexEntry
     22 {
     23   u8 shader_type;
     24   u8 shader_language;
     25   u8 unused[2];
     26   u32 source_length;
     27   u64 source_hash_low;
     28   u64 source_hash_high;
     29   u64 entry_point_low;
     30   u64 entry_point_high;
     31   u32 file_offset;
     32   u32 compressed_size;
     33   u32 uncompressed_size;
     34 };
     35 #pragma pack(pop)
     36 
     37 GPUShaderCache::GPUShaderCache() = default;
     38 
     39 GPUShaderCache::~GPUShaderCache()
     40 {
     41   Close();
     42 }
     43 
     44 bool GPUShaderCache::CacheIndexKey::operator==(const CacheIndexKey& key) const
     45 {
     46   return (std::memcmp(this, &key, sizeof(*this)) == 0);
     47 }
     48 
     49 bool GPUShaderCache::CacheIndexKey::operator!=(const CacheIndexKey& key) const
     50 {
     51   return (std::memcmp(this, &key, sizeof(*this)) != 0);
     52 }
     53 
     54 std::size_t GPUShaderCache::CacheIndexEntryHash::operator()(const CacheIndexKey& e) const noexcept
     55 {
     56   std::size_t h = 0;
     57   hash_combine(h, e.entry_point_low, e.entry_point_high, e.source_hash_low, e.source_hash_high, e.source_length,
     58                e.shader_type);
     59   return h;
     60 }
     61 
     62 bool GPUShaderCache::Open(std::string_view base_filename, u32 version)
     63 {
     64   m_base_filename = base_filename;
     65   m_version = version;
     66 
     67   if (base_filename.empty())
     68     return true;
     69 
     70   const std::string index_filename = fmt::format("{}.idx", m_base_filename);
     71   const std::string blob_filename = fmt::format("{}.bin", m_base_filename);
     72   return ReadExisting(index_filename, blob_filename);
     73 }
     74 
     75 bool GPUShaderCache::Create()
     76 {
     77   const std::string index_filename = fmt::format("{}.idx", m_base_filename);
     78   const std::string blob_filename = fmt::format("{}.bin", m_base_filename);
     79   return CreateNew(index_filename, blob_filename);
     80 }
     81 
     82 void GPUShaderCache::Close()
     83 {
     84   if (m_index_file)
     85   {
     86     std::fclose(m_index_file);
     87     m_index_file = nullptr;
     88   }
     89   if (m_blob_file)
     90   {
     91     std::fclose(m_blob_file);
     92     m_blob_file = nullptr;
     93   }
     94 }
     95 
     96 void GPUShaderCache::Clear()
     97 {
     98   if (!IsOpen())
     99     return;
    100 
    101   Close();
    102 
    103   WARNING_LOG("Clearing shader cache at {}.", Path::GetFileName(m_base_filename));
    104 
    105   const std::string index_filename = fmt::format("{}.idx", m_base_filename);
    106   const std::string blob_filename = fmt::format("{}.bin", m_base_filename);
    107   CreateNew(index_filename, blob_filename);
    108 }
    109 
    110 bool GPUShaderCache::CreateNew(const std::string& index_filename, const std::string& blob_filename)
    111 {
    112   if (FileSystem::FileExists(index_filename.c_str()))
    113   {
    114     WARNING_LOG("Removing existing index file '{}'", Path::GetFileName(index_filename));
    115     FileSystem::DeleteFile(index_filename.c_str());
    116   }
    117   if (FileSystem::FileExists(blob_filename.c_str()))
    118   {
    119     WARNING_LOG("Removing existing blob file '{}'", Path::GetFileName(blob_filename));
    120     FileSystem::DeleteFile(blob_filename.c_str());
    121   }
    122 
    123   m_index_file = FileSystem::OpenCFile(index_filename.c_str(), "wb");
    124   if (!m_index_file) [[unlikely]]
    125   {
    126     ERROR_LOG("Failed to open index file '{}' for writing", Path::GetFileName(index_filename));
    127     return false;
    128   }
    129 
    130   if (std::fwrite(&m_version, sizeof(m_version), 1, m_index_file) != 1) [[unlikely]]
    131   {
    132     ERROR_LOG("Failed to write version to index file '{}'", Path::GetFileName(index_filename));
    133     std::fclose(m_index_file);
    134     m_index_file = nullptr;
    135     FileSystem::DeleteFile(index_filename.c_str());
    136     return false;
    137   }
    138 
    139   m_blob_file = FileSystem::OpenCFile(blob_filename.c_str(), "w+b");
    140   if (!m_blob_file) [[unlikely]]
    141   {
    142     ERROR_LOG("Failed to open blob file '{}' for writing", Path::GetFileName(blob_filename));
    143     std::fclose(m_index_file);
    144     m_index_file = nullptr;
    145     FileSystem::DeleteFile(index_filename.c_str());
    146     return false;
    147   }
    148 
    149   return true;
    150 }
    151 
    152 bool GPUShaderCache::ReadExisting(const std::string& index_filename, const std::string& blob_filename)
    153 {
    154   m_index_file = FileSystem::OpenCFile(index_filename.c_str(), "r+b");
    155   if (!m_index_file)
    156   {
    157     // special case here: when there's a sharing violation (i.e. two instances running),
    158     // we don't want to blow away the cache. so just continue without a cache.
    159     if (errno == EACCES)
    160     {
    161       WARNING_LOG("Failed to open shader cache index with EACCES, are you running two instances?");
    162       return true;
    163     }
    164 
    165     return false;
    166   }
    167 
    168   u32 file_version = 0;
    169   if (std::fread(&file_version, sizeof(file_version), 1, m_index_file) != 1 || file_version != m_version) [[unlikely]]
    170   {
    171     ERROR_LOG("Bad file/data version in '{}'", Path::GetFileName(index_filename));
    172     std::fclose(m_index_file);
    173     m_index_file = nullptr;
    174     return false;
    175   }
    176 
    177   m_blob_file = FileSystem::OpenCFile(blob_filename.c_str(), "a+b");
    178   if (!m_blob_file) [[unlikely]]
    179   {
    180     ERROR_LOG("Blob file '{}' is missing", Path::GetFileName(blob_filename));
    181     std::fclose(m_index_file);
    182     m_index_file = nullptr;
    183     return false;
    184   }
    185 
    186   std::fseek(m_blob_file, 0, SEEK_END);
    187   const u32 blob_file_size = static_cast<u32>(std::ftell(m_blob_file));
    188 
    189   for (;;)
    190   {
    191     CacheIndexEntry entry;
    192     if (std::fread(&entry, sizeof(entry), 1, m_index_file) != 1 ||
    193         (entry.file_offset + entry.compressed_size) > blob_file_size) [[unlikely]]
    194     {
    195       if (std::feof(m_index_file))
    196         break;
    197 
    198       ERROR_LOG("Failed to read entry from '{}', corrupt file?", Path::GetFileName(index_filename));
    199       m_index.clear();
    200       std::fclose(m_blob_file);
    201       m_blob_file = nullptr;
    202       std::fclose(m_index_file);
    203       m_index_file = nullptr;
    204       return false;
    205     }
    206 
    207     const CacheIndexKey key{entry.shader_type,     entry.shader_language, {},
    208                             entry.source_length,   entry.source_hash_low, entry.source_hash_high,
    209                             entry.entry_point_low, entry.entry_point_high};
    210     const CacheIndexData data{entry.file_offset, entry.compressed_size, entry.uncompressed_size};
    211     m_index.emplace(key, data);
    212   }
    213 
    214   // ensure we don't write before seeking
    215   std::fseek(m_index_file, 0, SEEK_END);
    216 
    217   DEV_LOG("Read {} entries from '{}'", m_index.size(), Path::GetFileName(index_filename));
    218   return true;
    219 }
    220 
    221 GPUShaderCache::CacheIndexKey GPUShaderCache::GetCacheKey(GPUShaderStage stage, GPUShaderLanguage language,
    222                                                           std::string_view shader_code, std::string_view entry_point)
    223 {
    224   union
    225   {
    226     struct
    227     {
    228       u64 hash_low;
    229       u64 hash_high;
    230     };
    231     u8 hash[16];
    232   } h;
    233 
    234   CacheIndexKey key = {};
    235   key.shader_type = static_cast<u8>(stage);
    236   key.shader_language = static_cast<u8>(language);
    237 
    238   MD5Digest digest;
    239   digest.Update(shader_code.data(), static_cast<u32>(shader_code.length()));
    240   digest.Final(h.hash);
    241   key.source_hash_low = h.hash_low;
    242   key.source_hash_high = h.hash_high;
    243   key.source_length = static_cast<u32>(shader_code.length());
    244 
    245   digest.Reset();
    246   digest.Update(entry_point.data(), static_cast<u32>(entry_point.length()));
    247   digest.Final(h.hash);
    248   key.entry_point_low = h.hash_low;
    249   key.entry_point_high = h.hash_high;
    250 
    251   return key;
    252 }
    253 
    254 std::optional<GPUShaderCache::ShaderBinary> GPUShaderCache::Lookup(const CacheIndexKey& key)
    255 {
    256   std::optional<ShaderBinary> ret;
    257 
    258   auto iter = m_index.find(key);
    259   if (iter != m_index.end())
    260   {
    261     DynamicHeapArray<u8> compressed_data(iter->second.compressed_size);
    262 
    263     if (std::fseek(m_blob_file, iter->second.file_offset, SEEK_SET) != 0 ||
    264         std::fread(compressed_data.data(), iter->second.compressed_size, 1, m_blob_file) != 1) [[unlikely]]
    265     {
    266       ERROR_LOG("Read {} byte {} shader from file failed", iter->second.compressed_size,
    267                 GPUShader::GetStageName(static_cast<GPUShaderStage>(key.shader_type)));
    268     }
    269     else
    270     {
    271       Error error;
    272       ret = CompressHelpers::DecompressBuffer(CompressHelpers::CompressType::Zstandard,
    273                                               CompressHelpers::OptionalByteBuffer(std::move(compressed_data)),
    274                                               iter->second.uncompressed_size, &error);
    275       if (!ret.has_value()) [[unlikely]]
    276         ERROR_LOG("Failed to decompress shader: {}", error.GetDescription());
    277     }
    278   }
    279 
    280   return ret;
    281 }
    282 
    283 bool GPUShaderCache::Insert(const CacheIndexKey& key, const void* data, u32 data_size)
    284 {
    285   Error error;
    286   CompressHelpers::OptionalByteBuffer compress_buffer =
    287     CompressHelpers::CompressToBuffer(CompressHelpers::CompressType::Zstandard, data, data_size, -1, &error);
    288   if (!compress_buffer.has_value()) [[unlikely]]
    289   {
    290     ERROR_LOG("Failed to compress shader: {}", error.GetDescription());
    291     return false;
    292   }
    293 
    294   if (!m_blob_file || std::fseek(m_blob_file, 0, SEEK_END) != 0)
    295     return false;
    296 
    297   CacheIndexData idata;
    298   idata.file_offset = static_cast<u32>(std::ftell(m_blob_file));
    299   idata.compressed_size = static_cast<u32>(compress_buffer->size());
    300   idata.uncompressed_size = data_size;
    301 
    302   CacheIndexEntry entry = {};
    303   entry.shader_type = static_cast<u8>(key.shader_type);
    304   entry.shader_language = static_cast<u8>(key.shader_language);
    305   entry.source_length = key.source_length;
    306   entry.source_hash_low = key.source_hash_low;
    307   entry.source_hash_high = key.source_hash_high;
    308   entry.entry_point_low = key.entry_point_low;
    309   entry.entry_point_high = key.entry_point_high;
    310   entry.file_offset = idata.file_offset;
    311   entry.compressed_size = idata.compressed_size;
    312   entry.uncompressed_size = idata.uncompressed_size;
    313 
    314   if (std::fwrite(compress_buffer->data(), compress_buffer->size(), 1, m_blob_file) != 1 ||
    315       std::fflush(m_blob_file) != 0 || std::fwrite(&entry, sizeof(entry), 1, m_index_file) != 1 ||
    316       std::fflush(m_index_file) != 0) [[unlikely]]
    317   {
    318     ERROR_LOG("Failed to write {} byte {} shader blob to file", data_size,
    319               GPUShader::GetStageName(static_cast<GPUShaderStage>(key.shader_type)));
    320     return false;
    321   }
    322 
    323   DEV_LOG("Cached compressed {} shader: {} -> {} bytes",
    324           GPUShader::GetStageName(static_cast<GPUShaderStage>(key.shader_type)), data_size, compress_buffer->size());
    325   m_index.emplace(key, idata);
    326   return true;
    327 }