summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/gfx-unit-test/gfx-test-util.cpp12
-rw-r--r--tools/gfx-unit-test/gfx-test-util.h4
-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.slang68
-rw-r--r--tools/gfx-unit-test/shader-cache-tests.cpp1449
-rw-r--r--tools/gfx/gfx.slang24
-rw-r--r--tools/gfx/persistent-shader-cache.cpp316
-rw-r--r--tools/gfx/persistent-shader-cache.h99
-rw-r--r--tools/gfx/renderer-shared.cpp94
-rw-r--r--tools/gfx/renderer-shared.h27
-rw-r--r--tools/slang-unit-test/unit-test-lock-file.cpp4
-rw-r--r--tools/slang-unit-test/unit-test-persistent-cache.cpp629
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();
+}