diff options
Diffstat (limited to 'tools/gfx-unit-test/shader-cache-tests.cpp')
| -rw-r--r-- | tools/gfx-unit-test/shader-cache-tests.cpp | 624 |
1 files changed, 624 insertions, 0 deletions
diff --git a/tools/gfx-unit-test/shader-cache-tests.cpp b/tools/gfx-unit-test/shader-cache-tests.cpp new file mode 100644 index 000000000..184b71c06 --- /dev/null +++ b/tools/gfx-unit-test/shader-cache-tests.cpp @@ -0,0 +1,624 @@ +#include "tools/unit-test/slang-unit-test.h" + +#include "slang-gfx.h" +#include "gfx-test-util.h" +#include "tools/gfx-util/shader-cursor.h" +#include "source/core/slang-basic.h" + +#include "source/core/slang-memory-file-system.h" +#include "source/core/slang-file-system.h" + +using namespace gfx; + +namespace gfx_test +{ + + struct BaseShaderCacheTest + { + UnitTestContext* context; + Slang::RenderApiFlag::Enum api; + + ComPtr<IDevice> device; + ComPtr<IPipelineState> pipelineState; + ComPtr<IResourceView> bufferView; + + // diskFileSystem is used to save the test shaders to disk as necessary as + // loadComputeProgram() always loads from the OS file system. cacheFileSystem + // is used to hold the shader cache in memory to avoid needing to manually erase all the + // shader cache entry files between consecutive runs of the test. + ComPtr<ISlangMutableFileSystem> diskFileSystem; + ComPtr<ISlangMutableFileSystem> cacheFileSystem; + + // Simple compute shaders we can pipe to our individual shader files for cache testing + Slang::String contentsA = Slang::String( + R"(uniform RWStructuredBuffer<float> buffer; + + [shader("compute")] + [numthreads(4, 1, 1)] + void computeMain( + uint3 sv_dispatchThreadID : SV_DispatchThreadID) + { + var input = buffer[sv_dispatchThreadID.x]; + buffer[sv_dispatchThreadID.x] = input + 1.0f; + })"); + + Slang::String contentsB = Slang::String( + R"(uniform RWStructuredBuffer<float> buffer; + + [shader("compute")] + [numthreads(4, 1, 1)] + void computeMain( + uint3 sv_dispatchThreadID : SV_DispatchThreadID) + { + var input = buffer[sv_dispatchThreadID.x]; + buffer[sv_dispatchThreadID.x] = input + 2.0f; + })"); + + Slang::String contentsC = Slang::String( + R"(uniform RWStructuredBuffer<float> buffer; + + [shader("compute")] + [numthreads(4, 1, 1)] + void computeMain( + uint3 sv_dispatchThreadID : SV_DispatchThreadID) + { + var input = buffer[sv_dispatchThreadID.x]; + buffer[sv_dispatchThreadID.x] = input + 3.0f; + })"); + + void createRequiredResources() + { + const int numberCount = 4; + float initialData[] = { 0.0f, 1.0f, 2.0f, 3.0f }; + IBufferResource::Desc bufferDesc = {}; + bufferDesc.sizeInBytes = numberCount * sizeof(float); + bufferDesc.format = gfx::Format::Unknown; + bufferDesc.elementSize = sizeof(float); + bufferDesc.allowedStates = ResourceStateSet( + ResourceState::ShaderResource, + ResourceState::UnorderedAccess, + ResourceState::CopyDestination, + ResourceState::CopySource); + bufferDesc.defaultState = ResourceState::UnorderedAccess; + bufferDesc.memoryType = MemoryType::DeviceLocal; + + ComPtr<IBufferResource> numbersBuffer; + GFX_CHECK_CALL_ABORT(device->createBufferResource( + bufferDesc, + (void*)initialData, + numbersBuffer.writeRef())); + + IResourceView::Desc viewDesc = {}; + viewDesc.type = IResourceView::Type::UnorderedAccess; + viewDesc.format = Format::Unknown; + GFX_CHECK_CALL_ABORT( + device->createBufferView(numbersBuffer, nullptr, viewDesc, bufferView.writeRef())); + } + + void freeOldResources() + { + bufferView = nullptr; + pipelineState = nullptr; + device = 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() + { + freeOldResources(); + device = createTestingDevice(context, api, cacheFileSystem); + } + + void init(ComPtr<IDevice> device, UnitTestContext* context) + { + this->device = device; + this->context = context; + switch (device->getDeviceInfo().deviceType) + { + case DeviceType::DirectX11: + api = Slang::RenderApiFlag::D3D11; + break; + case DeviceType::DirectX12: + api = Slang::RenderApiFlag::D3D12; + break; + case DeviceType::Vulkan: + api = Slang::RenderApiFlag::Vulkan; + break; + case DeviceType::CPU: + api = Slang::RenderApiFlag::CPU; + break; + case DeviceType::CUDA: + api = Slang::RenderApiFlag::CUDA; + break; + case DeviceType::OpenGl: + api = Slang::RenderApiFlag::OpenGl; + break; + default: + SLANG_IGNORE_TEST + } + + cacheFileSystem = new Slang::MemoryFileSystem(); + diskFileSystem = Slang::OSFileSystem::getMutableSingleton(); + diskFileSystem = new Slang::RelativeFileSystem(diskFileSystem, "tools/gfx-unit-test"); + } + + void submitGPUWork() + { + Slang::ComPtr<ITransientResourceHeap> transientHeap; + ITransientResourceHeap::Desc transientHeapDesc = {}; + transientHeapDesc.constantBufferSize = 4096; + GFX_CHECK_CALL_ABORT( + device->createTransientResourceHeap(transientHeapDesc, transientHeap.writeRef())); + + ICommandQueue::Desc queueDesc = { ICommandQueue::QueueType::Graphics }; + auto queue = device->createCommandQueue(queueDesc); + + auto commandBuffer = transientHeap->createCommandBuffer(); + auto encoder = commandBuffer->encodeComputeCommands(); + + auto rootObject = encoder->bindPipeline(pipelineState); + + ShaderCursor rootCursor(rootObject); + // Bind buffer view to the entry point. + rootCursor.getPath("buffer").setResource(bufferView); + + encoder->dispatchCompute(1, 1, 1); + encoder->endEncoding(); + commandBuffer->close(); + queue->executeCommandBuffer(commandBuffer); + queue->waitOnHost(); + } + }; + + // One shader file on disk, all modifications are done to the same file + struct SingleEntryShaderCache : BaseShaderCacheTest + { + void generateNewPipelineState(Slang::String shaderContents) + { + diskFileSystem->saveFile("shader-cache-shader.slang", shaderContents.getBuffer(), shaderContents.getLength()); + + ComPtr<IShaderProgram> shaderProgram; + slang::ProgramLayout* slangReflection; + GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "shader-cache-shader", "computeMain", slangReflection)); + + ComputePipelineStateDesc pipelineDesc = {}; + pipelineDesc.program = shaderProgram.get(); + GFX_CHECK_CALL_ABORT( + device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); + } + + void run() + { + ComPtr<IShaderCacheStatistics> shaderCacheStats; + + // Due to needing a workaround to prevent loading old, outdated modules, we need to + // recreate the device between each segment of the test. However, we need to maintain the + // same cache filesystem for the duration of the test, so the device is immediately recreated + // to ensure we can pass the filesystem all the way through. + // + // 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. + 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); + } + }; + + // Several shader files on disk, modifications may be done to any file + struct MultipleEntryShaderCache : BaseShaderCacheTest + { + void modifyShaderA(Slang::String shaderContents) + { + diskFileSystem->saveFile("shader-cache-shader-A.slang", shaderContents.getBuffer(), shaderContents.getLength()); + } + + void modifyShaderB(Slang::String shaderContents) + { + diskFileSystem->saveFile("shader-cache-shader-B.slang", shaderContents.getBuffer(), shaderContents.getLength()); + } + + void modifyShaderC(Slang::String shaderContents) + { + diskFileSystem->saveFile("shader-cache-shader-C.slang", shaderContents.getBuffer(), shaderContents.getLength()); + } + + void generateNewPipelineState(GfxIndex shaderIndex) + { + ComPtr<IShaderProgram> shaderProgram; + slang::ProgramLayout* slangReflection; + char* shaderFilename; + switch (shaderIndex) + { + case 0: + shaderFilename = "shader-cache-shader-A"; + break; + case 1: + shaderFilename = "shader-cache-shader-B"; + break; + case 2: + shaderFilename = "shader-cache-shader-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())); + } + + void checkAllCacheEntries() + { + generateNewPipelineState(0); + submitGPUWork(); + generateNewPipelineState(1); + submitGPUWork(); + generateNewPipelineState(2); + submitGPUWork(); + } + + void run() + { + ComPtr<IShaderCacheStatistics> shaderCacheStats; + + // Due to needing a workaround to prevent loading old, outdated modules, we need to + // recreate the device between each segment of the test. However, we need to maintain the + // same cache filesystem for the duration of the test, so the device is immediately recreated + // to ensure we can pass the filesystem all the way through. + // + // 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. + 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); + } + }; + + // One shader file on disk containing several entry points, no modifications are made to the file + struct MultipleEntryPointShader : BaseShaderCacheTest + { + void generateNewPipelineState(GfxIndex shaderIndex) + { + ComPtr<IShaderProgram> shaderProgram; + slang::ProgramLayout* slangReflection; + 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)); + + ComputePipelineStateDesc pipelineDesc = {}; + pipelineDesc.program = shaderProgram.get(); + GFX_CHECK_CALL_ABORT( + device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); + } + + void run() + { + ComPtr<IShaderCacheStatistics> shaderCacheStats; + + // Due to needing a workaround to prevent loading old, outdated modules, we need to + // recreate the device between each segment of the test. However, we need to maintain the + // same cache filesystem for the duration of the test, so the device is immediately recreated + // to ensure we can pass the filesystem all the way through. + // + // 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. + 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); + } + }; + + // 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 + { + Slang::String importedContentsA = Slang::String( + R"(struct TestFunction + { + void simpleElementAdd(RWStructuredBuffer<float> buffer, uint index) + { + var input = buffer[index]; + buffer[index] = input + 1.0f; + } + };)"); + + Slang::String importedContentsB = Slang::String( + R"(struct TestFunction + { + void simpleElementAdd(RWStructuredBuffer<float> buffer, uint index) + { + var input = buffer[index]; + buffer[index] = input + 2.0f; + } + };)"); + + Slang::String importFile = Slang::String( + R"(import imported; + + uniform RWStructuredBuffer<float> buffer; + + [shader("compute")] + [numthreads(4, 1, 1)] + void computeMain( + uint3 sv_dispatchThreadID : SV_DispatchThreadID) + { + TestFunction test; + for (uint i = 0; i < 4; ++i) + { + test.simpleElementAdd(buffer, i); + } + })"); + + Slang::String includeFile = Slang::String( + R"(#include "imported.slang" + + uniform RWStructuredBuffer<float> buffer; + + [shader("compute")] + [numthreads(4, 1, 1)] + void computeMain( + uint3 sv_dispatchThreadID : SV_DispatchThreadID) + { + TestFunction test; + for (uint i = 0; i < 4; ++i) + { + test.simpleElementAdd(buffer, i); + } + })"); + + void initializeFiles() + { + diskFileSystem->saveFile("imported.slang", importedContentsA.getBuffer(), importedContentsA.getLength()); + diskFileSystem->saveFile("importing-shader-cache-shader.slang", importFile.getBuffer(), importFile.getLength()); + } + + void modifyImportedFile(Slang::String importedContents) + { + diskFileSystem->saveFile("imported.slang", importedContents.getBuffer(), importedContents.getLength()); + } + + void changeImportToInclude() + { + diskFileSystem->saveFile("importing-shader-cache-shader.slang", includeFile.getBuffer(), includeFile.getLength()); + } + + void generateNewPipelineState() + { + ComPtr<IShaderProgram> shaderProgram; + slang::ProgramLayout* slangReflection; + GFX_CHECK_CALL_ABORT(loadComputeProgram(device, shaderProgram, "importing-shader-cache-shader", "computeMain", slangReflection)); + + ComputePipelineStateDesc pipelineDesc = {}; + pipelineDesc.program = shaderProgram.get(); + GFX_CHECK_CALL_ABORT( + device->createComputePipelineState(pipelineDesc, pipelineState.writeRef())); + } + + void run() + { + ComPtr<IShaderCacheStatistics> shaderCacheStats; + + // Due to needing a workaround to prevent loading old, outdated modules, we need to + // recreate the device between each segment of the test. However, we need to maintain the + // same cache filesystem for the duration of the test, so the device is immediately recreated + // to ensure we can pass the filesystem all the way through. + // + // 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. + 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); + } + }; + + template <typename T> + void shaderCacheTestImpl(ComPtr<IDevice> device, UnitTestContext* context) + { + T test; + test.init(device, context); + test.run(); + } + + SLANG_UNIT_TEST(singleEntryShaderCacheD3D12) + { + runTestImpl(shaderCacheTestImpl<SingleEntryShaderCache>, unitTestContext, Slang::RenderApiFlag::D3D12); + } + + SLANG_UNIT_TEST(singleEntryShaderCacheVulkan) + { + runTestImpl(shaderCacheTestImpl<SingleEntryShaderCache>, unitTestContext, Slang::RenderApiFlag::Vulkan); + } + + SLANG_UNIT_TEST(multipleEntryShaderCacheD3D12) + { + runTestImpl(shaderCacheTestImpl<MultipleEntryShaderCache>, unitTestContext, Slang::RenderApiFlag::D3D12); + } + + SLANG_UNIT_TEST(multipleEntryShaderCacheVulkan) + { + runTestImpl(shaderCacheTestImpl<MultipleEntryShaderCache>, unitTestContext, Slang::RenderApiFlag::Vulkan); + } + + SLANG_UNIT_TEST(multipleEntryPointShaderCacheD3D12) + { + runTestImpl(shaderCacheTestImpl<MultipleEntryPointShader>, unitTestContext, Slang::RenderApiFlag::D3D12); + } + + SLANG_UNIT_TEST(multipleEntryPointShaderCacheVulkan) + { + runTestImpl(shaderCacheTestImpl<MultipleEntryPointShader>, unitTestContext, Slang::RenderApiFlag::Vulkan); + } + + SLANG_UNIT_TEST(shaderFileImportsShaderCacheD3D12) + { + runTestImpl(shaderCacheTestImpl<ShaderFileImportsShaderCache>, unitTestContext, Slang::RenderApiFlag::D3D12); + } + + SLANG_UNIT_TEST(shaderFileImportsShaderCacheVulkan) + { + runTestImpl(shaderCacheTestImpl<ShaderFileImportsShaderCache>, unitTestContext, Slang::RenderApiFlag::Vulkan); + } +} |
