summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/gfx-unit-test/gfx-test-util.cpp15
-rw-r--r--tools/gfx-unit-test/gfx-test-util.h5
-rw-r--r--tools/gfx-unit-test/shader-cache-tests.cpp230
-rw-r--r--tools/gfx/persistent-shader-cache.cpp263
-rw-r--r--tools/gfx/persistent-shader-cache.h65
-rw-r--r--tools/gfx/renderer-shared.cpp17
6 files changed, 431 insertions, 164 deletions
diff --git a/tools/gfx-unit-test/gfx-test-util.cpp b/tools/gfx-unit-test/gfx-test-util.cpp
index fa0745872..116b4222a 100644
--- a/tools/gfx-unit-test/gfx-test-util.cpp
+++ b/tools/gfx-unit-test/gfx-test-util.cpp
@@ -72,6 +72,21 @@ namespace gfx_test
return SLANG_OK;
}
+ Slang::Result loadComputeProgramFromSource(
+ gfx::IDevice* device,
+ Slang::ComPtr<gfx::IShaderProgram>& outShaderProgram,
+ Slang::String source)
+ {
+ Slang::ComPtr<slang::IBlob> diagnosticsBlob;
+
+ gfx::IShaderProgram::CreateDesc2 programDesc = {};
+ programDesc.sourceType = gfx::ShaderModuleSourceType::SlangSource;
+ programDesc.sourceData = (void*)source.getBuffer();
+ programDesc.sourceDataSize = source.getLength();
+
+ return device->createProgram2(programDesc, outShaderProgram.writeRef(), diagnosticsBlob.writeRef());
+ }
+
Slang::Result loadGraphicsProgram(
gfx::IDevice* device,
Slang::ComPtr<gfx::IShaderProgram>& outShaderProgram,
diff --git a/tools/gfx-unit-test/gfx-test-util.h b/tools/gfx-unit-test/gfx-test-util.h
index 5175366c4..d11d5623c 100644
--- a/tools/gfx-unit-test/gfx-test-util.h
+++ b/tools/gfx-unit-test/gfx-test-util.h
@@ -18,6 +18,11 @@ namespace gfx_test
const char* entryPointName,
slang::ProgramLayout*& slangReflection);
+ Slang::Result loadComputeProgramFromSource(
+ gfx::IDevice* device,
+ Slang::ComPtr<gfx::IShaderProgram>& outShaderProgram,
+ Slang::String source);
+
Slang::Result loadGraphicsProgram(
gfx::IDevice* device,
Slang::ComPtr<gfx::IShaderProgram>& outShaderProgram,
diff --git a/tools/gfx-unit-test/shader-cache-tests.cpp b/tools/gfx-unit-test/shader-cache-tests.cpp
index a7306599a..c1d058d70 100644
--- a/tools/gfx-unit-test/shader-cache-tests.cpp
+++ b/tools/gfx-unit-test/shader-cache-tests.cpp
@@ -24,6 +24,7 @@ namespace gfx_test
RenderApiFlag::Enum api;
ComPtr<IDevice> device;
+ ComPtr<IShaderCacheStatistics> shaderCacheStats;
ComPtr<IPipelineState> pipelineState;
ComPtr<IResourceView> bufferView;
@@ -33,12 +34,11 @@ namespace gfx_test
//
// - diskFileSystem - Used to save any files that must exist on disk for subsequent
// save/load function calls (most prominently loadComputeProgram()) to pick up.
- // - cacheFileSystem - Used to hold for the actual cache for all tests. This removes the need to
- // manually clean out old cache files from previous test runs as it is
- // located in-memory. This is the file system passed to device creation
- // as part of the shader cache desc.
+ // This is also used to test the file stream implementation for the cache.
+ // - memoryFileSystem - Used to test the fallback path for the cache in the case physical
+ // file paths cannot be obtained, which prevents usage of file streams.
ComPtr<ISlangMutableFileSystem> diskFileSystem;
- ComPtr<ISlangMutableFileSystem> cacheFileSystem;
+ ComPtr<ISlangMutableFileSystem> memoryFileSystem;
// Simple compute shaders we can pipe to our individual shader files for cache testing
String contentsA = String(
@@ -114,6 +114,7 @@ namespace gfx_test
bufferView = nullptr;
pipelineState = nullptr;
device = nullptr;
+ shaderCacheStats = nullptr;
}
// TODO: This should be removed at some point. Currently exists as a workaround for module loading
@@ -152,11 +153,10 @@ namespace gfx_test
SLANG_IGNORE_TEST
}
- cacheFileSystem = new MemoryFileSystem();
+ memoryFileSystem = new MemoryFileSystem();
diskFileSystem = OSFileSystem::getMutableSingleton();
- diskFileSystem = new RelativeFileSystem(diskFileSystem, "tools/gfx-unit-test");
-
- shaderCache.shaderCacheFileSystem = cacheFileSystem;
+ diskFileSystem->createDirectory("tools/gfx-unit-test/shader-cache-test");
+ diskFileSystem = new RelativeFileSystem(diskFileSystem, "tools/gfx-unit-test/shader-cache-test");
}
void submitGPUWork()
@@ -185,6 +185,44 @@ namespace gfx_test
queue->executeCommandBuffer(commandBuffer);
queue->waitOnHost();
}
+
+ void cleanUpFiles()
+ {
+ freeOldResources();
+
+ List<String> filePaths;
+ diskFileSystem->enumeratePathContents(
+ ".",
+ [](SlangPathType pathType, const char* name, void* userData)
+ {
+ if (pathType == SlangPathType::SLANG_PATH_TYPE_FILE)
+ {
+ List<String>& out = *(List<String>*)userData;
+ out.add(String(name));
+ }
+ },
+ &filePaths);
+
+ for (auto file : filePaths)
+ {
+ diskFileSystem->remove(file.getBuffer());
+ }
+ // Get a mutable singleton so we can delete the folder.
+ auto fileSystem = OSFileSystem::getMutableSingleton();
+ fileSystem->remove("tools/gfx-unit-test/shader-cache-test");
+ }
+
+ void run()
+ {
+ shaderCache.shaderCacheFileSystem = diskFileSystem;
+ runTests();
+ shaderCache.shaderCacheFileSystem = memoryFileSystem;
+ runTests();
+
+ cleanUpFiles();
+ }
+
+ virtual void runTests() = 0;
};
// Due to needing a workaround to prevent loading old, outdated modules, we need to
@@ -205,7 +243,7 @@ namespace gfx_test
ComPtr<IShaderProgram> shaderProgram;
slang::ProgramLayout* slangReflection;
- GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "test-tmp-single-entry", "computeMain", slangReflection));
+ GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "shader-cache-test/test-tmp-single-entry", "computeMain", slangReflection));
ComputePipelineStateDesc pipelineDesc = {};
pipelineDesc.program = shaderProgram.get();
@@ -213,10 +251,8 @@ namespace gfx_test
device->createComputePipelineState(pipelineDesc, pipelineState.writeRef()));
}
- void run()
+ void runTests()
{
- ComPtr<IShaderCacheStatistics> shaderCacheStats;
-
generateNewDevice();
createRequiredResources();
generateNewPipelineState(contentsA);
@@ -275,13 +311,13 @@ namespace gfx_test
switch (shaderIndex)
{
case 0:
- shaderFilename = "test-tmp-multi-entry-A";
+ shaderFilename = "shader-cache-test/test-tmp-multi-entry-A";
break;
case 1:
- shaderFilename = "test-tmp-multi-entry-B";
+ shaderFilename = "shader-cache-test/test-tmp-multi-entry-B";
break;
case 2:
- shaderFilename = "test-tmp-multi-entry-C";
+ shaderFilename = "shader-cache-test/test-tmp-multi-entry-C";
break;
default:
// Should never reach this point since we wrote the test
@@ -305,10 +341,8 @@ namespace gfx_test
submitGPUWork();
}
- void run()
+ void runTests()
{
- ComPtr<IShaderCacheStatistics> shaderCacheStats;
-
generateNewDevice();
createRequiredResources();
modifyShaderA(contentsA);
@@ -385,10 +419,8 @@ namespace gfx_test
device->createComputePipelineState(pipelineDesc, pipelineState.writeRef()));
}
- void run()
+ void runTests()
{
- ComPtr<IShaderCacheStatistics> shaderCacheStats;
-
generateNewDevice();
createRequiredResources();
generateNewPipelineState(0);
@@ -513,7 +545,7 @@ namespace gfx_test
{
ComPtr<IShaderProgram> shaderProgram;
slang::ProgramLayout* slangReflection;
- GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "test-tmp-importing", "computeMain", slangReflection));
+ GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "shader-cache-test/test-tmp-importing", "computeMain", slangReflection));
ComputePipelineStateDesc pipelineDesc = {};
pipelineDesc.program = shaderProgram.get();
@@ -521,10 +553,8 @@ namespace gfx_test
device->createComputePipelineState(pipelineDesc, pipelineState.writeRef()));
}
- void run()
+ void runTests()
{
- ComPtr<IShaderCacheStatistics> shaderCacheStats;
-
generateNewDevice();
createRequiredResources();
initializeFiles();
@@ -652,10 +682,8 @@ namespace gfx_test
device->createComputePipelineState(pipelineDesc, pipelineState.writeRef()));
}
- void run()
+ void runTests()
{
- ComPtr<IShaderCacheStatistics> shaderCacheStats;
-
generateNewDevice();
createRequiredResources();
generateNewPipelineState();
@@ -837,15 +865,13 @@ namespace gfx_test
queue->waitOnHost();
}
- void run()
+ void runTests()
{
generateNewDevice();
createShaderProgram();
createRequiredResources();
submitGPUWork();
- ComPtr<IShaderCacheStatistics> shaderCacheStats;
-
device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 2);
SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0);
@@ -930,15 +956,13 @@ namespace gfx_test
return SLANG_OK;
}
- void run()
+ void runTests()
{
generateNewDevice();
createShaderProgram();
createRequiredResources();
submitGPUWork();
- ComPtr<IShaderCacheStatistics> shaderCacheStats;
-
device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 2);
SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0);
@@ -952,6 +976,10 @@ namespace gfx_test
// This test does not modify shaders as other tests already test this, instead focusing on checking
// that entries are correctly removed as cache limits are reached and that entries are always in
// the right order.
+ //
+ // As opening multiple streams to the same file is dependent on the OS, this test is run on the
+ // in-memory file system. Cache eviction policy with an on-disk file system will need to be inspected
+ // manually.
struct CacheWithMaxEntryLimit : MultipleEntryShaderCache
{
List<String> test0Lines; // C -> B -> A
@@ -959,14 +987,12 @@ namespace gfx_test
List<String> test2Lines; // A -> B
List<String> test3Lines; // A -> C
List<String> test4Lines; // C -> B -> A
- List<String> entryKeys; // C, B, A
-
- ComPtr<IShaderCacheStatistics> shaderCacheStats;
+ List<String> entryKeys; // C, B, A
void getCacheFile(List<String>& lines)
{
ComPtr<ISlangBlob> contentsBlob;
- cacheFileSystem->loadFile(shaderCache.cacheFilename, contentsBlob.writeRef());
+ memoryFileSystem->loadFile(shaderCache.cacheFilename, contentsBlob.writeRef());
List<UnownedStringSlice> temp;
StringUtil::calcLines(UnownedStringSlice((char*)contentsBlob->getBufferPointer()), temp);
for (auto line : temp)
@@ -999,19 +1025,26 @@ namespace gfx_test
shaderCache.entryCountLimit = 3;
generateNewDevice();
createRequiredResources();
+ modifyShaderA(contentsA);
+ modifyShaderB(contentsB);
+ modifyShaderC(contentsC);
generateNewPipelineState(0);
submitGPUWork();
generateNewPipelineState(1);
submitGPUWork();
generateNewPipelineState(2);
submitGPUWork();
- getCacheFile(test0Lines);
device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 3);
SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0);
SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0);
+ // This needs to be called in order to force the cache file to be updated, otherwise we will
+ // be unable to perform the necessary checks.
+ freeOldResources();
+
+ getCacheFile(test0Lines);
SLANG_CHECK(test0Lines.getCount() == 3);
// This segment also doubles as the point where we fetch the keys for all three shaders
@@ -1026,9 +1059,9 @@ namespace gfx_test
}
ComPtr<ISlangBlob> unused;
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
}
// Cache limit 2, access shaders A then B then C
@@ -1046,19 +1079,21 @@ namespace gfx_test
submitGPUWork();
generateNewPipelineState(2);
submitGPUWork();
- getCacheFile(test1Lines);
device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 3);
SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0);
SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0);
+ freeOldResources();
+
+ getCacheFile(test1Lines);
SLANG_CHECK(test1Lines.getCount() == 2);
ComPtr<ISlangBlob> unused;
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_FAILED(cacheFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_FAILED(memoryFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
}
// Cache limit 2, access shaders B and then A
@@ -1070,19 +1105,21 @@ namespace gfx_test
submitGPUWork();
generateNewPipelineState(0);
submitGPUWork();
- getCacheFile(test2Lines);
device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 1);
SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 1);
SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0);
+ freeOldResources();
+
+ getCacheFile(test2Lines);
SLANG_CHECK(test2Lines.getCount() == 2);
ComPtr<ISlangBlob> unused;
- SLANG_CHECK(SLANG_FAILED(cacheFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_FAILED(memoryFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
}
// Cache limit 2, access shaders C and then A
@@ -1094,19 +1131,21 @@ namespace gfx_test
submitGPUWork();
generateNewPipelineState(0);
submitGPUWork();
- getCacheFile(test3Lines);
device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 1);
SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 1);
SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0);
+ freeOldResources();
+
+ getCacheFile(test3Lines);
SLANG_CHECK(test3Lines.getCount() == 2);
ComPtr<ISlangBlob> unused;
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_FAILED(cacheFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_FAILED(memoryFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
}
// Cache limit 3, access shaders A then B then C
@@ -1121,22 +1160,24 @@ namespace gfx_test
submitGPUWork();
generateNewPipelineState(2);
submitGPUWork();
- getCacheFile(test4Lines);
device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 1);
SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 2);
SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0);
+ freeOldResources();
+
+ getCacheFile(test4Lines);
SLANG_CHECK(test4Lines.getCount() == 3);
ComPtr<ISlangBlob> unused;
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
- SLANG_CHECK(SLANG_SUCCEEDED(cacheFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[0].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[1].getBuffer(), unused.writeRef())));
+ SLANG_CHECK(SLANG_SUCCEEDED(memoryFileSystem->loadFile(entryKeys[2].getBuffer(), unused.writeRef())));
}
- void run()
+ void runTests()
{
runTest0();
runTest1();
@@ -1146,6 +1187,65 @@ namespace gfx_test
checkCacheFiles();
}
+
+ void run()
+ {
+ shaderCache.shaderCacheFileSystem = memoryFileSystem;
+ runTests();
+
+ cleanUpFiles();
+ }
+ };
+
+ // This test is specifically for source files which live entirely in memory. The key difference between
+ // these and physical source files is such files have their contents hash added to the file dependencies
+ // list instead of a file path, meaning any given specific set of shader contents will be treated as a
+ // wholly unique module.
+ struct NonPhysicalFileDependencyEntry : BaseShaderCacheTest
+ {
+ void generateNewPipelineState(Slang::String shaderContents)
+ {
+ ComPtr<IShaderProgram> shaderProgram;
+ GFX_CHECK_CALL_ABORT(loadComputeProgramFromSource(device, shaderProgram, shaderContents));
+
+ ComputePipelineStateDesc pipelineDesc = {};
+ pipelineDesc.program = shaderProgram.get();
+ GFX_CHECK_CALL_ABORT(
+ device->createComputePipelineState(pipelineDesc, pipelineState.writeRef()));
+ }
+
+ void runTests()
+ {
+ generateNewDevice();
+ createRequiredResources();
+ generateNewPipelineState(contentsA);
+ submitGPUWork();
+
+ device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
+ SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 1);
+ SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0);
+ SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0);
+
+ generateNewDevice();
+ createRequiredResources();
+ generateNewPipelineState(contentsA);
+ submitGPUWork();
+
+ device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
+ SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 0);
+ SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 1);
+ SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0);
+
+ generateNewDevice();
+ createRequiredResources();
+ generateNewPipelineState(contentsC);
+ submitGPUWork();
+
+ device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef());
+ SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 1);
+ SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0);
+ SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0);
+ }
};
template <typename T>
@@ -1235,4 +1335,14 @@ namespace gfx_test
{
runTestImpl(shaderCacheTestImpl<SplitGraphicsShader>, unitTestContext, Slang::RenderApiFlag::Vulkan);
}
+
+ SLANG_UNIT_TEST(nonPhysicalFileDependenciesCacheEntryD3D12)
+ {
+ runTestImpl(shaderCacheTestImpl<NonPhysicalFileDependencyEntry>, unitTestContext, Slang::RenderApiFlag::D3D12);
+ }
+
+ SLANG_UNIT_TEST(nonPhysicalFileDependenciesCacheEntryVulkan)
+ {
+ runTestImpl(shaderCacheTestImpl<NonPhysicalFileDependencyEntry>, unitTestContext, Slang::RenderApiFlag::Vulkan);
+ }
}
diff --git a/tools/gfx/persistent-shader-cache.cpp b/tools/gfx/persistent-shader-cache.cpp
index c0fd3b44a..54c46f9dc 100644
--- a/tools/gfx/persistent-shader-cache.cpp
+++ b/tools/gfx/persistent-shader-cache.cpp
@@ -6,9 +6,15 @@
#include "../../source/core/slang-string-util.h"
#include "../../source/core/slang-file-system.h"
+#include "../../source/core/slang-char-util.h"
+
+#include <chrono>
+
namespace gfx
{
+using namespace std::chrono;
+
PersistentShaderCache::PersistentShaderCache(const IDevice::ShaderCacheDesc& inDesc)
{
desc = inDesc;
@@ -35,52 +41,97 @@ PersistentShaderCache::PersistentShaderCache(const IDevice::ShaderCacheDesc& inD
loadCacheFromFile();
}
+PersistentShaderCache::~PersistentShaderCache()
+{
+ if (isMemoryFileSystem)
+ {
+ saveCacheToMemory();
+ }
+}
+
// Load a previous cache index saved to disk. If not found, create a new cache index
// and save it to disk as filename.
void PersistentShaderCache::loadCacheFromFile()
{
- ComPtr<ISlangBlob> indexBlob;
- if (SLANG_FAILED(desc.shaderCacheFileSystem->loadFile(desc.cacheFilename, indexBlob.writeRef())))
+ // We will need to combine the filename with the cache path in order to have the correct
+ // file path for initializing the stream. This needs to be done separately because there
+ // is no guarantee that the underlying file system is mutable.
+ String filePath;
+ if (mutableShaderCacheFileSystem)
{
- // Cache index not found, so we'll create and save a new one.
- if (mutableShaderCacheFileSystem)
+ ComPtr<ISlangBlob> fullPath;
+ if (SLANG_FAILED(mutableShaderCacheFileSystem->getPath(PathKind::OperatingSystem, desc.cacheFilename, fullPath.writeRef())))
{
- mutableShaderCacheFileSystem->saveFile(desc.cacheFilename, nullptr, 0);
+ // If we fail to obtain a physical file path, then this must be a MemoryFileSystem. In this case, file streams
+ // will not work as they require the file to be on disk, so we will rely on a fall back implementation.
+ isMemoryFileSystem = true;
+ loadCacheFromMemory();
return;
}
- // Cache index not found and we can't save a new one due to the file system being immutable.
- return;
+ filePath = String((char*)fullPath->getBufferPointer());
+ }
+ else
+ {
+ filePath = Path::combine(String(desc.shaderCachePath), String(desc.cacheFilename));
}
- auto indexString = UnownedStringSlice((char*)indexBlob->getBufferPointer());
-
- List<UnownedStringSlice> lines;
- StringUtil::calcLines(indexString, lines);
- for (auto line : lines)
+ if (SLANG_FAILED(indexStream.init(filePath, FileMode::Open, FileAccess::ReadWrite, FileShare::ReadWrite)))
{
- List<UnownedStringSlice> digests;
- StringUtil::split(line, ' ', digests); // This will return our two hashes as two elements in digests, unless we've reached the end.
- if (digests.getCount() != 2)
- continue;
- auto dependencyDigest = DigestUtil::fromString(digests[0]);
- auto contentsDigest = DigestUtil::fromString(digests[1]);
+ // If we failed to open a stream to the file, then the file does not yet exist on disk.
+ // We will create the index file if our underlying file system is mutable.
+ if (mutableShaderCacheFileSystem)
+ {
+ indexStream.init(filePath, FileMode::Create, FileAccess::ReadWrite, FileShare::ReadWrite);
+ }
+ return;
+ }
+ else
+ {
+ const auto start = indexStream.getPosition();
+ indexStream.seek(SeekOrigin::End, 0);
+ const auto end = indexStream.getPosition();
+ indexStream.seek(SeekOrigin::Start, 0);
+ const Index numEntries = (Index)(end - start) / sizeof(ShaderCacheEntry);
- ShaderCacheEntry entry = { dependencyDigest, contentsDigest };
- auto entryNode = entries.AddLast(entry);
- keyToEntry.Add(dependencyDigest, entryNode);
+ if (desc.entryCountLimit > 0 && numEntries > desc.entryCountLimit)
+ {
+ // If the size limit for the current cache is smaller than the cache that produced the file we're trying to
+ // load, re-create the entire file.
+ //
+ // FileStream does not currently have any methods for truncating an existing file, so in this case, our cache
+ // index would no longer accurately reflect the state of our cache due to the extra now-garbage lines present.
+ // While this has no impact on cache operation, it could be problematic for debugging purposes, etc.
+ indexStream.close();
+ indexStream.init(filePath, FileMode::Create, FileAccess::ReadWrite, FileShare::ReadWrite);
+ return;
+ }
+ else
+ {
+ // The cache index is not guaranteed to be ordered by most recent access, so we need a temporary list to store
+ // all the entries in order to sort them before filling in our linked list.
+ List<ShaderCacheEntry> tempEntries;
+ tempEntries.setCount(numEntries);
+ size_t bytesRead;
+ indexStream.read(tempEntries.getBuffer(), sizeof(ShaderCacheEntry) * numEntries, bytesRead);
- // If there are more entries present in the cache file than the entry count limit allows,
- // ignore the rest. Since we output the lines in the cache file in order of most to least
- // recently used, the cache's behavior will still be correct.
- if (desc.entryCountLimit > 0 && entries.Count() == desc.entryCountLimit)
- break;
+ // We will need to sort tempEntries by last accessed time before we can add entries to our linked list.
+ tempEntries.quickSort(tempEntries.getBuffer(), 0, tempEntries.getCount() - 1, [](ShaderCacheEntry a, ShaderCacheEntry b) { return a.lastAccessedTime > b.lastAccessedTime; });
+ for (auto& entry : tempEntries)
+ {
+ // If we reach this point, then the current cache is at least the same size in entries as the cache
+ // that produced the index we're reading in, so we don't need to check if we're exceeding capacity.
+ auto entryIndexNode = orderedEntries.AddLast(entries.getCount());
+ entries.add(entry);
+ keyToEntry.Add(entry.dependencyBasedDigest, entryIndexNode);
+ }
+ }
}
}
-LinkedNode<ShaderCacheEntry>* PersistentShaderCache::findEntry(const slang::Digest& key, ISlangBlob** outCompiledCode)
+ShaderCacheEntry* PersistentShaderCache::findEntry(const slang::Digest& key, ISlangBlob** outCompiledCode)
{
- LinkedNode<ShaderCacheEntry>* entryNode;
- if (!keyToEntry.TryGetValue(key, entryNode))
+ LinkedNode<Index>* entryIndexNode;
+ if (!keyToEntry.TryGetValue(key, entryIndexNode))
{
// The key was not found in the cache, so we return nullptr.
*outCompiledCode = nullptr;
@@ -90,19 +141,24 @@ LinkedNode<ShaderCacheEntry>* PersistentShaderCache::findEntry(const slang::Dige
// If the key is found, load the stored contents from disk. We then move the corresponding
// entry to the front of the linked list and update the cache file on disk
desc.shaderCacheFileSystem->loadFile(DigestUtil::toString(key).getBuffer(), outCompiledCode);
- if (entries.FirstNode() != entryNode)
+ auto index = entryIndexNode->Value;
+ entries[index].lastAccessedTime = (double)high_resolution_clock::now().time_since_epoch().count();
+ if (orderedEntries.FirstNode() != entryIndexNode)
{
- entries.RemoveFromList(entryNode);
- entries.AddFirst(entryNode);
- if (mutableShaderCacheFileSystem)
+ orderedEntries.RemoveFromList(entryIndexNode);
+ orderedEntries.AddFirst(entryIndexNode);
+ if (mutableShaderCacheFileSystem && !isMemoryFileSystem)
{
- saveCacheToFile();
+ auto offset = index * sizeof(ShaderCacheEntry);
+ indexStream.seek(SeekOrigin::Start, offset + 2 * sizeof(slang::Digest));
+ indexStream.write(&entries[index].lastAccessedTime, sizeof(double));
+ indexStream.flush();
}
}
- return entryNode;
+ return &entries[index];
}
-void PersistentShaderCache::addEntry(const slang::Digest& dependencyDigest, const slang::Digest& astDigest, ISlangBlob* compiledCode)
+void PersistentShaderCache::addEntry(const slang::Digest& dependencyDigest, const slang::Digest& contentsDigest, ISlangBlob* compiledCode)
{
if (!mutableShaderCacheFileSystem)
{
@@ -113,25 +169,45 @@ void PersistentShaderCache::addEntry(const slang::Digest& dependencyDigest, cons
// Check that we do not exceed the cache's size limit by adding another entry. If so,
// remove the least recently used entry first.
//
- // In theory, this could loop infinitely since deleteLRUEntry() immediately returns if
- // mutableShaderCacheFileSystem is not set. However, this situation is functionally impossible
- // because we check immediately before this as well.
- while (desc.entryCountLimit > 0 && entries.Count() >= desc.entryCountLimit)
+ // In theory, the cache could be more than just one entry over the entry count limit.
+ // However, this is impossible in practice because we fully re-create the entry list
+ // and cache index file if the size of the current cache is smaller than the cache
+ // that generated the index file we loaded. In any case, the initial number of entries
+ // in the cache will always be fewer than the size limit and this check will be hit
+ // on the first entry added that exceeds the cache's size.
+ Index index = entries.getCount();
+ if (desc.entryCountLimit > 0 && orderedEntries.Count() >= desc.entryCountLimit)
{
- deleteLRUEntry();
+ index = deleteLRUEntry();
}
- ShaderCacheEntry entry = { dependencyDigest, astDigest };
- auto entryNode = entries.AddFirst(entry);
+ auto lastAccessedTime = (double)high_resolution_clock::now().time_since_epoch().count();
+
+ ShaderCacheEntry entry = { dependencyDigest, contentsDigest, lastAccessedTime };
+ auto entryNode = orderedEntries.AddFirst(index);
+ if (index == entries.getCount())
+ {
+ // No entries were removed, so we can tack this entry on at the end.
+ entries.add(entry);
+ }
+ else
+ {
+ // An entry was deleted, so we overwrite that slot with the new entry.
+ entries[index] = entry;
+ }
keyToEntry.Add(dependencyDigest, entryNode);
mutableShaderCacheFileSystem->saveFileBlob(DigestUtil::toString(dependencyDigest).getBuffer(), compiledCode);
- saveCacheToFile();
+ if (!isMemoryFileSystem)
+ {
+ indexStream.seek(SeekOrigin::End, 0);
+ indexStream.write(&entry, sizeof(ShaderCacheEntry));
+ indexStream.flush();
+ }
}
void PersistentShaderCache::updateEntry(
- LinkedNode<ShaderCacheEntry>* entryNode,
const slang::Digest& dependencyDigest,
const slang::Digest& contentsDigest,
ISlangBlob* updatedCode)
@@ -143,48 +219,99 @@ void PersistentShaderCache::updateEntry(
return;
}
- entryNode->Value.contentsBasedDigest = contentsDigest;
+ // Unlike in addEntry(), we only update the contents digest here because the last accessed time will have already
+ // been updated while finding the entry.
+ auto entryIndexNode = *keyToEntry.TryGetValue(dependencyDigest);
+ auto index = entryIndexNode->Value;
+ entries[index].contentsBasedDigest = contentsDigest;
mutableShaderCacheFileSystem->saveFileBlob(DigestUtil::toString(dependencyDigest).getBuffer(), updatedCode);
- saveCacheToFile();
+ if (!isMemoryFileSystem)
+ {
+ auto offset = index * sizeof(ShaderCacheEntry);
+ indexStream.seek(SeekOrigin::Start, offset + sizeof(slang::Digest));
+ indexStream.write(&contentsDigest, sizeof(slang::Digest));
+ indexStream.flush();
+ }
}
-void PersistentShaderCache::saveCacheToFile()
+Index PersistentShaderCache::deleteLRUEntry()
{
if (!mutableShaderCacheFileSystem)
{
- // Cannot save the index to disk if the underlying file system isn't mutable.
- return;
+ // This is here as a safety precaution but should never be hit as
+ // addEntry() and its memory-based equivalent are the only functions
+ // that should call this.
+ return -1;
}
- StringBuilder indexSb;
- for (auto& entry : entries)
- {
- indexSb << entry.dependencyBasedDigest;
- indexSb << " ";
- indexSb << entry.contentsBasedDigest;
- indexSb << "\n";
- }
+ auto lruEntry = orderedEntries.LastNode();
+ auto index = lruEntry->Value;
+ auto shaderKey = entries[index].dependencyBasedDigest;
- mutableShaderCacheFileSystem->saveFile(desc.cacheFilename, indexSb.getBuffer(), indexSb.getLength());
+ keyToEntry.Remove(shaderKey);
+ mutableShaderCacheFileSystem->remove(DigestUtil::toString(shaderKey).getBuffer());
+
+ orderedEntries.Delete(lruEntry);
+ return index;
}
-void PersistentShaderCache::deleteLRUEntry()
+// An in-memory file system cannot utilize file streaming to update the index file in place.
+// Consequently, the cache index file is updated once on exit and is guaranteed to maintain the
+// correct order of entries from most to least recently used. However, any kind of interruption
+// in program execution that results in the cache destructor not being called will result in an
+// inaccurate cache index.
+//
+// These currently assume that the underlying file system must be a MemoryFileSystem as this is the
+// only in-memory file system that currently exists in Slang, which is guaranteed to be mutable.
+// Mutability checks will need to be added if this changes in the future.
+void PersistentShaderCache::loadCacheFromMemory()
{
- if (!mutableShaderCacheFileSystem)
+ ComPtr<ISlangBlob> indexBlob;
+ if (SLANG_FAILED(mutableShaderCacheFileSystem->loadFile(desc.cacheFilename, indexBlob.writeRef())))
{
- // This is here as a safety precaution but should never be hit as
- // addEntry() is the only function that should call this.
+ mutableShaderCacheFileSystem->saveFile(desc.cacheFilename, nullptr, 0);
return;
}
- auto lruEntry = entries.LastNode();
- auto shaderKey = lruEntry->Value.dependencyBasedDigest;
+ auto indexString = UnownedStringSlice((char*)indexBlob->getBufferPointer());
- keyToEntry.Remove(shaderKey);
- mutableShaderCacheFileSystem->remove(DigestUtil::toString(shaderKey).getBuffer());
+ List<UnownedStringSlice> lines;
+ StringUtil::calcLines(indexString, lines);
+ for (auto line : lines)
+ {
+ List<UnownedStringSlice> entryFields;
+ StringUtil::split(line, ' ', entryFields);
+ if (entryFields.getCount() != 2)
+ continue;
+
+ ShaderCacheEntry entry;
+ entry.dependencyBasedDigest = DigestUtil::fromString(entryFields[0]);
+ entry.contentsBasedDigest = DigestUtil::fromString(entryFields[1]);
+ entry.lastAccessedTime = 0;
+
+ auto entryNode = orderedEntries.AddLast(entries.getCount());
+ entries.add(entry);
+ keyToEntry.Add(entry.dependencyBasedDigest, entryNode);
+
+ if (desc.entryCountLimit > 0 && orderedEntries.Count() == desc.entryCountLimit)
+ break;
+ }
+}
- entries.Delete(lruEntry);
+void PersistentShaderCache::saveCacheToMemory()
+{
+ StringBuilder indexSb;
+ for (auto& entryIndex : orderedEntries)
+ {
+ auto entry = entries[entryIndex];
+ indexSb << entry.dependencyBasedDigest;
+ indexSb << " ";
+ indexSb << entry.contentsBasedDigest;
+ indexSb << "\n";
+ }
+
+ mutableShaderCacheFileSystem->saveFile(desc.cacheFilename, indexSb.getBuffer(), indexSb.getLength());
}
}
diff --git a/tools/gfx/persistent-shader-cache.h b/tools/gfx/persistent-shader-cache.h
index 8d4ded4b9..8e05eddf6 100644
--- a/tools/gfx/persistent-shader-cache.h
+++ b/tools/gfx/persistent-shader-cache.h
@@ -7,27 +7,42 @@
#include "../../source/core/slang-string.h"
#include "../../source/core/slang-dictionary.h"
#include "../../source/core/slang-linked-list.h"
+#include "../../source/core/slang-stream.h"
namespace gfx
{
- using namespace Slang;
+using namespace Slang;
struct ShaderCacheEntry
{
slang::Digest dependencyBasedDigest;
slang::Digest contentsBasedDigest;
+ double lastAccessedTime;
+
+ bool operator==(const ShaderCacheEntry& rhs)
+ {
+ return dependencyBasedDigest == rhs.dependencyBasedDigest
+ && contentsBasedDigest == rhs.contentsBasedDigest
+ && lastAccessedTime == rhs.lastAccessedTime;
+ }
+
+ uint32_t getHashCode()
+ {
+ return dependencyBasedDigest.getHashCode();
+ }
};
class PersistentShaderCache : public RefObject
{
public:
PersistentShaderCache(const IDevice::ShaderCacheDesc& inDesc);
+ ~PersistentShaderCache();
// Fetch the cache entry corresponding to the provided key. If found, move the entry to
// the front of entries and return the entry and the corresponding compiled code in
// outCompiledCode. Else, return nullptr.
- LinkedNode<ShaderCacheEntry>* findEntry(const slang::Digest& key, ISlangBlob** outCompiledCode);
+ ShaderCacheEntry* findEntry(const slang::Digest& key, ISlangBlob** outCompiledCode);
// Add an entry to the cache with the provided key and contents hashes. If
// adding an entry causes the cache to exceed size limitations, this will also
@@ -36,38 +51,46 @@ public:
// Update the contents hash for the specified entry in the cache and update the
// corresponding file on disk.
- void updateEntry(
- LinkedNode<ShaderCacheEntry>* entryNode,
- const slang::Digest& dependencyDigest,
- const slang::Digest& contentsDigest,
- ISlangBlob* updatedCode);
+ void updateEntry(const slang::Digest& dependencyDigest, const slang::Digest& contentsDigest, ISlangBlob* updatedCode);
private:
// Load a previous cache index saved to disk. If not found, create a new cache index
// and save it to disk as filename.
void loadCacheFromFile();
- // Update the cache index on disk. This should be called any time an entry changes.
- void saveCacheToFile();
-
// Delete the last entry (the least recently used) from entries, remove its key/value pair
- // from keyToEntry, and remove the corresponding file on disk. This should only be called
- // by addEntry() when the cache reaches maximum capacity.
- void deleteLRUEntry();
+ // from keyToEntry, and remove the corresponding file on disk. Returns the index in 'entries'
+ // of the removed entry so addEntry() can overwrite the corresponding entry in 'entries'
+ // with the new entry. This should only be called by addEntry() when the cache reaches maximum capacity.
+ Index deleteLRUEntry();
+
+ // Without access to a physical file path, in-memory file systems cannot leverage file streams and
+ // need to fall back on a different implementation for loading and saving the cache to memory.
+ void loadCacheFromMemory();
+ void saveCacheToMemory();
// The shader cache's description.
IDevice::ShaderCacheDesc desc;
- // Dictionary mapping each shader's key to its corresponding node (entry) in the list
- // of entries.
- Dictionary<slang::Digest, LinkedNode<ShaderCacheEntry>*> keyToEntry;
-
- // Linked list containing the entries stored in the shader cache in order
- // of most to least recently used.
- LinkedList<ShaderCacheEntry> entries;
-
// The underlying file system used for the shader cache.
ComPtr<ISlangMutableFileSystem> mutableShaderCacheFileSystem = nullptr;
+ bool isMemoryFileSystem = false;
+
+ // A file stream to the index file opened during cache load. This will only
+ // exist for a cache that exists on-disk.
+ FileStream indexStream;
+
+ // Dictionary mapping each shader's key to its corresponding node (entry) in the
+ // linked list 'orderedEntries'.
+ Dictionary<slang::Digest, LinkedNode<Index>*> keyToEntry;
+
+ // Linked list containing the corresponding indices in 'entries' for entries in the
+ // shader cache ordered from most to least recently used.
+ LinkedList<Index> orderedEntries;
+
+ // List of entries in the shader cache. This list is not guaranteed to be in order of recency
+ // as the main and fall back implementations handle outputting to the file differently.
+ List<ShaderCacheEntry> entries;
};
}
diff --git a/tools/gfx/renderer-shared.cpp b/tools/gfx/renderer-shared.cpp
index 65c625d5d..3dcdb324c 100644
--- a/tools/gfx/renderer-shared.cpp
+++ b/tools/gfx/renderer-shared.cpp
@@ -331,19 +331,6 @@ void PipelineStateBase::initializeBase(const PipelineStateDesc& inDesc)
}
}
-void updateCacheEntry(ISlangMutableFileSystem* fileSystem, slang::IBlob* compiledCode, String shaderFilename, slang::Digest ASTHash)
-{
- auto hashSize = sizeof(slang::Digest);
-
- auto bufferSize = hashSize + compiledCode->getBufferSize();
- List<uint8_t> contents;
- contents.setCount(bufferSize);
- uint8_t* buffer = contents.begin();
- memcpy(buffer, &ASTHash, hashSize);
- memcpy(buffer + hashSize, (void*)compiledCode->getBufferPointer(), compiledCode->getBufferSize());
- fileSystem->saveFile(shaderFilename.getBuffer(), buffer, bufferSize);
-}
-
Result RendererBase::getEntryPointCodeFromShaderCache(
slang::IComponentType* program,
SlangInt entryPointIndex,
@@ -374,7 +361,7 @@ Result RendererBase::getEntryPointCodeFromShaderCache(
// Query the shader cache index for an entry with shaderKey as its key.
auto entry = persistentShaderCache->findEntry(shaderKey, codeBlob.writeRef());
- if (entry && contentsHash == entry->Value.contentsBasedDigest)
+ if (entry && contentsHash == entry->contentsBasedDigest)
{
// We found the entry in the cache, and the entry's contents are up-to-date. Nothing else needs to be done.
shaderCacheHitCount++;
@@ -394,7 +381,7 @@ Result RendererBase::getEntryPointCodeFromShaderCache(
}
else
{
- persistentShaderCache->updateEntry(entry, shaderKey, contentsHash, codeBlob);
+ persistentShaderCache->updateEntry(shaderKey, contentsHash, codeBlob);
shaderCacheEntryDirtyCount++;
}
}