diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/gfx-unit-test/gfx-test-util.cpp | 15 | ||||
| -rw-r--r-- | tools/gfx-unit-test/gfx-test-util.h | 5 | ||||
| -rw-r--r-- | tools/gfx-unit-test/shader-cache-tests.cpp | 230 | ||||
| -rw-r--r-- | tools/gfx/persistent-shader-cache.cpp | 263 | ||||
| -rw-r--r-- | tools/gfx/persistent-shader-cache.h | 65 | ||||
| -rw-r--r-- | tools/gfx/renderer-shared.cpp | 17 |
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++; } } |
