diff options
| author | skallweitNV <64953474+skallweitNV@users.noreply.github.com> | 2022-12-12 19:25:48 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-12-12 10:25:48 -0800 |
| commit | c2dc1a86ed2f5e160749fe9f99b70db6c3e4d7a6 (patch) | |
| tree | ea65b9635d892917a2420688a27c38537c4758be /tools | |
| parent | 8d359fc6133fa49d2d3b7f8bb4b37916e719c344 (diff) | |
Refactor shader cache (#2558)
* Fix a bug in Path::find
* Fix code formatting
* Fix LockFile and add LockFileGuard
* Add PersistentCache and unit test
* Replace file path dependency list with source file dependency list
* Add note on ordering in Module/FileDependencyList
* Remove old shader cache code
* Refactor shader cache implementation
* Temporarily skip unit tests reading/writing files
* Fix warning
* Reenable lock file test
* Rename shader cache tests and disable crashing test
* Testing
* Stop using Path::getCanonical
* Fix persistent cache lock and test
* Fix threading issues
* Move adding file dependency hashes to getEntryPointHash()
* Fix handling of #include files
* Allow specifying additional search paths for gfx testing device
* Work on shader cache tests
* Update project files
* Revive shader cache graphics tests
* Split graphics pipeline test
* Fix compilation
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/gfx-unit-test/gfx-test-util.cpp | 12 | ||||
| -rw-r--r-- | tools/gfx-unit-test/gfx-test-util.h | 4 | ||||
| -rw-r--r-- | tools/gfx-unit-test/shader-cache-graphics-fragment.slang (renamed from tools/gfx-unit-test/split-graphics-fragment.slang) | 2 | ||||
| -rw-r--r-- | tools/gfx-unit-test/shader-cache-graphics-vertex.slang (renamed from tools/gfx-unit-test/split-graphics-vertex.slang) | 2 | ||||
| -rw-r--r-- | tools/gfx-unit-test/shader-cache-multiple-entry-points.slang (renamed from tools/gfx-unit-test/multiple-entry-point-shader-cache-shader.slang) | 13 | ||||
| -rw-r--r-- | tools/gfx-unit-test/shader-cache-specialization.slang | 68 | ||||
| -rw-r--r-- | tools/gfx-unit-test/shader-cache-tests.cpp | 1449 | ||||
| -rw-r--r-- | tools/gfx/gfx.slang | 24 | ||||
| -rw-r--r-- | tools/gfx/persistent-shader-cache.cpp | 316 | ||||
| -rw-r--r-- | tools/gfx/persistent-shader-cache.h | 99 | ||||
| -rw-r--r-- | tools/gfx/renderer-shared.cpp | 94 | ||||
| -rw-r--r-- | tools/gfx/renderer-shared.h | 27 | ||||
| -rw-r--r-- | tools/slang-unit-test/unit-test-lock-file.cpp | 4 | ||||
| -rw-r--r-- | tools/slang-unit-test/unit-test-persistent-cache.cpp | 629 |
14 files changed, 1345 insertions, 1398 deletions
diff --git a/tools/gfx-unit-test/gfx-test-util.cpp b/tools/gfx-unit-test/gfx-test-util.cpp index 116b4222a..5cbb30e71 100644 --- a/tools/gfx-unit-test/gfx-test-util.cpp +++ b/tools/gfx-unit-test/gfx-test-util.cpp @@ -194,6 +194,7 @@ namespace gfx_test Slang::ComPtr<gfx::IDevice> createTestingDevice( UnitTestContext* context, Slang::RenderApiFlag::Enum api, + Slang::List<const char*> additionalSearchPaths, gfx::IDevice::ShaderCacheDesc shaderCache) { Slang::ComPtr<gfx::IDevice> device; @@ -222,10 +223,13 @@ namespace gfx_test SLANG_IGNORE_TEST } deviceDesc.slang.slangGlobalSession = context->slangGlobalSession; - const char* searchPaths[] = { "", "../../tools/gfx-unit-test", "tools/gfx-unit-test" }; - deviceDesc.slang.searchPathCount = (SlangInt)SLANG_COUNT_OF(searchPaths); - deviceDesc.slang.searchPaths = searchPaths; - + Slang::List<const char*> searchPaths; + searchPaths.add(""); + searchPaths.add("../../tools/gfx-unit-test"); + searchPaths.add("tools/gfx-unit-test"); + searchPaths.addRange(additionalSearchPaths); + deviceDesc.slang.searchPaths = searchPaths.getBuffer(); + deviceDesc.slang.searchPathCount = (gfx::GfxCount)searchPaths.getCount(); deviceDesc.shaderCache = shaderCache; gfx::D3D12DeviceExtendedDesc extDesc = {}; diff --git a/tools/gfx-unit-test/gfx-test-util.h b/tools/gfx-unit-test/gfx-test-util.h index d11d5623c..f829d6d12 100644 --- a/tools/gfx-unit-test/gfx-test-util.h +++ b/tools/gfx-unit-test/gfx-test-util.h @@ -77,6 +77,7 @@ namespace gfx_test Slang::ComPtr<gfx::IDevice> createTestingDevice( UnitTestContext* context, Slang::RenderApiFlag::Enum api, + Slang::List<const char*> additionalSearchPaths = {}, gfx::IDevice::ShaderCacheDesc shaderCache = {}); void initializeRenderDoc(); @@ -88,13 +89,14 @@ namespace gfx_test const ImplFunc& f, UnitTestContext* context, Slang::RenderApiFlag::Enum api, + Slang::List<const char*> searchPaths = {}, gfx::IDevice::ShaderCacheDesc shaderCache = {}) { if ((api & context->enabledApis) == 0) { SLANG_IGNORE_TEST } - auto device = createTestingDevice(context, api, shaderCache); + auto device = createTestingDevice(context, api, searchPaths, shaderCache); if (!device) { SLANG_IGNORE_TEST diff --git a/tools/gfx-unit-test/split-graphics-fragment.slang b/tools/gfx-unit-test/shader-cache-graphics-fragment.slang index db515a957..392aa15ba 100644 --- a/tools/gfx-unit-test/split-graphics-fragment.slang +++ b/tools/gfx-unit-test/shader-cache-graphics-fragment.slang @@ -1,4 +1,4 @@ -// split-graphics-fragment.slang +// shader-cache-graphics-fragment.slang // Output of the vertex shader, and input to the fragment shader. struct CoarseVertex diff --git a/tools/gfx-unit-test/split-graphics-vertex.slang b/tools/gfx-unit-test/shader-cache-graphics-vertex.slang index 615686a90..a86f8bcf1 100644 --- a/tools/gfx-unit-test/split-graphics-vertex.slang +++ b/tools/gfx-unit-test/shader-cache-graphics-vertex.slang @@ -1,4 +1,4 @@ -// split-graphics-vertex.slang +// shader-cache-graphics-vertex.slang // Per-vertex attributes to be assembled from bound vertex buffers. struct AssembledVertex diff --git a/tools/gfx-unit-test/multiple-entry-point-shader-cache-shader.slang b/tools/gfx-unit-test/shader-cache-multiple-entry-points.slang index 9287b62ea..a0015b83c 100644 --- a/tools/gfx-unit-test/multiple-entry-point-shader-cache-shader.slang +++ b/tools/gfx-unit-test/shader-cache-multiple-entry-points.slang @@ -1,9 +1,10 @@ -uniform RWStructuredBuffer<float> buffer; - +// shader-cache-multiple-entry-points.slang + [shader("compute")] [numthreads(4, 1, 1)] void computeA( -uint3 sv_dispatchThreadID : SV_DispatchThreadID) + uint3 sv_dispatchThreadID: SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer) { var input = buffer[sv_dispatchThreadID.x]; buffer[sv_dispatchThreadID.x] = input + 1.0f; @@ -12,7 +13,8 @@ uint3 sv_dispatchThreadID : SV_DispatchThreadID) [shader("compute")] [numthreads(4, 1, 1)] void computeB( -uint3 sv_dispatchThreadID : SV_DispatchThreadID) + uint3 sv_dispatchThreadID: SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer) { var input = buffer[sv_dispatchThreadID.x]; buffer[sv_dispatchThreadID.x] = input + 2.0f; @@ -21,7 +23,8 @@ uint3 sv_dispatchThreadID : SV_DispatchThreadID) [shader("compute")] [numthreads(4, 1, 1)] void computeC( -uint3 sv_dispatchThreadID : SV_DispatchThreadID) + uint3 sv_dispatchThreadID: SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer) { var input = buffer[sv_dispatchThreadID.x]; buffer[sv_dispatchThreadID.x] = input + 3.0f; diff --git a/tools/gfx-unit-test/shader-cache-specialization.slang b/tools/gfx-unit-test/shader-cache-specialization.slang new file mode 100644 index 000000000..63994aee8 --- /dev/null +++ b/tools/gfx-unit-test/shader-cache-specialization.slang @@ -0,0 +1,68 @@ +// shader-cache-specialization.slang + +// This is a copy of `shader-object.slang` in `shader-object` example +// for use by compute-smoke gfx unit test. + +// This file implements a simple compute shader that transforms +// input floating point numbers stored in a `RWStructuredBuffer`. +// Specifically, for each number x from input buffer, compute +// f(x) and store the result back in the same buffer. + +// The compute shader supports multiple transformation functions, +// such add(x, c) which returns x+c, or mul(x, c) which returns x*c. +// This functions are implemented as types that conforms to the +// `ITransformer` interface. + +// The main entry point function takes a parameter of `ITransformer` +// type, and applies the transformation to numbers in the input +// buffer. By defining the shader parameter using interfaces, +// we enable the flexiblity to generate either specialized compute +// kernels that performs specific transformation or a general +// kernel that can perform any transformations encoded by the +// parameter at run-time, without changing any shader code or +// host-application logic for setting and preparing shader parameters. + +// Defines the transformer interface, which implements a single +// `transform` operation. +interface ITransformer +{ + float transform(float x); +} + +// Represents a transform function f(x) = x + c. +struct AddTransformer : ITransformer +{ + float c; + float transform(float x) { return x + c + 10.0f; } +}; + +// Represents a transform function f(x) = x * c. +struct MulTransformer : ITransformer +{ + float c; + float transform(float x) { return x * c; } +}; + +// Represents a composite function f(x) = f0(f1(x)); +struct CompositeTransformer : ITransformer +{ + ITransformer func0; + ITransformer func1; + float transform(float x) + { + return func0.transform(func1.transform(x)); + } +}; + +// Main entry-point. Applies the transformation encoded by `transformer` +// to all elements in `buffer`. +[shader("compute")] +[numthreads(4,1,1)] +void computeMain( + uint3 sv_dispatchThreadID : SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer, + uniform ITransformer transformer) +{ + var input = buffer[sv_dispatchThreadID.x]; + buffer[sv_dispatchThreadID.x] = transformer.transform(input); +} diff --git a/tools/gfx-unit-test/shader-cache-tests.cpp b/tools/gfx-unit-test/shader-cache-tests.cpp index 486b59cda..4cccc726f 100644 --- a/tools/gfx-unit-test/shader-cache-tests.cpp +++ b/tools/gfx-unit-test/shader-cache-tests.cpp @@ -5,8 +5,7 @@ #include "tools/gfx-util/shader-cursor.h" #include "source/core/slang-basic.h" #include "source/core/slang-string-util.h" - -#include "source/core/slang-memory-file-system.h" +#include "source/core/slang-io.h" #include "source/core/slang-file-system.h" #include "gfx-test-texture-util.h" @@ -17,69 +16,136 @@ using namespace Slang; namespace gfx_test { - struct BaseShaderCacheTest + struct ShaderCacheTest { UnitTestContext* context; - RenderApiFlag::Enum api; + Slang::RenderApiFlag::Enum api; + + String testDirectory; + String cacheDirectory; + + ComPtr<ISlangMutableFileSystem> diskFileSystem; + + IDevice::ShaderCacheDesc shaderCacheDesc = {}; ComPtr<IDevice> device; - ComPtr<IShaderCacheStatistics> shaderCacheStats; + ComPtr<IShaderCache> shaderCache; ComPtr<IPipelineState> pipelineState; ComPtr<IResourceView> bufferView; - IDevice::ShaderCacheDesc shaderCache = {}; - - // Two file systems in order to get around problems posed by the testing framework. - // - // - diskFileSystem - Used to save any files that must exist on disk for subsequent - // save/load function calls (most prominently loadComputeProgram()) to pick up. - // 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> memoryFileSystem; - - // Simple compute shaders we can pipe to our individual shader files for cache testing - String contentsA = String( + String computeShaderA = String( R"( - uniform RWStructuredBuffer<float> buffer; - [shader("compute")] [numthreads(4, 1, 1)] - void computeMain( - uint3 sv_dispatchThreadID : SV_DispatchThreadID) + void main( + uint3 sv_dispatchThreadID : SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer) { var input = buffer[sv_dispatchThreadID.x]; buffer[sv_dispatchThreadID.x] = input + 1.0f; - })"); + } + )"); - String contentsB = String( + String computeShaderB = String( R"( - uniform RWStructuredBuffer<float> buffer; - [shader("compute")] [numthreads(4, 1, 1)] - void computeMain( - uint3 sv_dispatchThreadID : SV_DispatchThreadID) + void main( + uint3 sv_dispatchThreadID : SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer) { var input = buffer[sv_dispatchThreadID.x]; buffer[sv_dispatchThreadID.x] = input + 2.0f; - })"); + } + )"); - String contentsC = String( + String computeShaderC = String( R"( - uniform RWStructuredBuffer<float> buffer; - [shader("compute")] [numthreads(4, 1, 1)] - void computeMain( - uint3 sv_dispatchThreadID : SV_DispatchThreadID) + void main( + uint3 sv_dispatchThreadID : SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer) { var input = buffer[sv_dispatchThreadID.x]; buffer[sv_dispatchThreadID.x] = input + 3.0f; - })"); + } + )"); + + + void removeDirectory(const String& directory) + { + auto osFileSystem = OSFileSystem::getMutableSingleton(); + + struct Context + { + ISlangMutableFileSystem *fileSystem; + const String& directory; + } context { osFileSystem, directory }; + + osFileSystem->enumeratePathContents( + directory.getBuffer(), + [](SlangPathType pathType, const char* fileName, void* userData) + { + struct Context* context = static_cast<Context *>(userData); + if (pathType == SlangPathType::SLANG_PATH_TYPE_FILE) + { + String path = Path::simplify(context->directory + "/" + fileName); + context->fileSystem->remove(path.getBuffer()); + } + }, + &context); + + osFileSystem->remove(directory.getBuffer()); + } + + void writeShader(const String& source, const String& fileName) + { + diskFileSystem->saveFile(fileName.getBuffer(), source.getBuffer(), source.getLength()); + } + + void init(UnitTestContext* context, Slang::RenderApiFlag::Enum api) + { + this->context = context; + this->api = api; + + testDirectory = Path::simplify(Path::getParentDirectory(Path::getExecutablePath()) + "/shader-cache-test"); + cacheDirectory = Path::simplify(testDirectory + "/cache"); + + // Cleanup if there are stale files from a previously aborted test. + removeDirectory(cacheDirectory); + removeDirectory(testDirectory); + + Path::createDirectory(testDirectory); + diskFileSystem = new RelativeFileSystem(OSFileSystem::getMutableSingleton(), testDirectory); + shaderCacheDesc.shaderCachePath = cacheDirectory.getBuffer(); + } + + void cleanup() + { + removeDirectory(cacheDirectory); + removeDirectory(testDirectory); + } + + template<typename Func> + void runStep(Func func) + { + List<const char*> additionalSearchPaths; + additionalSearchPaths.add(testDirectory.getBuffer()); + + runTestImpl( + [this, func] (IDevice* device, UnitTestContext* ctx) + { + this->device = device; + device->queryInterface(SLANG_UUID_IShaderCache, (void**)this->shaderCache.writeRef()); + func(); + this->device = nullptr; + this->shaderCache = nullptr; + }, + context, api, additionalSearchPaths, shaderCacheDesc); + } - void createRequiredResources() + void createComputeResources() { const int numberCount = 4; float initialData[] = { 0.0f, 1.0f, 2.0f, 3.0f }; @@ -108,57 +174,25 @@ namespace gfx_test device->createBufferView(numbersBuffer, nullptr, viewDesc, bufferView.writeRef())); } - void freeOldResources() + void freeComputeResources() { bufferView = nullptr; pipelineState = nullptr; - device = nullptr; - shaderCacheStats = nullptr; } - // TODO: This should be removed at some point. Currently exists as a workaround for module loading - // seemingly not accounting for updated shader code under the same module name with the same entry point. - void generateNewDevice() + void createComputePipeline(const char* moduleName, const char* entryPointName) { - freeOldResources(); - device = createTestingDevice(context, api, shaderCache); - } - - void init(ComPtr<IDevice> device, UnitTestContext* context) - { - this->device = device; - this->context = context; - switch (device->getDeviceInfo().deviceType) - { - case DeviceType::DirectX11: - api = RenderApiFlag::D3D11; - break; - case DeviceType::DirectX12: - api = RenderApiFlag::D3D12; - break; - case DeviceType::Vulkan: - api = RenderApiFlag::Vulkan; - break; - case DeviceType::CPU: - api = RenderApiFlag::CPU; - break; - case DeviceType::CUDA: - api = RenderApiFlag::CUDA; - break; - case DeviceType::OpenGl: - api = RenderApiFlag::OpenGl; - break; - default: - SLANG_IGNORE_TEST - } + ComPtr<IShaderProgram> shaderProgram; + slang::ProgramLayout* slangReflection; + GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, moduleName, entryPointName, slangReflection)); - memoryFileSystem = new MemoryFileSystem(); - diskFileSystem = OSFileSystem::getMutableSingleton(); - diskFileSystem->createDirectory("tools/gfx-unit-test/shader-cache-test"); - diskFileSystem = new RelativeFileSystem(diskFileSystem, "tools/gfx-unit-test/shader-cache-test"); + ComputePipelineStateDesc pipelineDesc = {}; + pipelineDesc.program = shaderProgram.get(); + GFX_CHECK_CALL_ABORT( + device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); } - void submitGPUWork() + void dispatchComputePipeline() { ComPtr<ITransientResourceHeap> transientHeap; ITransientResourceHeap::Desc transientHeapDesc = {}; @@ -174,437 +208,284 @@ namespace gfx_test auto rootObject = encoder->bindPipeline(pipelineState); - ShaderCursor rootCursor(rootObject); + ShaderCursor entryPointCursor(rootObject->getEntryPoint(0)); + entryPointCursor.getPath("buffer").setResource(bufferView); + + // ShaderCursor rootCursor(rootObject); // Bind buffer view to the entry point. - rootCursor.getPath("buffer").setResource(bufferView); + // rootCursor.getPath("buffer").setResource(bufferView); - encoder->dispatchCompute(1, 1, 1); + encoder->dispatchCompute(4, 1, 1); encoder->endEncoding(); commandBuffer->close(); queue->executeCommandBuffer(commandBuffer); queue->waitOnHost(); - } + } - void cleanUpFiles() + void runComputePipeline(const char* moduleName, const char* entryPointName) { - 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); + createComputeResources(); + createComputePipeline(moduleName, entryPointName); + dispatchComputePipeline(); + freeComputeResources(); + } - 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"); + ShaderCacheStats getStats() + { + SLANG_ASSERT(shaderCache); + ShaderCacheStats stats; + shaderCache->getShaderCacheStats(&stats); + return stats; } - void run() + void run(UnitTestContext* context, Slang::RenderApiFlag::Enum api) { - shaderCache.shaderCacheFileSystem = diskFileSystem; - runTests(); - shaderCache.shaderCacheFileSystem = memoryFileSystem; + init(context, api); runTests(); - - cleanUpFiles(); + cleanup(); } virtual void runTests() = 0; }; - // Due to needing a workaround to prevent loading old, outdated modules, we need to - // recreate the device between each segment of the test for all tests. However, we need to maintain the - // same cache filesystem for the same duration, so the device is immediately recreated - // to ensure we can pass the filesystem all the way through. - // - // General TODO: Remove the repeated generateNewDevice() and createRequiredResources() calls once - // a solution exists that allows source code changes under the same module name to be picked - // up on load. - - // One shader file on disk, all modifications are done to the same file - struct SingleEntryShaderCache : BaseShaderCacheTest + // Basic shader cache test using 3 different shader files stored on disk. + struct ShaderCacheTestBasic : ShaderCacheTest { - void generateNewPipelineState(Slang::String shaderContents) + void runTests() { - diskFileSystem->saveFile("test-tmp-single-entry.slang", shaderContents.getBuffer(), shaderContents.getLength()); + // Write shader source files. + writeShader(computeShaderA, "shader-cache-tmp-a.slang"); + writeShader(computeShaderB, "shader-cache-tmp-b.slang"); + writeShader(computeShaderC, "shader-cache-tmp-c.slang"); - ComPtr<IShaderProgram> shaderProgram; - slang::ProgramLayout* slangReflection; - GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "shader-cache-test/test-tmp-single-entry", "computeMain", slangReflection)); + // Cache is cold and we expect 3 misses. + runStep( + [this]() + { + runComputePipeline("shader-cache-tmp-a", "main"); + runComputePipeline("shader-cache-tmp-b", "main"); + runComputePipeline("shader-cache-tmp-c", "main"); - ComputePipelineStateDesc pipelineDesc = {}; - pipelineDesc.program = shaderProgram.get(); - GFX_CHECK_CALL_ABORT( - device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); - } + SLANG_CHECK(getStats().missCount == 3); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 3); + } + ); - 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() == 0); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 1); - } - }; + // Cache is hot and we expect 3 hits. + runStep( + [this]() + { + runComputePipeline("shader-cache-tmp-a", "main"); + runComputePipeline("shader-cache-tmp-b", "main"); + runComputePipeline("shader-cache-tmp-c", "main"); - // Several shader files on disk, modifications may be done to any file - struct MultipleEntryShaderCache : BaseShaderCacheTest - { - void modifyShaderA(String shaderContents) - { - diskFileSystem->saveFile("test-tmp-multi-entry-A.slang", shaderContents.getBuffer(), shaderContents.getLength()); - } + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 3); + SLANG_CHECK(getStats().entryCount == 3); + } + ); - void modifyShaderB(String shaderContents) - { - diskFileSystem->saveFile("test-tmp-multi-entry-B.slang", shaderContents.getBuffer(), shaderContents.getLength()); - } + // Write shader source files, all rotated by one. + writeShader(computeShaderA, "shader-cache-tmp-b.slang"); + writeShader(computeShaderB, "shader-cache-tmp-c.slang"); + writeShader(computeShaderC, "shader-cache-tmp-a.slang"); - void modifyShaderC(String shaderContents) - { - diskFileSystem->saveFile("test-tmp-multi-entry-C.slang", shaderContents.getBuffer(), shaderContents.getLength()); - } + // Cache is cold again and we expect 3 misses. + runStep( + [this]() + { + runComputePipeline("shader-cache-tmp-a", "main"); + runComputePipeline("shader-cache-tmp-b", "main"); + runComputePipeline("shader-cache-tmp-c", "main"); - void generateNewPipelineState(GfxIndex shaderIndex) - { - ComPtr<IShaderProgram> shaderProgram; - slang::ProgramLayout* slangReflection; - const char* shaderFilename; - switch (shaderIndex) - { - case 0: - shaderFilename = "shader-cache-test/test-tmp-multi-entry-A"; - break; - case 1: - shaderFilename = "shader-cache-test/test-tmp-multi-entry-B"; - break; - case 2: - shaderFilename = "shader-cache-test/test-tmp-multi-entry-C"; - break; - default: - // Should never reach this point since we wrote the test - SLANG_IGNORE_TEST; - } - GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, shaderFilename, "computeMain", slangReflection)); - - ComputePipelineStateDesc pipelineDesc = {}; - pipelineDesc.program = shaderProgram.get(); - GFX_CHECK_CALL_ABORT( - device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); - } + SLANG_CHECK(getStats().missCount == 3); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 6); + } + ); - void checkAllCacheEntries() - { - generateNewPipelineState(0); - submitGPUWork(); - generateNewPipelineState(1); - submitGPUWork(); - generateNewPipelineState(2); - submitGPUWork(); - } + // Cache is hot again and we expect 3 hits. + runStep( + [this]() + { + runComputePipeline("shader-cache-tmp-a", "main"); + runComputePipeline("shader-cache-tmp-b", "main"); + runComputePipeline("shader-cache-tmp-c", "main"); - void runTests() - { - generateNewDevice(); - createRequiredResources(); - modifyShaderA(contentsA); - modifyShaderB(contentsB); - modifyShaderC(contentsC); - checkAllCacheEntries(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 3); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0); - - generateNewDevice(); - createRequiredResources(); - checkAllCacheEntries(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 3); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0); - - generateNewDevice(); - createRequiredResources(); - modifyShaderA(contentsB); - checkAllCacheEntries(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 2); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 1); - - generateNewDevice(); - createRequiredResources(); - modifyShaderA(contentsC); - modifyShaderB(contentsA); - modifyShaderC(contentsB); - checkAllCacheEntries(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 3); + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 3); + SLANG_CHECK(getStats().entryCount == 6); + } + ); } }; - // One shader file on disk containing several entry points, no modifications are made to the file - struct MultipleEntryPointShader : BaseShaderCacheTest + // Test one shader file on disk with multiple entry points. + struct ShaderCacheTestEntryPoint : ShaderCacheTest { - void generateNewPipelineState(GfxIndex shaderIndex) + void runTests() { - ComPtr<IShaderProgram> shaderProgram; - slang::ProgramLayout* slangReflection; - const char* entryPointName; - switch (shaderIndex) - { - case 0: - entryPointName = "computeA"; - break; - case 1: - entryPointName = "computeB"; - break; - case 2: - entryPointName = "computeC"; - break; - default: - // Should never reach this point since we wrote the test - SLANG_IGNORE_TEST; - } - GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "multiple-entry-point-shader-cache-shader", entryPointName, slangReflection)); + // Cache is cold and we expect 3 misses, one for each entry point. + runStep( + [this]() + { + runComputePipeline("shader-cache-multiple-entry-points", "computeA"); + runComputePipeline("shader-cache-multiple-entry-points", "computeB"); + runComputePipeline("shader-cache-multiple-entry-points", "computeC"); - ComputePipelineStateDesc pipelineDesc = {}; - pipelineDesc.program = shaderProgram.get(); - GFX_CHECK_CALL_ABORT( - device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); - } + SLANG_CHECK(getStats().missCount == 3); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 3); + } + ); - void runTests() - { - generateNewDevice(); - createRequiredResources(); - generateNewPipelineState(0); - 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(1); - submitGPUWork(); - generateNewPipelineState(0); - submitGPUWork(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 1); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 1); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0); - - generateNewDevice(); - createRequiredResources(); - generateNewPipelineState(2); - submitGPUWork(); - generateNewPipelineState(1); - submitGPUWork(); - generateNewPipelineState(0); - submitGPUWork(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 1); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 2); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0); + // Cache is hot and we expect 3 hits. + runStep( + [this]() + { + runComputePipeline("shader-cache-multiple-entry-points", "computeA"); + runComputePipeline("shader-cache-multiple-entry-points", "computeB"); + runComputePipeline("shader-cache-multiple-entry-points", "computeC"); + + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 3); + SLANG_CHECK(getStats().entryCount == 3); + } + ); } }; - // One shader file contains an import/include, direct code modifications are made to the imported file - // This test specifically checks four cases: - // 1. import w/o changes in the imported file - // 2. import w/ changes in the imported file - // 3. #include w/o changes in the included file (the included file is the same as the imported file in the prior step) - // 4. #include w/ changes in the included file - struct ShaderFileImportsShaderCache : BaseShaderCacheTest + // Test cache invalidation due to an import/include file being changed on disk. + struct ShaderCacheTestImportInclude : ShaderCacheTest { String importedContentsA = String( R"( - struct TestFunction + void processElement(RWStructuredBuffer<float> buffer, uint index) { - void simpleElementAdd(RWStructuredBuffer<float> buffer, uint index) - { - var input = buffer[index]; - buffer[index] = input + 1.0f; - } - };)"); + var input = buffer[index]; + buffer[index] = input + 1.0f; + } + )"); String importedContentsB = String( R"( - struct TestFunction + void processElement(RWStructuredBuffer<float> buffer, uint index) { - void simpleElementAdd(RWStructuredBuffer<float> buffer, uint index) - { - var input = buffer[index]; - buffer[index] = input + 2.0f; - } - };)"); + var input = buffer[index]; + buffer[index] = input + 2.0f; + } + )"); String importFile = String( R"( - import test_tmp_imported; - - uniform RWStructuredBuffer<float> buffer; + import shader_cache_tmp_imported; [shader("compute")] [numthreads(4, 1, 1)] - void computeMain( - uint3 sv_dispatchThreadID : SV_DispatchThreadID) + void main( + uint3 sv_dispatchThreadID : SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer) { - TestFunction test; - for (uint i = 0; i < 4; ++i) - { - test.simpleElementAdd(buffer, i); - } - })"); + processElement(buffer, sv_dispatchThreadID.x); + } + )"); String includeFile = String( R"( - #include "test-tmp-imported.slang" + #include "shader-cache-tmp-imported.slang" - uniform RWStructuredBuffer<float> buffer; - [shader("compute")] [numthreads(4, 1, 1)] - void computeMain( - uint3 sv_dispatchThreadID : SV_DispatchThreadID) + void main( + uint3 sv_dispatchThreadID : SV_DispatchThreadID, + uniform RWStructuredBuffer<float> buffer) { - TestFunction test; - for (uint i = 0; i < 4; ++i) - { - test.simpleElementAdd(buffer, i); - } + processElement(buffer, sv_dispatchThreadID.x); })"); - void initializeFiles() + void runTests() { - diskFileSystem->saveFile("test-tmp-imported.slang", importedContentsA.getBuffer(), importedContentsA.getLength()); - diskFileSystem->saveFile("test-tmp-importing.slang", importFile.getBuffer(), importFile.getLength()); - } + // Write shader source files. + writeShader(importedContentsA, "shader-cache-tmp-imported.slang"); + writeShader(importFile, "shader-cache-tmp-import.slang"); + writeShader(includeFile, "shader-cache-tmp-include.slang"); - void modifyImportedFile(String importedContents) - { - diskFileSystem->saveFile("test-tmp-imported.slang", importedContents.getBuffer(), importedContents.getLength()); - } + // Cache is cold and we expect 2 misses. + runStep( + [this]() + { + runComputePipeline("shader-cache-tmp-import", "main"); + runComputePipeline("shader-cache-tmp-include", "main"); - void changeImportToInclude() - { - diskFileSystem->saveFile("test-tmp-importing.slang", includeFile.getBuffer(), includeFile.getLength()); - } + SLANG_CHECK(getStats().missCount == 2); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 2); + } + ); - void generateNewPipelineState() - { - ComPtr<IShaderProgram> shaderProgram; - slang::ProgramLayout* slangReflection; - GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "shader-cache-test/test-tmp-importing", "computeMain", slangReflection)); + // Cache is hot and we expect 2 hits. + runStep( + [this]() + { + runComputePipeline("shader-cache-tmp-import", "main"); + runComputePipeline("shader-cache-tmp-include", "main"); - ComputePipelineStateDesc pipelineDesc = {}; - pipelineDesc.program = shaderProgram.get(); - GFX_CHECK_CALL_ABORT( - device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); - } + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 2); + SLANG_CHECK(getStats().entryCount == 2); + } + ); - void runTests() - { - generateNewDevice(); - createRequiredResources(); - initializeFiles(); - generateNewPipelineState(); - 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(); - modifyImportedFile(importedContentsB); - generateNewPipelineState(); - submitGPUWork(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 1); - - generateNewDevice(); - createRequiredResources(); - changeImportToInclude(); - generateNewPipelineState(); - submitGPUWork(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 1); - - generateNewDevice(); - createRequiredResources(); - modifyImportedFile(importedContentsA); - generateNewPipelineState(); - submitGPUWork(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 1); + // Change content of imported/included shader file. + writeShader(importedContentsB, "shader-cache-tmp-imported.slang"); + + // Cache is cold and we expect 2 misses. + runStep( + [this]() + { + runComputePipeline("shader-cache-tmp-import", "main"); + runComputePipeline("shader-cache-tmp-include", "main"); + + SLANG_CHECK(getStats().missCount == 2); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 4); + } + ); + + // Cache is hot and we expect 2 hits. + runStep( + [this]() + { + runComputePipeline("shader-cache-tmp-import", "main"); + runComputePipeline("shader-cache-tmp-include", "main"); + + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 2); + SLANG_CHECK(getStats().entryCount == 4); + } + ); } }; // One shader featuring multiple kinds of shader objects that can be bound. - struct SpecializationArgsEntries : BaseShaderCacheTest + struct ShaderCacheTestSpecialization : ShaderCacheTest { slang::ProgramLayout* slangReflection; + void createComputePipeline() + { + ComPtr<IShaderProgram> shaderProgram; + + GFX_CHECK_CALL_ABORT( + loadComputeProgram(device, shaderProgram, "shader-cache-specialization", "computeMain", slangReflection)); + + ComputePipelineStateDesc pipelineDesc = {}; + pipelineDesc.program = shaderProgram.get(); + GFX_CHECK_CALL_ABORT( + device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); + } + void createAddTransformer(IShaderObject** transformer) { slang::TypeReflection* addTransformerType = @@ -627,7 +508,7 @@ namespace gfx_test ShaderCursor(*transformer).getPath("c").setData(&c, sizeof(float)); } - void submitGPUWork(GfxIndex transformerType) + void dispatchComputePipeline(const char* transformerTypeName) { Slang::ComPtr<ITransientResourceHeap> transientHeap; ITransientResourceHeap::Desc transientHeapDesc = {}; @@ -643,23 +524,16 @@ namespace gfx_test auto rootObject = encoder->bindPipeline(pipelineState); - ComPtr<IShaderObject> transformer; - switch (transformerType) - { - case 0: - createAddTransformer(transformer.writeRef()); - break; - case 1: - createMulTransformer(transformer.writeRef()); - break; - default: - /* Should not get here */ - SLANG_IGNORE_TEST; - } + Slang::ComPtr<IShaderObject> transformer; + slang::TypeReflection* transformerType = slangReflection->findTypeByName(transformerTypeName); + GFX_CHECK_CALL_ABORT(device->createShaderObject( + transformerType, ShaderObjectContainerType::None, transformer.writeRef())); + + float c = 1.0f; + ShaderCursor(transformer).getPath("c").setData(&c, sizeof(float)); ShaderCursor entryPointCursor(rootObject->getEntryPoint(0)); entryPointCursor.getPath("buffer").setResource(bufferView); - entryPointCursor.getPath("transformer").setObject(transformer); encoder->dispatchCompute(1, 1, 1); @@ -669,78 +543,78 @@ namespace gfx_test queue->waitOnHost(); } - void generateNewPipelineState() + void runComputePipeline(const char* transformerTypeName) { - ComPtr<IShaderProgram> shaderProgram; - - GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "compute-smoke", "computeMain", slangReflection)); - - ComputePipelineStateDesc pipelineDesc = {}; - pipelineDesc.program = shaderProgram.get(); - GFX_CHECK_CALL_ABORT( - device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); - } + createComputeResources(); + createComputePipeline(); + dispatchComputePipeline(transformerTypeName); + freeComputeResources(); + } void runTests() { - generateNewDevice(); - createRequiredResources(); - generateNewPipelineState(); - submitGPUWork(0); - - 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(); - submitGPUWork(1); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 1); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0); - } - }; + // Cache is cold and we expect 2 misses. + runStep( + [this]() + { + runComputePipeline("AddTransformer"); + runComputePipeline("MulTransformer"); - // Same gist as the multiple entry point compute shader but with a graphics - // shader file containing a vertex and fragment shader - struct Vertex - { - float position[3]; - }; + SLANG_CHECK(getStats().missCount == 2); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 2); + } + ); - static const int kVertexCount = 3; - static const Vertex kVertexData[kVertexCount] = - { - { 0, 0, 0.5 }, - { 1, 0, 0.5 }, - { 0, 1, 0.5 }, + // Cache is hot and we expect 2 hits. + runStep( + [this]() + { + runComputePipeline("AddTransformer"); + runComputePipeline("MulTransformer"); + + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 2); + SLANG_CHECK(getStats().entryCount == 2); + } + ); + } }; - struct GraphicsShaderCache : BaseShaderCacheTest + // Same gist as the multiple entry point compute shader but with a graphics + // shader file containing a vertex and fragment shader. + struct ShaderCacheTestGraphics : ShaderCacheTest { - const int kWidth = 256; - const int kHeight = 256; - const Format format = Format::R32G32B32A32_FLOAT; + struct Vertex + { + float position[3]; + }; - ComPtr<IShaderProgram> shaderProgram; - ComPtr<IRenderPassLayout> renderPass; - ComPtr<IFramebuffer> framebuffer; + static const int kWidth = 256; + static const int kHeight = 256; + static const Format format = Format::R32G32B32A32_FLOAT; ComPtr<IBufferResource> vertexBuffer; ComPtr<ITextureResource> colorBuffer; + ComPtr<IInputLayout> inputLayout; + ComPtr<IFramebufferLayout> framebufferLayout; + ComPtr<IRenderPassLayout> renderPass; + ComPtr<IFramebuffer> framebuffer; ComPtr<IBufferResource> createVertexBuffer(IDevice* device) { + const Vertex vertices[] = { + { 0, 0, 0.5 }, + { 1, 0, 0.5 }, + { 0, 1, 0.5 }, + }; + IBufferResource::Desc vertexBufferDesc; vertexBufferDesc.type = IResource::Type::Buffer; - vertexBufferDesc.sizeInBytes = kVertexCount * sizeof(Vertex); + vertexBufferDesc.sizeInBytes = sizeof(vertices); vertexBufferDesc.defaultState = ResourceState::VertexBuffer; vertexBufferDesc.allowedStates = ResourceState::VertexBuffer; - ComPtr<IBufferResource> vertexBuffer = device->createBufferResource(vertexBufferDesc, &kVertexData[0]); + ComPtr<IBufferResource> vertexBuffer = device->createBufferResource(vertexBufferDesc, vertices); SLANG_CHECK_ABORT(vertexBuffer != nullptr); return vertexBuffer; } @@ -761,13 +635,7 @@ namespace gfx_test return colorBuffer; } - void createShaderProgram() - { - slang::ProgramLayout* slangReflection; - GFX_CHECK_CALL_ABORT(loadGraphicsProgram(device, shaderProgram, "shader-cache-graphics", "vertexMain", "fragmentMain", slangReflection)); - } - - void createRequiredResources() + void createGraphicsResources() { VertexStreamDesc vertexStreams[] = { { sizeof(Vertex), InputSlotClass::PerVertex, 0 }, @@ -782,7 +650,7 @@ namespace gfx_test inputLayoutDesc.inputElements = inputElements; inputLayoutDesc.vertexStreamCount = SLANG_COUNT_OF(vertexStreams); inputLayoutDesc.vertexStreams = vertexStreams; - auto inputLayout = device->createInputLayout(inputLayoutDesc); + inputLayout = device->createInputLayout(inputLayoutDesc); SLANG_CHECK_ABORT(inputLayout != nullptr); vertexBuffer = createVertexBuffer(device); @@ -795,18 +663,9 @@ namespace gfx_test IFramebufferLayout::Desc framebufferLayoutDesc; framebufferLayoutDesc.renderTargetCount = 1; framebufferLayoutDesc.renderTargets = &targetLayout; - ComPtr<gfx::IFramebufferLayout> framebufferLayout = device->createFramebufferLayout(framebufferLayoutDesc); + framebufferLayout = device->createFramebufferLayout(framebufferLayoutDesc); SLANG_CHECK_ABORT(framebufferLayout != nullptr); - GraphicsPipelineStateDesc pipelineDesc = {}; - pipelineDesc.program = shaderProgram.get(); - pipelineDesc.inputLayout = inputLayout; - pipelineDesc.framebufferLayout = framebufferLayout; - pipelineDesc.depthStencil.depthTestEnable = false; - pipelineDesc.depthStencil.depthWriteEnable = false; - GFX_CHECK_CALL_ABORT( - device->createGraphicsPipelineState(pipelineDesc, pipelineState.writeRef())); - IRenderPassLayout::Desc renderPassDesc = {}; renderPassDesc.framebufferLayout = framebufferLayout; renderPassDesc.renderTargetCount = 1; @@ -833,7 +692,35 @@ namespace gfx_test GFX_CHECK_CALL_ABORT(device->createFramebuffer(framebufferDesc, framebuffer.writeRef())); } - void submitGPUWork() + void freeGraphicsResources() + { + inputLayout = nullptr; + framebufferLayout = nullptr; + renderPass = nullptr; + framebuffer = nullptr; + vertexBuffer = nullptr; + colorBuffer = nullptr; + pipelineState = nullptr; + } + + void createGraphicsPipeline() + { + ComPtr<IShaderProgram> shaderProgram; + slang::ProgramLayout* slangReflection; + GFX_CHECK_CALL_ABORT( + loadGraphicsProgram(device, shaderProgram, "shader-cache-graphics", "vertexMain", "fragmentMain", slangReflection)); + + GraphicsPipelineStateDesc pipelineDesc = {}; + pipelineDesc.program = shaderProgram.get(); + pipelineDesc.inputLayout = inputLayout; + pipelineDesc.framebufferLayout = framebufferLayout; + pipelineDesc.depthStencil.depthTestEnable = false; + pipelineDesc.depthStencil.depthWriteEnable = false; + GFX_CHECK_CALL_ABORT( + device->createGraphicsPipelineState(pipelineDesc, pipelineState.writeRef())); + } + + void dispatchGraphicsPipeline() { ComPtr<ITransientResourceHeap> transientHeap; ITransientResourceHeap::Desc transientHeapDesc = {}; @@ -857,28 +744,50 @@ namespace gfx_test encoder->setVertexBuffer(0, vertexBuffer); encoder->setPrimitiveTopology(PrimitiveTopology::TriangleList); - encoder->draw(kVertexCount); + encoder->draw(3); encoder->endEncoding(); commandBuffer->close(); queue->executeCommandBuffer(commandBuffer); queue->waitOnHost(); } + void runGraphicsPipeline() + { + createGraphicsResources(); + createGraphicsPipeline(); + dispatchGraphicsPipeline(); + freeGraphicsResources(); + } + void runTests() { - generateNewDevice(); - createShaderProgram(); - createRequiredResources(); - submitGPUWork(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 2); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0); + // Cache is cold and we expect 2 misses (2 entry points). + runStep( + [this]() + { + runGraphicsPipeline(); + + SLANG_CHECK(getStats().missCount == 2); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 2); + } + ); + + // Cache is hot and we expect 2 hits. + runStep( + [this]() + { + runGraphicsPipeline(); + + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 2); + SLANG_CHECK(getStats().entryCount == 2); + } + ); } }; - // Same as GraphicsShaderCache, but instead of having a singular file containing both a vertex and fragment shader, we + // Same as ShaderCacheTestGraphics, but instead of having a singular file containing both a vertex and fragment shader, we // now have two separate shader files, one containing the vertex shader and the other the fragment with the same // names, with the expectation that we should record cache misses for both fetches. // @@ -890,54 +799,38 @@ namespace gfx_test // // We do not actively test geometry shaders here, but it is simply an extension of this test and should be expected // to behave similarly. - struct SplitGraphicsShader : GraphicsShaderCache + struct ShaderCacheTestGraphicsSplit : ShaderCacheTestGraphics { - void createShaderProgram() - { - slang::ProgramLayout* slangReflection; - const char* moduleNames[] = { "split-graphics-vertex", "split-graphics-fragment" }; - GFX_CHECK_CALL_ABORT(loadSplitGraphicsProgram(device, shaderProgram, moduleNames, "main", "main", slangReflection)); - } - - Result loadSplitGraphicsProgram( - IDevice* device, - ComPtr<IShaderProgram>& outShaderProgram, - const char** shaderModuleNames, - const char* vertexEntryPointName, - const char* fragmentEntryPointName, - slang::ProgramLayout*& slangReflection) + void createGraphicsPipeline() { ComPtr<slang::ISession> slangSession; - SLANG_RETURN_ON_FAIL(device->getSlangSession(slangSession.writeRef())); + GFX_CHECK_CALL_ABORT(device->getSlangSession(slangSession.writeRef())); - ComPtr<slang::IBlob> diagnosticsBlob; - slang::IModule* vertexModule = slangSession->loadModule(shaderModuleNames[0], diagnosticsBlob.writeRef()); - if (!vertexModule) - return SLANG_FAIL; - slang::IModule* fragmentModule = slangSession->loadModule(shaderModuleNames[1], diagnosticsBlob.writeRef()); - if (!fragmentModule) - return SLANG_FAIL; + slang::IModule* vertexModule = slangSession->loadModule("shader-cache-graphics-vertex"); + SLANG_CHECK_ABORT(vertexModule); + slang::IModule* fragmentModule = slangSession->loadModule("shader-cache-graphics-fragment"); + SLANG_CHECK_ABORT(fragmentModule); ComPtr<slang::IEntryPoint> vertexEntryPoint; - SLANG_RETURN_ON_FAIL( - vertexModule->findEntryPointByName(vertexEntryPointName, vertexEntryPoint.writeRef())); + GFX_CHECK_CALL_ABORT( + vertexModule->findEntryPointByName("main", vertexEntryPoint.writeRef())); ComPtr<slang::IEntryPoint> fragmentEntryPoint; - SLANG_RETURN_ON_FAIL( - fragmentModule->findEntryPointByName(fragmentEntryPointName, fragmentEntryPoint.writeRef())); + GFX_CHECK_CALL_ABORT( + fragmentModule->findEntryPointByName("main", fragmentEntryPoint.writeRef())); Slang::List<slang::IComponentType*> componentTypes; componentTypes.add(vertexModule); componentTypes.add(fragmentModule); Slang::ComPtr<slang::IComponentType> composedProgram; - SlangResult result = slangSession->createCompositeComponentType( - componentTypes.getBuffer(), - componentTypes.getCount(), - composedProgram.writeRef(), - diagnosticsBlob.writeRef()); - SLANG_RETURN_ON_FAIL(result); - slangReflection = composedProgram->getLayout(); + GFX_CHECK_CALL_ABORT( + slangSession->createCompositeComponentType( + componentTypes.getBuffer(), + componentTypes.getCount(), + composedProgram.writeRef())); + + slang::ProgramLayout* slangReflection = composedProgram->getLayout(); Slang::List<slang::IComponentType*> entryPoints; entryPoints.add(vertexEntryPoint); @@ -949,263 +842,60 @@ namespace gfx_test programDesc.entryPointCount = 2; programDesc.slangEntryPoints = entryPoints.getBuffer(); - auto shaderProgram = device->createProgram(programDesc); - - outShaderProgram = shaderProgram; - return SLANG_OK; - } - - void runTests() - { - generateNewDevice(); - createShaderProgram(); - createRequiredResources(); - submitGPUWork(); - - device->queryInterface(SLANG_UUID_IShaderCacheStatistics, (void**)shaderCacheStats.writeRef()); - SLANG_CHECK(shaderCacheStats->getCacheMissCount() == 2); - SLANG_CHECK(shaderCacheStats->getCacheHitCount() == 0); - SLANG_CHECK(shaderCacheStats->getCacheEntryDirtyCount() == 0); - } - }; - - // Same as MultipleEntryShaderCache, but we now set the maximum entry count limit, so the cache - // should remove entries as needed when it reaches capacity. - // - // 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 - List<String> test1Lines; // C -> B - List<String> test2Lines; // A -> B - List<String> test3Lines; // A -> C - List<String> test4Lines; // C -> B -> A - List<String> entryKeys; // C, B, A - - void getCacheFile(List<String>& lines) - { - ComPtr<ISlangBlob> contentsBlob; - memoryFileSystem->loadFile(shaderCache.cacheFilename, contentsBlob.writeRef()); - List<UnownedStringSlice> temp; - StringUtil::calcLines(UnownedStringSlice((char*)contentsBlob->getBufferPointer()), temp); - for (auto line : temp) - { - if (line.trim().getLength() != 0) - lines.add(line); - } - } - - // Check the correctness of the cache's entries by comparing the order of entries in the - // current state of the cache with what we expect. - void checkCacheFiles() - { - // Check that shader A appears where we expect it to. - SLANG_CHECK(test2Lines[0] == test3Lines[0]); - SLANG_CHECK(test2Lines[0] == test4Lines[2]); - - // Check that shader B appears where we expect it to. - SLANG_CHECK(test1Lines[1] == test2Lines[1]); - SLANG_CHECK(test1Lines[1] == test4Lines[1]); - - // Check that shader C appears where we expect it to. - SLANG_CHECK(test1Lines[0] == test3Lines[1]); - SLANG_CHECK(test1Lines[0] == test4Lines[0]); - } - - // Cache limit 3, three unique shaders - void runTest0() - { - shaderCache.entryCountLimit = 3; - generateNewDevice(); - createRequiredResources(); - modifyShaderA(contentsA); - modifyShaderB(contentsB); - modifyShaderC(contentsC); - generateNewPipelineState(0); - submitGPUWork(); - generateNewPipelineState(1); - submitGPUWork(); - generateNewPipelineState(2); - submitGPUWork(); - - 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 - // to use in later checks. - for (auto line : test0Lines) - { - List<UnownedStringSlice> digests; - StringUtil::split(line.getUnownedSlice(), ' ', digests); - if (digests.getCount() != 2) - continue; - entryKeys.add(digests[0]); - } - - ComPtr<ISlangBlob> unused; - 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 - void runTest1() - { - shaderCache.entryCountLimit = 2; - generateNewDevice(); - createRequiredResources(); - modifyShaderA(contentsA); - modifyShaderB(contentsB); - modifyShaderC(contentsC); - generateNewPipelineState(0); - submitGPUWork(); - generateNewPipelineState(1); - submitGPUWork(); - generateNewPipelineState(2); - submitGPUWork(); - - 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(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 - void runTest2() - { - generateNewDevice(); - createRequiredResources(); - generateNewPipelineState(1); - submitGPUWork(); - generateNewPipelineState(0); - submitGPUWork(); - - 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(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()))); - } + ComPtr<IShaderProgram> shaderProgram = device->createProgram(programDesc); - // Cache limit 2, access shaders C and then A - void runTest3() - { - generateNewDevice(); - createRequiredResources(); - generateNewPipelineState(2); - submitGPUWork(); - generateNewPipelineState(0); - submitGPUWork(); - - 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(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()))); + GraphicsPipelineStateDesc pipelineDesc = {}; + pipelineDesc.program = shaderProgram.get(); + pipelineDesc.inputLayout = inputLayout; + pipelineDesc.framebufferLayout = framebufferLayout; + pipelineDesc.depthStencil.depthTestEnable = false; + pipelineDesc.depthStencil.depthWriteEnable = false; + GFX_CHECK_CALL_ABORT( + device->createGraphicsPipelineState(pipelineDesc, pipelineState.writeRef())); } - // Cache limit 3, access shaders A then B then C - void runTest4() + void runGraphicsPipeline() { - shaderCache.entryCountLimit = 3; - generateNewDevice(); - createRequiredResources(); - generateNewPipelineState(0); - submitGPUWork(); - generateNewPipelineState(1); - submitGPUWork(); - generateNewPipelineState(2); - submitGPUWork(); - - 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(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()))); - } + createGraphicsResources(); + createGraphicsPipeline(); + dispatchGraphicsPipeline(); + freeGraphicsResources(); + } void runTests() { - runTest0(); - runTest1(); - runTest2(); - runTest3(); - runTest4(); + // Cache is cold and we expect 2 misses (2 entry points). + runStep( + [this]() + { + runGraphicsPipeline(); - checkCacheFiles(); - } + SLANG_CHECK(getStats().missCount == 2); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 2); + } + ); - void run() - { - shaderCache.shaderCacheFileSystem = memoryFileSystem; - runTests(); + // Cache is hot and we expect 2 hits. + runStep( + [this]() + { + runGraphicsPipeline(); - cleanUpFiles(); - } + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 2); + SLANG_CHECK(getStats().entryCount == 2); + } + ); } }; - // 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 + // Test caching of shaders that are compiled from source strings instead of files. + struct ShaderCacheTestSourceString : ShaderCacheTest { - void generateNewPipelineState(Slang::String shaderContents) + void createComputePipeline(Slang::String shaderSource) { ComPtr<IShaderProgram> shaderProgram; - GFX_CHECK_CALL_ABORT(loadComputeProgramFromSource(device, shaderProgram, shaderContents)); + GFX_CHECK_CALL_ABORT(loadComputeProgramFromSource(device, shaderProgram, shaderSource)); ComputePipelineStateDesc pipelineDesc = {}; pipelineDesc.program = shaderProgram.get(); @@ -1213,135 +903,120 @@ namespace gfx_test device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); } - void runTests() + void runComputePipeline(Slang::String shaderSource) { - 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); + createComputeResources(); + createComputePipeline(shaderSource); + dispatchComputePipeline(); + freeComputeResources(); } - }; - template <typename T> - void shaderCacheTestImpl(ComPtr<IDevice> device, UnitTestContext* context) - { - T test; - test.init(device, context); - test.run(); - } + void runTests() + { + // Cache is cold and we expect 3 misses. + runStep( + [this]() + { + runComputePipeline(computeShaderA); + runComputePipeline(computeShaderB); + runComputePipeline(computeShaderC); - SLANG_UNIT_TEST(singleEntryShaderCacheD3D12) - { - runTestImpl(shaderCacheTestImpl<SingleEntryShaderCache>, unitTestContext, Slang::RenderApiFlag::D3D12); - } + SLANG_CHECK(getStats().missCount == 3); + SLANG_CHECK(getStats().hitCount == 0); + SLANG_CHECK(getStats().entryCount == 3); + } + ); - SLANG_UNIT_TEST(singleEntryShaderCacheVulkan) - { - runTestImpl(shaderCacheTestImpl<SingleEntryShaderCache>, unitTestContext, Slang::RenderApiFlag::Vulkan); - } + // Cache is hot and we expect 3 hits. + runStep( + [this]() + { + runComputePipeline(computeShaderA); + runComputePipeline(computeShaderB); + runComputePipeline(computeShaderC); - SLANG_UNIT_TEST(multipleEntryShaderCacheD3D12) - { - runTestImpl(shaderCacheTestImpl<MultipleEntryShaderCache>, unitTestContext, Slang::RenderApiFlag::D3D12); - } + SLANG_CHECK(getStats().missCount == 0); + SLANG_CHECK(getStats().hitCount == 3); + SLANG_CHECK(getStats().entryCount == 3); + } + ); + } + }; - SLANG_UNIT_TEST(multipleEntryShaderCacheVulkan) + template<typename T> + void runTest(UnitTestContext* context, Slang::RenderApiFlag::Enum api) { - runTestImpl(shaderCacheTestImpl<MultipleEntryShaderCache>, unitTestContext, Slang::RenderApiFlag::Vulkan); + T test; + test.run(context, api); } - SLANG_UNIT_TEST(multipleEntryPointShaderCacheD3D12) + SLANG_UNIT_TEST(shaderCacheBasicD3D12) { - runTestImpl(shaderCacheTestImpl<MultipleEntryPointShader>, unitTestContext, Slang::RenderApiFlag::D3D12); + runTest<ShaderCacheTestBasic>(unitTestContext, Slang::RenderApiFlag::D3D12); } - SLANG_UNIT_TEST(multipleEntryPointShaderCacheVulkan) + SLANG_UNIT_TEST(shaderCacheBasicVulkan) { - runTestImpl(shaderCacheTestImpl<MultipleEntryPointShader>, unitTestContext, Slang::RenderApiFlag::Vulkan); + runTest<ShaderCacheTestBasic>(unitTestContext, Slang::RenderApiFlag::Vulkan); } - SLANG_UNIT_TEST(shaderFileImportsShaderCacheD3D12) + SLANG_UNIT_TEST(shaderCacheEntryPointD3D12) { - runTestImpl(shaderCacheTestImpl<ShaderFileImportsShaderCache>, unitTestContext, Slang::RenderApiFlag::D3D12); + runTest<ShaderCacheTestEntryPoint>(unitTestContext, Slang::RenderApiFlag::D3D12); } - SLANG_UNIT_TEST(shaderFileImportsShaderCacheVulkan) + SLANG_UNIT_TEST(shaderCacheEntryPointVulkan) { - runTestImpl(shaderCacheTestImpl<ShaderFileImportsShaderCache>, unitTestContext, Slang::RenderApiFlag::Vulkan); + runTest<ShaderCacheTestEntryPoint>(unitTestContext, Slang::RenderApiFlag::Vulkan); } - SLANG_UNIT_TEST(specializationArgsShaderCacheD3D12) + SLANG_UNIT_TEST(shaderCacheImportIncludeD3D12) { - runTestImpl(shaderCacheTestImpl<SpecializationArgsEntries>, unitTestContext, Slang::RenderApiFlag::D3D12); + runTest<ShaderCacheTestImportInclude>(unitTestContext, Slang::RenderApiFlag::D3D12); } - SLANG_UNIT_TEST(specializationArgsShaderCacheVulkan) + SLANG_UNIT_TEST(shaderCacheImportIncludeVulkan) { - runTestImpl(shaderCacheTestImpl<SpecializationArgsEntries>, unitTestContext, Slang::RenderApiFlag::Vulkan); + runTest<ShaderCacheTestImportInclude>(unitTestContext, Slang::RenderApiFlag::Vulkan); } - SLANG_UNIT_TEST(cacheEvictionPolicyD3D12) + SLANG_UNIT_TEST(shaderCacheSpecializationD3D12) { - runTestImpl(shaderCacheTestImpl<CacheWithMaxEntryLimit>, unitTestContext, Slang::RenderApiFlag::D3D12); + runTest<ShaderCacheTestSpecialization>(unitTestContext, Slang::RenderApiFlag::D3D12); } - SLANG_UNIT_TEST(cacheEvictionPolicyVulkan) + SLANG_UNIT_TEST(shaderCacheSpecializationVulkan) { - runTestImpl(shaderCacheTestImpl<CacheWithMaxEntryLimit>, unitTestContext, Slang::RenderApiFlag::Vulkan); + runTest<ShaderCacheTestSpecialization>(unitTestContext, Slang::RenderApiFlag::Vulkan); } - SLANG_UNIT_TEST(graphicsShaderCacheD3D12) + SLANG_UNIT_TEST(shaderCacheGraphicsD3D12) { - runTestImpl(shaderCacheTestImpl<GraphicsShaderCache>, unitTestContext, Slang::RenderApiFlag::D3D12); + runTest<ShaderCacheTestGraphics>(unitTestContext, Slang::RenderApiFlag::D3D12); } - SLANG_UNIT_TEST(graphicsShaderCacheVulkan) + SLANG_UNIT_TEST(shaderCacheGraphicsVulkan) { - runTestImpl(shaderCacheTestImpl<GraphicsShaderCache>, unitTestContext, Slang::RenderApiFlag::Vulkan); + runTest<ShaderCacheTestGraphics>(unitTestContext, Slang::RenderApiFlag::Vulkan); } - SLANG_UNIT_TEST(splitGraphicsShaderCacheD3D12) + SLANG_UNIT_TEST(shaderCacheGraphicsSplitD3D12) { - runTestImpl(shaderCacheTestImpl<SplitGraphicsShader>, unitTestContext, Slang::RenderApiFlag::D3D12); + runTest<ShaderCacheTestGraphicsSplit>(unitTestContext, Slang::RenderApiFlag::D3D12); } - SLANG_UNIT_TEST(splitGraphicsShaderCacheVulkan) + SLANG_UNIT_TEST(shaderCacheGraphicsSplitVulkan) { - runTestImpl(shaderCacheTestImpl<SplitGraphicsShader>, unitTestContext, Slang::RenderApiFlag::Vulkan); + runTest<ShaderCacheTestGraphicsSplit>(unitTestContext, Slang::RenderApiFlag::Vulkan); } - SLANG_UNIT_TEST(nonPhysicalFileDependenciesCacheEntryD3D12) + SLANG_UNIT_TEST(shaderCacheSourceStringD3D12) { - runTestImpl(shaderCacheTestImpl<NonPhysicalFileDependencyEntry>, unitTestContext, Slang::RenderApiFlag::D3D12); + runTest<ShaderCacheTestSourceString>(unitTestContext, Slang::RenderApiFlag::D3D12); } - SLANG_UNIT_TEST(nonPhysicalFileDependenciesCacheEntryVulkan) + SLANG_UNIT_TEST(shaderCacheSourceStringVulkan) { - runTestImpl(shaderCacheTestImpl<NonPhysicalFileDependencyEntry>, unitTestContext, Slang::RenderApiFlag::Vulkan); + runTest<ShaderCacheTestSourceString>(unitTestContext, Slang::RenderApiFlag::Vulkan); } } diff --git a/tools/gfx/gfx.slang b/tools/gfx/gfx.slang index b4bd76470..3d75e3b40 100644 --- a/tools/gfx/gfx.slang +++ b/tools/gfx/gfx.slang @@ -1712,15 +1712,10 @@ struct SlangDesc struct ShaderCacheDesc { - // The filename for the file the cache's state should be saved to or loaded from. - NativeString cacheFilename = "cache.txt"; - // The root directory for the shader cache. + // The root directory for the shader cache. If not set, shader cache is disabled. NativeString shaderCachePath; - // The file system for loading cached shader kernels. The layer does not maintain a strong reference to the object, - // instead the user is responsible for holding the object alive during the lifetime of an `IDevice`. - void* shaderCacheFileSystem = nullptr; // The maximum number of entries stored in the cache. - GfxCount entryCountLimit = 0; + GfxCount maxEntryCount = 0; }; struct DeviceInteropHandles @@ -1934,6 +1929,21 @@ interface IDevice Result getTextureRowAlignment(out Size outAlignment); }; +struct ShaderCacheStats +{ + GfxCount hitCount; + GfxCount missCount; + GfxCount entryCount; +}; + +[COM("715bdf26-5135-11eb-AE93-02-42-AC-13-00-02")] +interface IShaderCache +{ + Result clearShaderCache(); + Result getShaderCacheStats(out ShaderCacheStats outStats); + Result resetShaderCacheStats(); +}; + #define SLANG_GFX_IMPORT [DllImport("gfx")] /// Checks if format is compressed SLANG_GFX_IMPORT bool gfxIsCompressedFormat(Format format); diff --git a/tools/gfx/persistent-shader-cache.cpp b/tools/gfx/persistent-shader-cache.cpp deleted file mode 100644 index 7dc64632b..000000000 --- a/tools/gfx/persistent-shader-cache.cpp +++ /dev/null @@ -1,316 +0,0 @@ -// slang-shader-cache-index.cpp -#include "persistent-shader-cache.h" - -#include "../../source/core/slang-io.h" -#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; - - // If a path is provided, we will want our underlying file system to be initialized using that path. - if (desc.shaderCachePath) - { - if (!desc.shaderCacheFileSystem) - { - // Only a path was provided, so we get a mutable file system - // using OSFileSystem::getMutableSingleton. - desc.shaderCacheFileSystem = OSFileSystem::getMutableSingleton(); - } - desc.shaderCacheFileSystem = new RelativeFileSystem(desc.shaderCacheFileSystem, desc.shaderCachePath); - } - - // If our shader cache has an underlying file system, check if it's mutable. If so, store a pointer - // to the mutable version for operations which require writing to disk. - if (desc.shaderCacheFileSystem) - { - desc.shaderCacheFileSystem->queryInterface(ISlangMutableFileSystem::getTypeGuid(), (void**)mutableShaderCacheFileSystem.writeRef()); - } - - 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() -{ - // 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) - { - ComPtr<ISlangBlob> fullPath; - if (SLANG_FAILED(mutableShaderCacheFileSystem->getPath(PathKind::OperatingSystem, desc.cacheFilename, fullPath.writeRef()))) - { - // 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; - } - filePath = String((char*)fullPath->getBufferPointer()); - } - else - { - filePath = Path::combine(String(desc.shaderCachePath), String(desc.cacheFilename)); - } - - if (SLANG_FAILED(indexStream.init(filePath, FileMode::Open, FileAccess::ReadWrite, FileShare::ReadWrite))) - { - // 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); - - 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); - - // 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); - } - } - } -} - -ShaderCacheEntry* PersistentShaderCache::findEntry(const DigestType& key, ISlangBlob** outCompiledCode) -{ - LinkedNode<Index>* entryIndexNode; - if (!keyToEntry.TryGetValue(key, entryIndexNode)) - { - // The key was not found in the cache, so we return nullptr. - *outCompiledCode = nullptr; - return nullptr; - } - - // 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(key.toString().getBuffer(), outCompiledCode); - auto index = entryIndexNode->Value; - entries[index].lastAccessedTime = (double)high_resolution_clock::now().time_since_epoch().count(); - if (orderedEntries.FirstNode() != entryIndexNode) - { - orderedEntries.RemoveFromList(entryIndexNode); - orderedEntries.AddFirst(entryIndexNode); - if (mutableShaderCacheFileSystem && !isMemoryFileSystem) - { - auto offset = index * sizeof(ShaderCacheEntry); - indexStream.seek(SeekOrigin::Start, offset + 2 * sizeof(DigestType)); - indexStream.write(&entries[index].lastAccessedTime, sizeof(double)); - indexStream.flush(); - } - } - return &entries[index]; -} - -void PersistentShaderCache::addEntry(const DigestType& dependencyDigest, const DigestType& contentsDigest, ISlangBlob* compiledCode) -{ - if (!mutableShaderCacheFileSystem) - { - // Should not save new entries if the underlying file system isn't mutable. - return; - } - - // 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, 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) - { - index = deleteLRUEntry(); - } - - 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(dependencyDigest.toString().getBuffer(), compiledCode); - - if (!isMemoryFileSystem) - { - indexStream.seek(SeekOrigin::End, 0); - indexStream.write(&entry, sizeof(ShaderCacheEntry)); - indexStream.flush(); - } -} - -void PersistentShaderCache::updateEntry( - const DigestType& dependencyDigest, - const DigestType& contentsDigest, - ISlangBlob* updatedCode) -{ - if (!mutableShaderCacheFileSystem) - { - // Updating entries requires saving to disk in order to overwrite the old shader file - // on disk, so we return if the underlying file system isn't mutable. - return; - } - - // 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(dependencyDigest.toString().getBuffer(), updatedCode); - - if (!isMemoryFileSystem) - { - auto offset = index * sizeof(ShaderCacheEntry); - indexStream.seek(SeekOrigin::Start, offset + sizeof(DigestType)); - indexStream.write(&contentsDigest, sizeof(DigestType)); - indexStream.flush(); - } -} - -Index PersistentShaderCache::deleteLRUEntry() -{ - if (!mutableShaderCacheFileSystem) - { - // 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; - } - - auto lruEntry = orderedEntries.LastNode(); - auto index = lruEntry->Value; - auto shaderKey = entries[index].dependencyBasedDigest; - - keyToEntry.Remove(shaderKey); - mutableShaderCacheFileSystem->remove(shaderKey.toString().getBuffer()); - - orderedEntries.Delete(lruEntry); - return index; -} - -// 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() -{ - ComPtr<ISlangBlob> indexBlob; - if (SLANG_FAILED(mutableShaderCacheFileSystem->loadFile(desc.cacheFilename, indexBlob.writeRef()))) - { - mutableShaderCacheFileSystem->saveFile(desc.cacheFilename, nullptr, 0); - return; - } - - auto indexString = UnownedStringSlice((char*)indexBlob->getBufferPointer()); - - 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 = DigestType(entryFields[0]); - entry.contentsBasedDigest = DigestType(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; - } -} - -void PersistentShaderCache::saveCacheToMemory() -{ - StringBuilder indexSb; - for (auto& entryIndex : orderedEntries) - { - auto entry = entries[entryIndex]; - indexSb << entry.dependencyBasedDigest.toString(); - indexSb << " "; - indexSb << entry.contentsBasedDigest.toString(); - 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 deleted file mode 100644 index 530d50a58..000000000 --- a/tools/gfx/persistent-shader-cache.h +++ /dev/null @@ -1,99 +0,0 @@ -// slang-shader-cache-index.h -#pragma once -#include "../../slang.h" -#include "../../slang-gfx.h" -#include "../../slang-com-ptr.h" - -#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" -#include "../../source/core/slang-crypto.h" - -namespace gfx -{ - -using namespace Slang; - -using DigestType = MD5::Digest; - -struct ShaderCacheEntry -{ - DigestType dependencyBasedDigest; - DigestType 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. - ShaderCacheEntry* findEntry(const DigestType& 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 - // delete the least recently used entry. - void addEntry(const DigestType& dependencyDigest, const DigestType& contentsDigest, ISlangBlob* compiledCode); - - // Update the contents hash for the specified entry in the cache and update the - // corresponding file on disk. - void updateEntry(const DigestType& dependencyDigest, const DigestType& 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(); - - // Delete the last entry (the least recently used) from entries, remove its key/value pair - // 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; - - // 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<DigestType, 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 3397b325e..4a8fd04b6 100644 --- a/tools/gfx/renderer-shared.cpp +++ b/tools/gfx/renderer-shared.cpp @@ -27,7 +27,7 @@ const Slang::Guid GfxGUID::IID_IResource = SLANG_UUID_IResource; const Slang::Guid GfxGUID::IID_IBufferResource = SLANG_UUID_IBufferResource; const Slang::Guid GfxGUID::IID_ITextureResource = SLANG_UUID_ITextureResource; const Slang::Guid GfxGUID::IID_IDevice = SLANG_UUID_IDevice; -const Slang::Guid GfxGUID::IID_IShaderCacheStatistics = SLANG_UUID_IShaderCacheStatistics; +const Slang::Guid GfxGUID::IID_IShaderCache = SLANG_UUID_IShaderCache; const Slang::Guid GfxGUID::IID_IShaderObject = SLANG_UUID_IShaderObject; const Slang::Guid GfxGUID::IID_IRenderPassLayout = SLANG_UUID_IRenderPassLayout; @@ -343,48 +343,19 @@ Result RendererBase::getEntryPointCodeFromShaderCache( return program->getEntryPointCode(entryPointIndex, targetIndex, outCode, outDiagnostics); } - // Produce a string which we can use to query the shader cache by combining two separate hashes which - // together comprise all the compilation arguments for this program. - ComPtr<slang::ISession> session; - getSlangSession(session.writeRef()); - - ComPtr<ISlangBlob> shaderKeyBlob; - program->computeDependencyBasedHash(entryPointIndex, targetIndex, shaderKeyBlob.writeRef()); - DigestType shaderKey(shaderKeyBlob); - - // Produce a hash using the AST for this program - This is needed to check whether a cache entry is effectively dirty, - // or to save along with the compiled code into an entry so the entry can be checked if fetched later on. - ComPtr<ISlangBlob> contentsHashBlob; - program->computeContentsBasedHash(contentsHashBlob.writeRef()); - DigestType contentsHash(contentsHashBlob); + // Hash all relevant state for generating the entry point shader code to use as a key + // for the shader cache. + ComPtr<ISlangBlob> hashBlob; + program->getEntryPointHash(entryPointIndex, targetIndex, hashBlob.writeRef()); + PersistentCache::Key cacheKey(hashBlob); + // Query the shader cache. ComPtr<ISlangBlob> codeBlob; - - // Query the shader cache index for an entry with shaderKey as its key. - auto entry = persistentShaderCache->findEntry(shaderKey, codeBlob.writeRef()); - if (entry && contentsHash == entry->contentsBasedDigest) + if (persistentShaderCache->readEntry(cacheKey, codeBlob.writeRef()) != SLANG_OK) { - // We found the entry in the cache, and the entry's contents are up-to-date. Nothing else needs to be done. - shaderCacheHitCount++; - } - else - { - // There are two possibilities: the entry does not exist in the cache, or the entry's contents are out-of-date. - // Both will require calling getEntryPointCode() in order to fetch the correct compiled code, so we'll do that now. + // No cached entry found. Generate the code and add it to the cache. SLANG_RETURN_ON_FAIL(program->getEntryPointCode(entryPointIndex, targetIndex, codeBlob.writeRef(), outDiagnostics)); - - // If the entry was not found in the cache, let's add it. Otherwise, the entry's contents were out-of-date, so let's - // update the entry with the updated contents. - if (!entry) - { - persistentShaderCache->addEntry(shaderKey, contentsHash, codeBlob); - shaderCacheMissCount++; - } - else - { - persistentShaderCache->updateEntry(shaderKey, contentsHash, codeBlob); - shaderCacheEntryDirtyCount++; - } + persistentShaderCache->writeEntry(cacheKey, codeBlob); } *outCode = codeBlob.detach(); @@ -393,9 +364,10 @@ Result RendererBase::getEntryPointCodeFromShaderCache( SlangResult RendererBase::queryInterface(SlangUUID const& uuid, void** outObject) { - if (uuid == GfxGUID::IID_IShaderCacheStatistics) + // Only return the shader cache interface if it is enabled. + if (uuid == GfxGUID::IID_IShaderCache && persistentShaderCache) { - *outObject = static_cast<IShaderCacheStatistics*>(this); + *outObject = static_cast<IShaderCache*>(this); addRef(); return SLANG_OK; } @@ -413,12 +385,13 @@ IDevice* gfx::RendererBase::getInterface(const Guid& guid) SLANG_NO_THROW Result SLANG_MCALL RendererBase::initialize(const Desc& desc) { - auto cacheDesc = desc.shaderCache; - // We only want to initialize the shader cache if either a shader cache path or file system - // was provided. - if (cacheDesc.shaderCachePath || cacheDesc.shaderCacheFileSystem) + // We only want to initialize the shader cache if a shader cache path was provided. + if (desc.shaderCache.shaderCachePath) { - persistentShaderCache = new PersistentShaderCache(desc.shaderCache); + PersistentCache::Desc cacheDesc; + cacheDesc.directory = desc.shaderCache.shaderCachePath; + cacheDesc.maxEntryCount = desc.shaderCache.maxEntryCount; + persistentShaderCache = new PersistentCache(cacheDesc); } if (desc.apiCommandDispatcher) @@ -751,26 +724,31 @@ Result RendererBase::getShaderObjectLayout( return SLANG_OK; } -GfxCount RendererBase::getCacheMissCount() +Result RendererBase::clearShaderCache() { - return shaderCacheMissCount; + SLANG_ASSERT(persistentShaderCache); + return persistentShaderCache->clear(); } -GfxCount RendererBase::getCacheHitCount() +Result RendererBase::getShaderCacheStats(ShaderCacheStats* outStats) { - return shaderCacheHitCount; -} + SLANG_ASSERT(persistentShaderCache); + if (!outStats) + { + return SLANG_E_INVALID_ARG; + } -GfxCount RendererBase::getCacheEntryDirtyCount() -{ - return shaderCacheEntryDirtyCount; + const auto& stats = persistentShaderCache->getStats(); + outStats->entryCount = (GfxCount)stats.entryCount; + outStats->hitCount = (GfxCount)stats.hitCount; + outStats->missCount = (GfxCount)stats.missCount; + return SLANG_OK; } -Result RendererBase::resetCacheStatistics() +Result RendererBase::resetShaderCacheStats() { - shaderCacheMissCount = 0; - shaderCacheHitCount = 0; - shaderCacheEntryDirtyCount = 0; + SLANG_ASSERT(persistentShaderCache); + persistentShaderCache->resetStats(); return SLANG_OK; } diff --git a/tools/gfx/renderer-shared.h b/tools/gfx/renderer-shared.h index 01111e292..c7137f0fa 100644 --- a/tools/gfx/renderer-shared.h +++ b/tools/gfx/renderer-shared.h @@ -4,8 +4,7 @@ #include "slang-context.h" #include "core/slang-basic.h" #include "core/slang-com-object.h" - -#include "persistent-shader-cache.h" +#include "core/slang-persistent-cache.h" #include "resource-desc-utils.h" @@ -28,7 +27,7 @@ struct GfxGUID static const Slang::Guid IID_ITextureResource; static const Slang::Guid IID_IInputLayout; static const Slang::Guid IID_IDevice; - static const Slang::Guid IID_IShaderCacheStatistics; + static const Slang::Guid IID_IShaderCache; static const Slang::Guid IID_IShaderObjectLayout; static const Slang::Guid IID_IShaderObject; static const Slang::Guid IID_IRenderPassLayout; @@ -1214,7 +1213,7 @@ public: // Renderer implementation shared by all platforms. // Responsible for shader compilation, specialization and caching. -class RendererBase : public IDevice, public IShaderCacheStatistics, public Slang::ComObject +class RendererBase : public IDevice, public IShaderCache, public Slang::ComObject { friend class ShaderObjectBase; public: @@ -1354,27 +1353,21 @@ public: ShaderObjectLayoutBase* layout, IShaderObject** outObject) = 0; + public: + // IShaderCache interface + virtual SLANG_NO_THROW Result SLANG_MCALL clearShaderCache() SLANG_OVERRIDE; + virtual SLANG_NO_THROW Result SLANG_MCALL getShaderCacheStats(ShaderCacheStats* outStats) SLANG_OVERRIDE; + virtual SLANG_NO_THROW Result SLANG_MCALL resetShaderCacheStats() SLANG_OVERRIDE; + protected: virtual SLANG_NO_THROW SlangResult SLANG_MCALL initialize(const Desc& desc); protected: Slang::List<Slang::String> m_features; - -public: - virtual SLANG_NO_THROW GfxCount SLANG_MCALL getCacheMissCount() override; - virtual SLANG_NO_THROW GfxCount SLANG_MCALL getCacheHitCount() override; - virtual SLANG_NO_THROW GfxCount SLANG_MCALL getCacheEntryDirtyCount() override; - virtual SLANG_NO_THROW Result SLANG_MCALL resetCacheStatistics() override; - -protected: - GfxCount shaderCacheMissCount = 0; - GfxCount shaderCacheHitCount = 0; - GfxCount shaderCacheEntryDirtyCount = 0; - public: SlangContext slangContext; ShaderCache shaderCache; - RefPtr<PersistentShaderCache> persistentShaderCache = nullptr; + Slang::RefPtr<Slang::PersistentCache> persistentShaderCache; Slang::Dictionary<slang::TypeLayoutReflection*, Slang::RefPtr<ShaderObjectLayoutBase>> m_shaderObjectLayoutCache; Slang::ComPtr<IPipelineCreationAPIDispatcher> m_pipelineCreationAPIDispatcher; diff --git a/tools/slang-unit-test/unit-test-lock-file.cpp b/tools/slang-unit-test/unit-test-lock-file.cpp index c5709242d..33e787a1d 100644 --- a/tools/slang-unit-test/unit-test-lock-file.cpp +++ b/tools/slang-unit-test/unit-test-lock-file.cpp @@ -12,13 +12,13 @@ using namespace Slang; SLANG_UNIT_TEST(lockFile) { - static const String fileName = "test_lock_file"; + static String fileName = Path::simplify(Path::getParentDirectory(Path::getExecutablePath()) + "/test_lock_file"); // Open/close lock file. { LockFile file; SLANG_CHECK(file.isOpen() == false); - SLANG_CHECK(file.open(fileName) == SLANG_OK); + SLANG_CHECK_ABORT(file.open(fileName) == SLANG_OK); SLANG_CHECK(file.isOpen() == true); SLANG_CHECK(File::exists(fileName) == true); file.close(); diff --git a/tools/slang-unit-test/unit-test-persistent-cache.cpp b/tools/slang-unit-test/unit-test-persistent-cache.cpp new file mode 100644 index 000000000..55c358d77 --- /dev/null +++ b/tools/slang-unit-test/unit-test-persistent-cache.cpp @@ -0,0 +1,629 @@ +// unit-test-persistent-cache.cpp +#include "tools/unit-test/slang-unit-test.h" + +#include "../../source/core/slang-persistent-cache.h" +#include "../../source/core/slang-io.h" +#include "../../source/core/slang-file-system.h" +#include "../../source/core/slang-random-generator.h" + +#include <chrono> +#include <thread> +#include <atomic> +#include <mutex> +#include <condition_variable> +#include <functional> + +using namespace Slang; + +static DefaultRandomGenerator rng(0xdeadbeef); + +inline ComPtr<ISlangBlob> createRandomBlob(size_t size) +{ + ScopedAllocation alloc; + alloc.allocate(size); + rng.nextData(alloc.getData(), size); + return RawBlob::moveCreate(alloc); +} + +inline bool isBlobEqual(ISlangBlob* a, ISlangBlob* b) +{ + return + a->getBufferSize() == b->getBufferSize() && + ::memcmp(a->getBufferPointer(), b->getBufferPointer(), a->getBufferSize()) == 0; +} + +class Barrier +{ +public: + Barrier(size_t threadCount, std::function<void()> completionFunc = nullptr) + : m_threadCount(threadCount) + , m_waitCount(threadCount) + , m_completionFunc(completionFunc) + {} + + Barrier(const Barrier& barrier) = delete; + Barrier& operator=(const Barrier& barrier) = delete; + + void wait() + { + std::unique_lock<std::mutex> lock(m_mutex); + + auto generation = m_generation; + + if (--m_waitCount == 0) + { + if (m_completionFunc) m_completionFunc(); + ++m_generation; + m_waitCount = m_threadCount; + m_condition.notify_all(); + } + else + { + m_condition.wait(lock, [this, generation] () { return generation != m_generation; }); + } + } + +private: + size_t m_threadCount; + size_t m_waitCount; + size_t m_generation = 0; + std::function<void()> m_completionFunc; + std::mutex m_mutex; + std::condition_variable m_condition; +}; + +namespace Slang +{ + +/// Helper class for performing tests on the persistent cache. +/// This class is a friend class of PersistentCache and can access its internals. +struct PersistentCacheTest +{ + ISlangMutableFileSystem* osFileSystem; + String cacheDirectory; + RefPtr<PersistentCache> cache; + + PersistentCacheTest(Count maxEntryCount = 0) + { + osFileSystem = OSFileSystem::getMutableSingleton(); + cacheDirectory = Path::simplify(Path::getParentDirectory(Path::getExecutablePath()) + "/persistent-cache-test"); + + removeCacheFiles(); + + PersistentCache::Desc desc; + desc.directory = cacheDirectory.getBuffer(); + desc.maxEntryCount = maxEntryCount; + cache = new PersistentCache(desc); + } + + virtual ~PersistentCacheTest() + { + cache = nullptr; + + removeCacheFiles(); + } + + void removeCacheFiles() + { + // Remove all files the cache created. + osFileSystem->enumeratePathContents( + cacheDirectory.getBuffer(), + [](SlangPathType pathType, const char* fileName, void* userData) + { + PersistentCacheTest* self = static_cast<PersistentCacheTest*>(userData); + String path = self->cacheDirectory + "/" + fileName; + self->osFileSystem->remove(path.getBuffer()); + }, + this); + + // Also remove the cache directory. + osFileSystem->remove(cacheDirectory.getBuffer()); + } + + // Entry (key, data) for testing. + struct Entry + { + PersistentCache::Key key; + ComPtr<ISlangBlob> data; + }; + + // Helper to write an entry to the cache. + void writeEntry(const Entry& entry) + { + SLANG_CHECK(cache->writeEntry(entry.key, entry.data) == SLANG_OK); + } + + // Helper to read an entry from the cache and discard the data. + // Returns true if the entry was found, false otherwise. + bool readEntry(const Entry& entry) + { + ComPtr<ISlangBlob> data; + SlangResult result = cache->readEntry(entry.key, data.writeRef()); + SLANG_CHECK(result == SLANG_OK || result == SLANG_E_NOT_FOUND); + if (result == SLANG_OK) + { + SLANG_CHECK(isBlobEqual(data, entry.data)); + } + if (result == SLANG_E_NOT_FOUND) + { + SLANG_CHECK(data == nullptr); + } + return result == SLANG_OK; + } + + // Get the absolute filename for a cache entry file. + String getEntryFileName(const Entry& entry) + { + return cache->getEntryFileName(entry.key); + } + + // Get the absolute filename of the cache index file. + String getIndexFilename() + { + return cache->m_indexFileName; + } +}; + +} // namespace Slang + +// Performs basic tests on the cache. +// - write/read entries +// - check for correct cache stats +// - clearing the cache +// - resetting stats +struct BasicTest : public PersistentCacheTest +{ + BasicTest() : PersistentCacheTest() {} + + void run() + { + // Check that cache is empty. + SLANG_CHECK(cache->getStats().entryCount == 0); + SLANG_CHECK(cache->getStats().hitCount == 0); + SLANG_CHECK(cache->getStats().missCount == 0); + + // Setup a list of entries to store in the cache. + List<Entry> entries; + for (size_t i = 0; i < 10; ++i) + { + auto data = createRandomBlob(i * 1024); + auto key = SHA1::compute(data->getBufferPointer(), data->getBufferSize()); + entries.add(Entry{ key, data }); + } + + for (size_t i = 0; i < 10; ++i) + { + const auto& entry = entries[i]; + ComPtr<ISlangBlob> data; + + // Try to read an entry. Check that its not found and counts as a miss. + SLANG_CHECK(cache->readEntry(entry.key, data.writeRef()) == SLANG_E_NOT_FOUND); + SLANG_CHECK(cache->getStats().missCount == i + 1); + + // Write the entry. Check that it gets added. + SLANG_CHECK(cache->writeEntry(entry.key, entry.data) == SLANG_OK); + SLANG_CHECK(cache->getStats().entryCount == i + 1); + } + + SLANG_CHECK(cache->getStats().entryCount == 10); + SLANG_CHECK(cache->getStats().hitCount == 0); + SLANG_CHECK(cache->getStats().missCount == 10); + + for (size_t i = 0; i < 10; ++i) + { + const auto& entry = entries[i]; + ComPtr<ISlangBlob> data; + + // Read entries. Check that these are cache hits and return the correct data. + SLANG_CHECK(cache->readEntry(entry.key, data.writeRef()) == SLANG_OK); + SLANG_CHECK(cache->getStats().hitCount == i + 1); + SLANG_CHECK(isBlobEqual(data, entry.data)); + } + + SLANG_CHECK(cache->getStats().entryCount == 10); + SLANG_CHECK(cache->getStats().hitCount == 10); + SLANG_CHECK(cache->getStats().missCount == 10); + + // Clear the cache. Check that entry count is reset. + SLANG_CHECK(cache->clear() == SLANG_OK); + SLANG_CHECK(cache->getStats().entryCount == 0); + SLANG_CHECK(cache->getStats().hitCount == 10); + SLANG_CHECK(cache->getStats().missCount == 10); + + // Reset stats. + cache->resetStats(); + SLANG_CHECK(cache->getStats().entryCount == 0); + SLANG_CHECK(cache->getStats().hitCount == 0); + SLANG_CHECK(cache->getStats().missCount == 0); + + // Check that cache is empty. + for (size_t i = 0; i < 10; ++i) + { + const auto& entry = entries[i]; + ComPtr<ISlangBlob> data; + SLANG_CHECK(cache->readEntry(entry.key, data.writeRef()) == SLANG_E_NOT_FOUND); + } + SLANG_CHECK(cache->getStats().missCount == 10); + } +}; + +// Tests the least-recently-used cache eviction policy. +struct EvictionTest : public PersistentCacheTest +{ + EvictionTest() : PersistentCacheTest(3) {} + + void run() + { + // Setup a list of entries to store in the cache. + List<Entry> entries; + for (size_t i = 0; i < 10; ++i) + { + auto data = createRandomBlob(4096); + auto key = SHA1::compute(data->getBufferPointer(), data->getBufferSize()); + entries.add(Entry{ key, data }); + } + + writeEntry(entries[0]); + writeEntry(entries[1]); + writeEntry(entries[2]); + + SLANG_CHECK(readEntry(entries[0]) == true); + SLANG_CHECK(readEntry(entries[1]) == true); + SLANG_CHECK(readEntry(entries[2]) == true); + + // Evict LRU entry 0. + writeEntry(entries[3]); + SLANG_CHECK(readEntry(entries[0]) == false); + SLANG_CHECK(readEntry(entries[1]) == true); + SLANG_CHECK(readEntry(entries[2]) == true); + SLANG_CHECK(readEntry(entries[3]) == true); + + // Evict LRU entry 1. + writeEntry(entries[4]); + SLANG_CHECK(readEntry(entries[1]) == false); + SLANG_CHECK(readEntry(entries[2]) == true); + SLANG_CHECK(readEntry(entries[3]) == true); + SLANG_CHECK(readEntry(entries[4]) == true); + + // Evict LRU entry 2. + writeEntry(entries[5]); + SLANG_CHECK(readEntry(entries[2]) == false); + SLANG_CHECK(readEntry(entries[3]) == true); + SLANG_CHECK(readEntry(entries[4]) == true); + SLANG_CHECK(readEntry(entries[5]) == true); + + // Evict LRU entry 4. + SLANG_CHECK(readEntry(entries[3]) == true); + writeEntry(entries[6]); + SLANG_CHECK(readEntry(entries[3]) == true); + SLANG_CHECK(readEntry(entries[4]) == false); + SLANG_CHECK(readEntry(entries[5]) == true); + SLANG_CHECK(readEntry(entries[6]) == true); + } +}; + + +// Tests the cache to be robust against various corruptions. +// These can happen if the cache files are manipulated externally. +// The cache might also be corrupted if the application is terminated while writing. +struct CorruptionTest : public PersistentCacheTest +{ + List<Entry> entries; + + template<typename Func> + void testIndexCorruption(Func func, SlangResult expectedReadResult) + { + writeEntry(entries[0]); + SLANG_CHECK(readEntry(entries[0]) == true); + func(); + // We expect a SLANG_E_NOT_FOUND because the cache has an empty index now. + ComPtr<ISlangBlob> data; + SLANG_CHECK(cache->readEntry(entries[0].key, data.writeRef()) == expectedReadResult); + + writeEntry(entries[0]); + SLANG_CHECK(readEntry(entries[0]) == true); + func(); + writeEntry(entries[0]); + SLANG_CHECK(readEntry(entries[0]) == true); + } + + void run() + { + // Setup a list of entries to store in the cache. + for (size_t i = 0; i < 10; ++i) + { + auto data = createRandomBlob(4096); + auto key = SHA1::compute(data->getBufferPointer(), data->getBufferSize()); + entries.add(Entry{ key, data }); + } + + // Test behavior when a cached entry file is removed externally before reading. + writeEntry(entries[0]); + SLANG_CHECK(readEntry(entries[0]) == true); + osFileSystem->remove(getEntryFileName(entries[0]).getBuffer()); + ComPtr<ISlangBlob> data; + // First time we read the entry, we expect a SLANG_E_CANNOT_OPEN because the file is gone. + SLANG_CHECK(cache->readEntry(entries[0].key, data.writeRef()) == SLANG_E_CANNOT_OPEN); + // The next time we read the entry, we expect a SLANG_E_NOT_FOUND because the entry has + // been removed from the cache index. + SLANG_CHECK(cache->readEntry(entries[0].key, data.writeRef()) == SLANG_E_NOT_FOUND); + + // Test behavior when a cached entry file is removed externally before writing. + writeEntry(entries[0]); + SLANG_CHECK(readEntry(entries[0]) == true); + osFileSystem->remove(getEntryFileName(entries[0]).getBuffer()); + writeEntry(entries[0]); + SLANG_CHECK(readEntry(entries[0]) == true); + + // Test behavior when the index file is removed before reading. + writeEntry(entries[0]); + SLANG_CHECK(readEntry(entries[0]) == true); + osFileSystem->remove(getIndexFilename().getBuffer()); + // We expect a SLANG_E_NOT_FOUND because the cache has an empty index now. + SLANG_CHECK(cache->readEntry(entries[0].key, data.writeRef()) == SLANG_E_NOT_FOUND); + + // Test behavior when the index file is removed before writing. + writeEntry(entries[0]); + SLANG_CHECK(readEntry(entries[0]) == true); + osFileSystem->remove(getIndexFilename().getBuffer()); + writeEntry(entries[1]); + SLANG_CHECK(readEntry(entries[1]) == true); + + // Test different corruptions of the index file. + testIndexCorruption( + [this]() + { + osFileSystem->remove(getIndexFilename().getBuffer()); + }, + SLANG_E_NOT_FOUND); + + testIndexCorruption( + [this]() + { + FileStream fs; + fs.init(getIndexFilename(), FileMode::Open, FileAccess::ReadWrite, FileShare::ReadWrite); + fs.write("x", 1); + }, + SLANG_E_INTERNAL_FAIL); + + testIndexCorruption( + [this]() + { + FileStream fs; + fs.init(getIndexFilename(), FileMode::Open, FileAccess::ReadWrite, FileShare::ReadWrite); + fs.seek(SeekOrigin::Start, 4); + uint32_t version = 0xffffffff; + fs.write(&version, sizeof(version)); + }, + SLANG_E_INTERNAL_FAIL); + + testIndexCorruption( + [this]() + { + FileStream fs; + fs.init(getIndexFilename(), FileMode::Open, FileAccess::ReadWrite, FileShare::ReadWrite); + fs.seek(SeekOrigin::Start, 8); + uint32_t count = 0x7fffffff; + fs.write(&count, sizeof(count)); + }, + SLANG_E_INTERNAL_FAIL); + + testIndexCorruption( + [this]() + { + FileStream fs; + fs.init(getIndexFilename(), FileMode::Open, FileAccess::ReadWrite, FileShare::ReadWrite); + fs.seek(SeekOrigin::Start, 8); + uint32_t count = 0; + fs.write(&count, sizeof(count)); + }, + SLANG_E_INTERNAL_FAIL); + + testIndexCorruption( + [this]() + { + FileStream fs; + fs.init(getIndexFilename(), FileMode::Open, FileAccess::ReadWrite, FileShare::ReadWrite); + fs.seek(SeekOrigin::End, 0); + fs.write("x", 1); + }, + SLANG_E_INTERNAL_FAIL); + } +}; + +struct MultiThreadingTest : public PersistentCacheTest +{ + void run() + { + } +}; + + +#undef ENABLE_LOGGING +#undef ENABLE_WRITE_TEST + +#ifdef ENABLE_LOGGING +#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__); fflush(stdout); +#else +#define LOG(fmt, ...) +#endif + +// Stress testing. +// This test spawns a number of threads to do concurrent access to the cache. +// For now this is fairly simple: +// - spawn a number of threads +// - write random entries to the cache concurrenctly (slightly oversubscribe) +// - synchronize +// - read entries from the cache concurretly (test that we get the expected number of hits/misses) +// - synchronize +// - repeat for a number of iterations +struct StressTest : public PersistentCacheTest +{ + // Number of entries to write/read per iteration. + static const uint32_t kEntryCount = 100; + // Number of entries the cache is short for storing one iteration. + static const uint32_t kEntryShortageCount = 10; + // Number of parallel threads to write/read. + static const uint32_t kThreadCount = 4; + // Number of entries to write/read per thread per iteration. + static const uint32_t kBatchCount = kEntryCount / kThreadCount; + // Total number of iterations. + static const uint32_t kIterationCount = 4; + + static_assert(kEntryCount % kThreadCount == 0, "kEntryCount must be divisible by kThreadCount"); + + List<Entry> entries; + + std::atomic<uint32_t> iteration{0}; + std::atomic<uint32_t> entriesWritten{0}; + std::atomic<uint32_t> bytesWritten{0}; + std::atomic<uint32_t> entriesRead{0}; + std::atomic<uint32_t> bytesRead{0}; + std::atomic<uint32_t> readSuccess{0}; + std::thread threads[kThreadCount]; + + Barrier *read_barrier; + Barrier *write_barrier; + + std::mutex mutex; + std::condition_variable conditionVariable; + uint32_t generation{0}; + + StressTest() : PersistentCacheTest(kEntryCount - kEntryShortageCount) {} + + void run() + { + // Setup a list of entries to store in the cache. + for (size_t i = 0; i < kEntryCount * 2; ++i) + { + size_t size = rng.nextInt32InRange(256, 64 * 1024); + auto data = createRandomBlob(size); + auto key = SHA1::compute(data->getBufferPointer(), data->getBufferSize()); + entries.add(Entry{ key, data }); + } + + auto startTime = std::chrono::high_resolution_clock::now(); + + Barrier read_barrier_( + kThreadCount, + []() + { + LOG("Read synchronized\n"); + }); + Barrier write_barrier_( + kThreadCount, + [this](){ + LOG("Write synchronized\n"); +#ifndef ENABLE_WRITE_TEST + SLANG_CHECK(readSuccess == kEntryCount - kEntryShortageCount); + readSuccess.store(0); +#endif + iteration += 1; + }); + + read_barrier = &read_barrier_; + write_barrier = &write_barrier_; + + for (uint32_t threadIndex = 0; threadIndex < kThreadCount; ++threadIndex) + { + threads[threadIndex] = std::thread( + [](StressTest* self, uint32_t threadIndex) + { + LOG("Thread %u: starting\n", threadIndex); + + while (true) + { + // Write to cache. + size_t startIndex = (self->iteration * kEntryCount + (threadIndex * kBatchCount)) % (kEntryCount * 2); + for (size_t i = 0; i < kBatchCount; ++i) + { + const Entry& entry = self->entries[startIndex + i]; +#ifdef ENABLE_WRITE_TEST + self->osFileSystem->saveFileBlob(self->getEntryFileName(entry).getBuffer(), entry.data); +#else + self->writeEntry(entry); +#endif + self->entriesWritten.fetch_add(1); + self->bytesWritten.fetch_add((uint32_t)entry.data->getBufferSize()); + } + + LOG("Thread %u: ended writing (iteration=%u)\n", threadIndex, self->iteration.load()); + + // Synchronize. + self->read_barrier->wait(); + + // Read from cache. + for (size_t i = 0; i < kBatchCount; ++i) + { + const Entry& entry = self->entries[startIndex + i]; +#ifndef ENABLE_WRITE_TEST + if (self->readEntry(entry)) + { + self->readSuccess.fetch_add(1); + self->bytesRead.fetch_add((uint32_t)entry.data->getBufferSize()); + } +#endif + self->entriesRead.fetch_add(1); + } + + LOG("Thread %u: ended reading (iteration=%u)\n", threadIndex, self->iteration.load()); + + // Synchronize. + self->write_barrier->wait(); + + // Terminate. + if (self->iteration >= kIterationCount) + { + LOG("Thread %u: terminates\n", threadIndex); + return; + } + } + }, + this, threadIndex); + } + + for (auto& thread : threads) + { + thread.join(); + } + + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = endTime - startTime; + auto seconds = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() / 1000.0; + + LOG("Total time: %.3fs\n", seconds); + LOG("Total bytes written: %d\n", bytesWritten.load()); + LOG("Write througput: %.3fMB/s\n", (bytesWritten.load() / (1024.0 * 1024.0)) / seconds); + LOG("Total bytes read: %d\n", bytesRead.load()); + } +}; + +SLANG_UNIT_TEST(persistentCacheBasic) +{ + BasicTest test; + test.run(); +} + +SLANG_UNIT_TEST(persistentCacheEviction) +{ + EvictionTest test; + test.run(); +} + +SLANG_UNIT_TEST(persistentCacheCorruption) +{ + CorruptionTest test; + test.run(); +} + +SLANG_UNIT_TEST(persistentCacheMultiThreading) +{ + MultiThreadingTest test; + test.run(); +} + +SLANG_UNIT_TEST(persistentCacheStress) +{ + StressTest test; + test.run(); +} |
