Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/util/gpu_shader_cache.cpp
7328 views
1
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: 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_CHANNEL(GPUDevice);
19
20
struct CacheFileHeader
21
{
22
u32 signature;
23
u32 render_api_version;
24
u32 cache_version;
25
};
26
static_assert(sizeof(CacheFileHeader) == 12, "Cache file header has no padding");
27
28
static constexpr u32 EXPECTED_SIGNATURE = 0x434B5544; // DUKC
29
30
static constexpr size_t KEY_COPY_SIZE = offsetof(GPUShaderCache::CacheIndexKey, unused);
31
32
template<typename A, typename B>
33
ALWAYS_INLINE static int CompareEntries(const A& a, const B& b)
34
{
35
// don't compare file fields when looking up
36
return std::memcmp(&a, &b, KEY_COPY_SIZE);
37
}
38
39
GPUShaderCache::GPUShaderCache()
40
{
41
static_assert(std::is_standard_layout_v<CacheIndexKey> && std::is_trivially_copyable_v<CacheIndexKey>,
42
"Cache key must be trivially copyable");
43
static_assert(std::is_standard_layout_v<CacheIndexEntry> && std::is_trivially_copyable_v<CacheIndexEntry>,
44
"Cache entry must be trivially copyable");
45
static_assert(offsetof(CacheIndexKey, shader_type) == offsetof(CacheIndexEntry, shader_type) &&
46
offsetof(CacheIndexKey, shader_language) == offsetof(CacheIndexEntry, shader_language) &&
47
offsetof(CacheIndexKey, source_hash_low) == offsetof(CacheIndexEntry, source_hash_low) &&
48
offsetof(CacheIndexKey, source_hash_high) == offsetof(CacheIndexEntry, source_hash_high) &&
49
offsetof(CacheIndexKey, entry_point_low) == offsetof(CacheIndexEntry, entry_point_low) &&
50
offsetof(CacheIndexKey, entry_point_high) == offsetof(CacheIndexEntry, entry_point_high) &&
51
offsetof(CacheIndexKey, source_length) == offsetof(CacheIndexEntry, source_length),
52
"Cache key and entry must have matching layout");
53
}
54
55
GPUShaderCache::~GPUShaderCache()
56
{
57
Close();
58
}
59
60
bool GPUShaderCache::Open(std::string_view base_filename, u32 render_api_version, u32 cache_version)
61
{
62
m_base_filename = base_filename;
63
m_render_api_version = render_api_version;
64
m_version = cache_version;
65
66
if (base_filename.empty())
67
return true;
68
69
const std::string index_filename = fmt::format("{}.idx", m_base_filename);
70
const std::string blob_filename = fmt::format("{}.bin", m_base_filename);
71
return ReadExisting(index_filename, blob_filename);
72
}
73
74
bool GPUShaderCache::Create()
75
{
76
const std::string index_filename = fmt::format("{}.idx", m_base_filename);
77
const std::string blob_filename = fmt::format("{}.bin", m_base_filename);
78
return CreateNew(index_filename, blob_filename);
79
}
80
81
void GPUShaderCache::Close()
82
{
83
if (m_index_file)
84
{
85
std::fclose(m_index_file);
86
m_index_file = nullptr;
87
}
88
if (m_blob_file)
89
{
90
std::fclose(m_blob_file);
91
m_blob_file = nullptr;
92
}
93
}
94
95
void GPUShaderCache::Clear()
96
{
97
if (!IsOpen())
98
return;
99
100
Close();
101
102
WARNING_LOG("Clearing shader cache at {}.", Path::GetFileName(m_base_filename));
103
104
const std::string index_filename = fmt::format("{}.idx", m_base_filename);
105
const std::string blob_filename = fmt::format("{}.bin", m_base_filename);
106
CreateNew(index_filename, blob_filename);
107
}
108
109
bool GPUShaderCache::CreateNew(const std::string& index_filename, const std::string& blob_filename)
110
{
111
if (FileSystem::FileExists(index_filename.c_str()))
112
{
113
WARNING_LOG("Removing existing index file '{}'", Path::GetFileName(index_filename));
114
FileSystem::DeleteFile(index_filename.c_str());
115
}
116
if (FileSystem::FileExists(blob_filename.c_str()))
117
{
118
WARNING_LOG("Removing existing blob file '{}'", Path::GetFileName(blob_filename));
119
FileSystem::DeleteFile(blob_filename.c_str());
120
}
121
122
m_index_file = FileSystem::OpenCFile(index_filename.c_str(), "wb");
123
if (!m_index_file) [[unlikely]]
124
{
125
ERROR_LOG("Failed to open index file '{}' for writing", Path::GetFileName(index_filename));
126
return false;
127
}
128
129
const CacheFileHeader file_header = {
130
.signature = EXPECTED_SIGNATURE, .render_api_version = m_render_api_version, .cache_version = m_version};
131
if (std::fwrite(&file_header, sizeof(file_header), 1, m_index_file) != 1) [[unlikely]]
132
{
133
ERROR_LOG("Failed to write version to index file '{}'", Path::GetFileName(index_filename));
134
std::fclose(m_index_file);
135
m_index_file = nullptr;
136
FileSystem::DeleteFile(index_filename.c_str());
137
return false;
138
}
139
140
m_blob_file = FileSystem::OpenCFile(blob_filename.c_str(), "w+b");
141
if (!m_blob_file) [[unlikely]]
142
{
143
ERROR_LOG("Failed to open blob file '{}' for writing", Path::GetFileName(blob_filename));
144
std::fclose(m_index_file);
145
m_index_file = nullptr;
146
FileSystem::DeleteFile(index_filename.c_str());
147
return false;
148
}
149
150
return true;
151
}
152
153
bool GPUShaderCache::ReadExisting(const std::string& index_filename, const std::string& blob_filename)
154
{
155
m_index_file = FileSystem::OpenCFile(index_filename.c_str(), "r+b");
156
if (!m_index_file)
157
{
158
// special case here: when there's a sharing violation (i.e. two instances running),
159
// we don't want to blow away the cache. so just continue without a cache.
160
if (errno == EACCES)
161
{
162
WARNING_LOG("Failed to open shader cache index with EACCES, are you running two instances?");
163
return true;
164
}
165
166
return false;
167
}
168
169
CacheFileHeader file_header;
170
if (std::fread(&file_header, sizeof(file_header), 1, m_index_file) != 1 ||
171
file_header.signature != EXPECTED_SIGNATURE || file_header.render_api_version != m_render_api_version ||
172
file_header.cache_version != m_version) [[unlikely]]
173
{
174
ERROR_LOG("Bad file/data version in '{}'", Path::GetFileName(index_filename));
175
std::fclose(m_index_file);
176
m_index_file = nullptr;
177
return false;
178
}
179
180
m_blob_file = FileSystem::OpenCFile(blob_filename.c_str(), "a+b");
181
if (!m_blob_file) [[unlikely]]
182
{
183
ERROR_LOG("Blob file '{}' is missing", Path::GetFileName(blob_filename));
184
std::fclose(m_index_file);
185
m_index_file = nullptr;
186
return false;
187
}
188
189
const s64 start_pos = FileSystem::FTell64(m_index_file);
190
s64 end_pos;
191
if (start_pos < 0 || !FileSystem::FSeek64(m_index_file, 0, SEEK_END, nullptr) ||
192
(end_pos = FileSystem::FTell64(m_index_file)) < 0 ||
193
!FileSystem::FSeek64(m_index_file, start_pos, SEEK_SET, nullptr) ||
194
((end_pos - start_pos) % sizeof(CacheIndexEntry)) != 0) [[unlikely]]
195
{
196
ERROR_LOG("Failed to seek in index file '{}'", Path::GetFileName(index_filename));
197
std::fclose(m_blob_file);
198
m_blob_file = nullptr;
199
std::fclose(m_index_file);
200
m_index_file = nullptr;
201
return false;
202
}
203
204
const size_t num_entries = static_cast<size_t>((end_pos - start_pos) / sizeof(CacheIndexEntry));
205
m_index.resize(num_entries);
206
207
if (std::fread(m_index.data(), sizeof(CacheIndexEntry), num_entries, m_index_file) != num_entries) [[unlikely]]
208
{
209
ERROR_LOG("Failed to read entries from index file '{}'", Path::GetFileName(index_filename));
210
m_index.clear();
211
std::fclose(m_blob_file);
212
m_blob_file = nullptr;
213
std::fclose(m_index_file);
214
m_index_file = nullptr;
215
return false;
216
}
217
218
// ensure we don't write before seeking
219
FileSystem::FSeek64(m_index_file, 0, SEEK_END);
220
221
// the index won't be sorted initially, since the file is append only
222
std::ranges::sort(m_index,
223
[](const CacheIndexEntry& a, const CacheIndexEntry& b) { return (CompareEntries(a, b) < 0); });
224
225
DEV_LOG("Read {} entries from '{}'", m_index.size(), Path::GetFileName(index_filename));
226
return true;
227
}
228
229
GPUShaderCache::CacheIndexKey GPUShaderCache::GetCacheKey(GPUShaderStage stage, GPUShaderLanguage language,
230
std::string_view shader_code, std::string_view entry_point)
231
{
232
union
233
{
234
struct
235
{
236
u64 hash_low;
237
u64 hash_high;
238
};
239
u8 hash[16];
240
} h;
241
242
CacheIndexKey key;
243
key.shader_type = static_cast<u32>(stage);
244
key.shader_language = static_cast<u32>(language);
245
246
MD5Digest digest;
247
digest.Update(shader_code.data(), static_cast<u32>(shader_code.length()));
248
digest.Final(h.hash);
249
key.source_hash_low = h.hash_low;
250
key.source_hash_high = h.hash_high;
251
key.source_length = static_cast<u32>(shader_code.length());
252
253
digest.Reset();
254
digest.Update(entry_point.data(), static_cast<u32>(entry_point.length()));
255
digest.Final(h.hash);
256
key.entry_point_low = h.hash_low;
257
key.entry_point_high = h.hash_high;
258
259
return key;
260
}
261
262
std::optional<GPUShaderCache::ShaderBinary> GPUShaderCache::Lookup(const CacheIndexKey& key)
263
{
264
std::optional<ShaderBinary> ret;
265
266
const auto iter =
267
std::lower_bound(m_index.begin(), m_index.end(), key,
268
[](const CacheIndexEntry& a, const CacheIndexKey& b) { return (CompareEntries(a, b) < 0); });
269
if (iter != m_index.end() && CompareEntries(*iter, key) == 0)
270
{
271
DynamicHeapArray<u8> compressed_data(iter->compressed_size);
272
273
if (std::fseek(m_blob_file, iter->file_offset, SEEK_SET) != 0 ||
274
std::fread(compressed_data.data(), iter->compressed_size, 1, m_blob_file) != 1) [[unlikely]]
275
{
276
ERROR_LOG("Read {} byte {} shader from file failed", iter->compressed_size,
277
GPUShader::GetStageName(static_cast<GPUShaderStage>(key.shader_type)));
278
}
279
else
280
{
281
Error error;
282
ret = CompressHelpers::DecompressBuffer(CompressHelpers::CompressType::Zstandard,
283
CompressHelpers::OptionalByteBuffer(std::move(compressed_data)),
284
iter->uncompressed_size, &error);
285
if (!ret.has_value()) [[unlikely]]
286
ERROR_LOG("Failed to decompress shader: {}", error.GetDescription());
287
}
288
}
289
290
return ret;
291
}
292
293
bool GPUShaderCache::Insert(const CacheIndexKey& key, const void* data, u32 data_size)
294
{
295
Error error;
296
CompressHelpers::OptionalByteBuffer compress_buffer =
297
CompressHelpers::CompressToBuffer(CompressHelpers::CompressType::Zstandard, data, data_size, -1, &error);
298
if (!compress_buffer.has_value()) [[unlikely]]
299
{
300
ERROR_LOG("Failed to compress shader: {}", error.GetDescription());
301
return false;
302
}
303
304
if (!m_blob_file || std::fseek(m_blob_file, 0, SEEK_END) != 0)
305
return false;
306
307
auto iter =
308
std::lower_bound(m_index.begin(), m_index.end(), key,
309
[](const CacheIndexEntry& a, const CacheIndexKey& b) { return (CompareEntries(a, b) < 0); });
310
iter = m_index.emplace(iter);
311
std::memcpy(&(*iter), &key, KEY_COPY_SIZE);
312
iter->file_offset = static_cast<u32>(std::ftell(m_blob_file));
313
iter->compressed_size = static_cast<u32>(compress_buffer->size());
314
iter->uncompressed_size = data_size;
315
316
if (std::fwrite(compress_buffer->data(), compress_buffer->size(), 1, m_blob_file) != 1 ||
317
std::fflush(m_blob_file) != 0 || std::fwrite(&(*iter), sizeof(CacheIndexEntry), 1, m_index_file) != 1 ||
318
std::fflush(m_index_file) != 0) [[unlikely]]
319
{
320
ERROR_LOG("Failed to write {} byte {} shader blob to file", data_size,
321
GPUShader::GetStageName(static_cast<GPUShaderStage>(key.shader_type)));
322
m_index.erase(iter);
323
return false;
324
}
325
326
DEV_LOG("Cached compressed {} shader: {} -> {} bytes",
327
GPUShader::GetStageName(static_cast<GPUShaderStage>(key.shader_type)), data_size, compress_buffer->size());
328
329
return true;
330
}
331
332