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 }