yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Gangzheng TongConvert gfx unit tests and examples to use slang-rhi (#7577)43d0c2100

master
39.6 KiB1137 linesraw
1#if 0
2// Duplicated: this is identical to slang-rhi\tests\test-shader-cache.cpp
3// This file uses gfx-specific shader cache functionality (IShaderCache) 
4// that has been replaced with a different caching system (IPersistentCache) in slang-rhi.
5
6#include "core/slang-basic.h"
7#include "core/slang-file-system.h"
8#include "core/slang-io.h"
9#include "core/slang-process.h"
10#include "core/slang-string-util.h"
11#include "gfx-test-texture-util.h"
12#include "gfx-test-util.h"
13#include "slang-gfx.h"
14#include "slang-rhi/shader-cursor.h"
15#include "unit-test/slang-unit-test.h"
16
17using namespace gfx;
18using namespace Slang;
19
20namespace gfx_test
21{
22// Base class for shader cache tests.
23// Slang currently does not allow reloading shaders from modified sources.
24// Because of this, the tests recreate a GFX device for each test step,
25// allowing to modify shader sources in between.
26struct ShaderCacheTest
27{
28    UnitTestContext* context;
29    Slang::RenderApiFlag::Enum api;
30
31    String testDirectory;
32    String cacheDirectory;
33
34    ComPtr<ISlangMutableFileSystem> diskFileSystem;
35
36    IDevice::ShaderCacheDesc shaderCacheDesc = {};
37
38    ComPtr<IDevice> device;
39    ComPtr<IShaderCache> shaderCache;
40    ComPtr<IPipelineState> pipelineState;
41    ComPtr<IBufferResource> bufferResource;
42    ComPtr<IResourceView> bufferView;
43
44    String computeShaderA = String(
45        R"(
46            [shader("compute")]
47            [numthreads(4, 1, 1)]
48            void main(
49                uint3 sv_dispatchThreadID : SV_DispatchThreadID,
50                uniform RWStructuredBuffer<float> buffer)
51            {
52                var input = buffer[sv_dispatchThreadID.x];
53                buffer[sv_dispatchThreadID.x] = input + 1.0f;
54            }
55            )");
56
57    String computeShaderB = String(
58        R"(
59            [shader("compute")]
60            [numthreads(4, 1, 1)]
61            void main(
62                uint3 sv_dispatchThreadID : SV_DispatchThreadID,
63                uniform RWStructuredBuffer<float> buffer)
64            {
65                var input = buffer[sv_dispatchThreadID.x];
66                buffer[sv_dispatchThreadID.x] = input + 2.0f;
67            }
68            )");
69
70    String computeShaderC = String(
71        R"(
72            [shader("compute")]
73            [numthreads(4, 1, 1)]
74            void main(
75                uint3 sv_dispatchThreadID : SV_DispatchThreadID,
76                uniform RWStructuredBuffer<float> buffer)
77            {
78                var input = buffer[sv_dispatchThreadID.x];
79                buffer[sv_dispatchThreadID.x] = input + 3.0f;
80            }
81            )");
82
83
84    void removeDirectory(const String& directory)
85    {
86        auto osFileSystem = OSFileSystem::getMutableSingleton();
87
88        struct Context
89        {
90            ISlangMutableFileSystem* fileSystem;
91            const String& directory;
92        } context{osFileSystem, directory};
93
94        osFileSystem->enumeratePathContents(
95            directory.getBuffer(),
96            [](SlangPathType pathType, const char* fileName, void* userData)
97            {
98                struct Context* context = static_cast<Context*>(userData);
99                if (pathType == SlangPathType::SLANG_PATH_TYPE_FILE)
100                {
101                    String path = Path::simplify(context->directory + "/" + fileName);
102                    context->fileSystem->remove(path.getBuffer());
103                }
104            },
105            &context);
106
107        osFileSystem->remove(directory.getBuffer());
108    }
109
110    void writeShader(const String& source, const String& fileName)
111    {
112        diskFileSystem->saveFile(fileName.getBuffer(), source.getBuffer(), source.getLength());
113    }
114
115    void init(UnitTestContext* context, Slang::RenderApiFlag::Enum api)
116    {
117        this->context = context;
118        this->api = api;
119        testDirectory = Path::simplify(
120            Path::getParentDirectory(Path::getExecutablePath()) + "/shader-cache-test" +
121            String(Process::getId()));
122        cacheDirectory = Path::simplify(testDirectory + "/cache" + String(Process::getId()));
123
124        // Cleanup if there are stale files from a previously aborted test.
125        removeDirectory(cacheDirectory);
126        removeDirectory(testDirectory);
127
128        Path::createDirectory(testDirectory);
129        diskFileSystem = new RelativeFileSystem(OSFileSystem::getMutableSingleton(), testDirectory);
130        shaderCacheDesc.shaderCachePath = cacheDirectory.getBuffer();
131    }
132
133    void cleanup()
134    {
135        removeDirectory(cacheDirectory);
136        removeDirectory(testDirectory);
137    }
138
139    template<typename Func>
140    void runStep(Func func)
141    {
142        List<const char*> additionalSearchPaths;
143        additionalSearchPaths.add(testDirectory.getBuffer());
144
145        runTestImpl(
146            [this, func](IDevice* device, UnitTestContext* ctx)
147            {
148                this->device = device;
149                SLANG_CHECK_ABORT(SLANG_SUCCEEDED(device->queryInterface(
150                    SLANG_UUID_IShaderCache,
151                    (void**)this->shaderCache.writeRef())));
152                func();
153                this->device = nullptr;
154                this->shaderCache = nullptr;
155            },
156            context,
157            api,
158            additionalSearchPaths,
159            shaderCacheDesc);
160    }
161
162    void createComputeResources()
163    {
164        const int numberCount = 4;
165        float initialData[] = {0.0f, 1.0f, 2.0f, 3.0f};
166        IBufferResource::Desc bufferDesc = {};
167        bufferDesc.sizeInBytes = numberCount * sizeof(float);
168        bufferDesc.format = Format::Unknown;
169        bufferDesc.elementSize = sizeof(float);
170        bufferDesc.allowedStates = ResourceStateSet(
171            ResourceState::ShaderResource,
172            ResourceState::UnorderedAccess,
173            ResourceState::CopyDestination,
174            ResourceState::CopySource);
175        bufferDesc.defaultState = ResourceState::UnorderedAccess;
176        bufferDesc.memoryType = MemoryType::DeviceLocal;
177
178        GFX_CHECK_CALL_ABORT(device->createBufferResource(
179            bufferDesc,
180            (void*)initialData,
181            bufferResource.writeRef()));
182
183        IResourceView::Desc viewDesc = {};
184        viewDesc.type = IResourceView::Type::UnorderedAccess;
185        viewDesc.format = Format::Unknown;
186        GFX_CHECK_CALL_ABORT(
187            device->createBufferView(bufferResource, nullptr, viewDesc, bufferView.writeRef()));
188    }
189
190    void freeComputeResources()
191    {
192        bufferResource = nullptr;
193        bufferView = nullptr;
194        pipelineState = nullptr;
195    }
196
197    void createComputePipeline(const char* moduleName, const char* entryPointName)
198    {
199        ComPtr<IShaderProgram> shaderProgram;
200        slang::ProgramLayout* slangReflection;
201        GFX_CHECK_CALL_ABORT(
202            loadComputeProgram(device, shaderProgram, moduleName, entryPointName, slangReflection));
203
204        ComputePipelineStateDesc pipelineDesc = {};
205        pipelineDesc.program = shaderProgram.get();
206        GFX_CHECK_CALL_ABORT(
207            device->createComputePipelineState(pipelineDesc, pipelineState.writeRef()));
208    }
209
210    void createComputePipeline(Slang::String shaderSource)
211    {
212        ComPtr<IShaderProgram> shaderProgram;
213        GFX_CHECK_CALL_ABORT(loadComputeProgramFromSource(device, shaderProgram, shaderSource));
214
215        ComputePipelineStateDesc pipelineDesc = {};
216        pipelineDesc.program = shaderProgram.get();
217        GFX_CHECK_CALL_ABORT(
218            device->createComputePipelineState(pipelineDesc, pipelineState.writeRef()));
219    }
220
221    void dispatchComputePipeline()
222    {
223        ComPtr<ITransientResourceHeap> transientHeap;
224        ITransientResourceHeap::Desc transientHeapDesc = {};
225        transientHeapDesc.constantBufferSize = 4096;
226        GFX_CHECK_CALL_ABORT(
227            device->createTransientResourceHeap(transientHeapDesc, transientHeap.writeRef()));
228
229        ICommandQueue::Desc queueDesc = {ICommandQueue::QueueType::Graphics};
230        auto queue = device->createCommandQueue(queueDesc);
231
232        auto commandBuffer = transientHeap->createCommandBuffer();
233        auto encoder = commandBuffer->encodeComputeCommands();
234
235        auto rootObject = encoder->bindPipeline(pipelineState);
236
237        // Bind buffer view to the entry point.
238        ShaderCursor entryPointCursor(rootObject->getEntryPoint(0));
239        entryPointCursor.getPath("buffer").setResource(bufferView);
240
241        encoder->dispatchCompute(4, 1, 1);
242        encoder->endEncoding();
243        commandBuffer->close();
244        queue->executeCommandBuffer(commandBuffer);
245        queue->waitOnHost();
246    }
247
248    bool checkOutput(const List<float>& expectedOutput)
249    {
250        ComPtr<ISlangBlob> bufferBlob;
251        device->readBufferResource(bufferResource, 0, 4 * sizeof(float), bufferBlob.writeRef());
252        SLANG_CHECK_ABORT(
253            bufferBlob && bufferBlob->getBufferSize() == expectedOutput.getCount() * sizeof(float));
254        return ::memcmp(
255                   bufferBlob->getBufferPointer(),
256                   expectedOutput.getBuffer(),
257                   bufferBlob->getBufferSize()) == 0;
258    }
259
260    bool runComputePipeline(
261        const char* moduleName,
262        const char* entryPointName,
263        const List<float>& expectedOutput)
264    {
265        createComputeResources();
266        createComputePipeline(moduleName, entryPointName);
267        dispatchComputePipeline();
268        bool hasExpectedOutput = checkOutput(expectedOutput);
269        SLANG_CHECK(hasExpectedOutput);
270        freeComputeResources();
271        return hasExpectedOutput;
272    }
273
274    bool runComputePipeline(Slang::String shaderSource, const List<float>& expectedOutput)
275    {
276        createComputeResources();
277        createComputePipeline(shaderSource);
278        dispatchComputePipeline();
279        bool hasExpectedOutput = checkOutput(expectedOutput);
280        SLANG_CHECK(hasExpectedOutput);
281        freeComputeResources();
282        return hasExpectedOutput;
283    }
284
285    ShaderCacheStats getStats()
286    {
287        SLANG_ASSERT(shaderCache);
288        ShaderCacheStats stats;
289        shaderCache->getShaderCacheStats(&stats);
290        return stats;
291    }
292
293    void run(UnitTestContext* context, Slang::RenderApiFlag::Enum api)
294    {
295        init(context, api);
296        runTests();
297        cleanup();
298    }
299
300    virtual void runTests() = 0;
301};
302
303// Basic shader cache test using 3 different shader files stored on disk.
304struct ShaderCacheSourceFile : ShaderCacheTest
305{
306    void runTests()
307    {
308        // Write shader source files.
309        writeShader(computeShaderA, "shader-cache-tmp-a.slang");
310        writeShader(computeShaderB, "shader-cache-tmp-b.slang");
311        writeShader(computeShaderC, "shader-cache-tmp-c.slang");
312
313        // Cache is cold and we expect 3 misses.
314        runStep(
315            [this]()
316            {
317                SLANG_CHECK(runComputePipeline("shader-cache-tmp-a", "main", {1.f, 2.f, 3.f, 4.f}));
318                SLANG_CHECK(runComputePipeline("shader-cache-tmp-b", "main", {2.f, 3.f, 4.f, 5.f}));
319                SLANG_CHECK(runComputePipeline("shader-cache-tmp-c", "main", {3.f, 4.f, 5.f, 6.f}));
320
321                SLANG_CHECK(getStats().missCount == 3);
322                SLANG_CHECK(getStats().hitCount == 0);
323                SLANG_CHECK(getStats().entryCount == 3);
324            });
325
326        // Cache is hot and we expect 3 hits.
327        runStep(
328            [this]()
329            {
330                SLANG_CHECK(runComputePipeline("shader-cache-tmp-a", "main", {1.f, 2.f, 3.f, 4.f}));
331                SLANG_CHECK(runComputePipeline("shader-cache-tmp-b", "main", {2.f, 3.f, 4.f, 5.f}));
332                SLANG_CHECK(runComputePipeline("shader-cache-tmp-c", "main", {3.f, 4.f, 5.f, 6.f}));
333
334                SLANG_CHECK(getStats().missCount == 0);
335                SLANG_CHECK(getStats().hitCount == 3);
336                SLANG_CHECK(getStats().entryCount == 3);
337            });
338
339        // Write shader source files, all rotated by one.
340        writeShader(computeShaderA, "shader-cache-tmp-b.slang");
341        writeShader(computeShaderB, "shader-cache-tmp-c.slang");
342        writeShader(computeShaderC, "shader-cache-tmp-a.slang");
343
344        // Cache is cold again and we expect 3 misses.
345        runStep(
346            [this]()
347            {
348                SLANG_CHECK(runComputePipeline("shader-cache-tmp-b", "main", {1.f, 2.f, 3.f, 4.f}));
349                SLANG_CHECK(runComputePipeline("shader-cache-tmp-c", "main", {2.f, 3.f, 4.f, 5.f}));
350                SLANG_CHECK(runComputePipeline("shader-cache-tmp-a", "main", {3.f, 4.f, 5.f, 6.f}));
351
352                SLANG_CHECK(getStats().missCount == 3);
353                SLANG_CHECK(getStats().hitCount == 0);
354                SLANG_CHECK(getStats().entryCount == 6);
355            });
356
357        // Cache is hot again and we expect 3 hits.
358        runStep(
359            [this]()
360            {
361                SLANG_CHECK(runComputePipeline("shader-cache-tmp-b", "main", {1.f, 2.f, 3.f, 4.f}));
362                SLANG_CHECK(runComputePipeline("shader-cache-tmp-c", "main", {2.f, 3.f, 4.f, 5.f}));
363                SLANG_CHECK(runComputePipeline("shader-cache-tmp-a", "main", {3.f, 4.f, 5.f, 6.f}));
364
365                SLANG_CHECK(getStats().missCount == 0);
366                SLANG_CHECK(getStats().hitCount == 3);
367                SLANG_CHECK(getStats().entryCount == 6);
368            });
369    }
370};
371
372// Test caching of shaders that are compiled from source strings instead of files.
373struct ShaderCacheTestSourceString : ShaderCacheTest
374{
375    void runTests()
376    {
377        // Cache is cold and we expect 3 misses.
378        runStep(
379            [this]()
380            {
381                SLANG_CHECK(runComputePipeline(computeShaderA, {1.f, 2.f, 3.f, 4.f}));
382                SLANG_CHECK(runComputePipeline(computeShaderB, {2.f, 3.f, 4.f, 5.f}));
383                SLANG_CHECK(runComputePipeline(computeShaderC, {3.f, 4.f, 5.f, 6.f}));
384
385                SLANG_CHECK(getStats().missCount == 3);
386                SLANG_CHECK(getStats().hitCount == 0);
387                SLANG_CHECK(getStats().entryCount == 3);
388            });
389
390        // Cache is hot and we expect 3 hits.
391        runStep(
392            [this]()
393            {
394                SLANG_CHECK(runComputePipeline(computeShaderA, {1.f, 2.f, 3.f, 4.f}));
395                SLANG_CHECK(runComputePipeline(computeShaderB, {2.f, 3.f, 4.f, 5.f}));
396                SLANG_CHECK(runComputePipeline(computeShaderC, {3.f, 4.f, 5.f, 6.f}));
397
398                SLANG_CHECK(getStats().missCount == 0);
399                SLANG_CHECK(getStats().hitCount == 3);
400                SLANG_CHECK(getStats().entryCount == 3);
401            });
402    }
403};
404
405// Test one shader file on disk with multiple entry points.
406struct ShaderCacheTestEntryPoint : ShaderCacheTest
407{
408    void runTests()
409    {
410        // Cache is cold and we expect 3 misses, one for each entry point.
411        runStep(
412            [this]()
413            {
414                SLANG_CHECK(runComputePipeline(
415                    "shader-cache-multiple-entry-points",
416                    "computeA",
417                    {1.f, 2.f, 3.f, 4.f}));
418                SLANG_CHECK(runComputePipeline(
419                    "shader-cache-multiple-entry-points",
420                    "computeB",
421                    {2.f, 3.f, 4.f, 5.f}));
422                SLANG_CHECK(runComputePipeline(
423                    "shader-cache-multiple-entry-points",
424                    "computeC",
425                    {3.f, 4.f, 5.f, 6.f}));
426
427                SLANG_CHECK(getStats().missCount == 3);
428                SLANG_CHECK(getStats().hitCount == 0);
429                SLANG_CHECK(getStats().entryCount == 3);
430            });
431
432        // Cache is hot and we expect 3 hits.
433        runStep(
434            [this]()
435            {
436                SLANG_CHECK(runComputePipeline(
437                    "shader-cache-multiple-entry-points",
438                    "computeA",
439                    {1.f, 2.f, 3.f, 4.f}));
440                SLANG_CHECK(runComputePipeline(
441                    "shader-cache-multiple-entry-points",
442                    "computeB",
443                    {2.f, 3.f, 4.f, 5.f}));
444                SLANG_CHECK(runComputePipeline(
445                    "shader-cache-multiple-entry-points",
446                    "computeC",
447                    {3.f, 4.f, 5.f, 6.f}));
448
449                SLANG_CHECK(getStats().missCount == 0);
450                SLANG_CHECK(getStats().hitCount == 3);
451                SLANG_CHECK(getStats().entryCount == 3);
452            });
453    }
454};
455
456// Test cache invalidation due to an import/include file being changed on disk.
457struct ShaderCacheTestImportInclude : ShaderCacheTest
458{
459    String importedContentsA = String(
460        R"(
461            public void processElement(RWStructuredBuffer<float> buffer, uint index)
462            {
463                var input = buffer[index];
464                buffer[index] = input + 1.0f;
465            }
466            )");
467
468    String importedContentsB = String(
469        R"(
470            public void processElement(RWStructuredBuffer<float> buffer, uint index)
471            {
472                var input = buffer[index];
473                buffer[index] = input + 2.0f;
474            }
475            )");
476
477    String importFile = String(
478        R"(
479            import shader_cache_tmp_imported;
480            
481            [shader("compute")]
482            [numthreads(4, 1, 1)]
483            void main(
484                uint3 sv_dispatchThreadID : SV_DispatchThreadID,
485                uniform RWStructuredBuffer<float> buffer)
486            {
487                processElement(buffer, sv_dispatchThreadID.x);
488            }
489            )");
490
491    String includeFile = String(
492        R"(
493            #include "shader-cache-tmp-imported.slang"
494
495            [shader("compute")]
496            [numthreads(4, 1, 1)]
497            void main(
498                uint3 sv_dispatchThreadID : SV_DispatchThreadID,
499                uniform RWStructuredBuffer<float> buffer)
500            {
501                processElement(buffer, sv_dispatchThreadID.x);
502            })");
503
504    void runTests()
505    {
506        // Write shader source files.
507        writeShader(importedContentsA, "shader-cache-tmp-imported.slang");
508        writeShader(importFile, "shader-cache-tmp-import.slang");
509        writeShader(includeFile, "shader-cache-tmp-include.slang");
510
511        // Cache is cold and we expect 2 misses.
512        runStep(
513            [this]()
514            {
515                SLANG_CHECK(
516                    runComputePipeline("shader-cache-tmp-import", "main", {1.f, 2.f, 3.f, 4.f}));
517                SLANG_CHECK(
518                    runComputePipeline("shader-cache-tmp-include", "main", {1.f, 2.f, 3.f, 4.f}));
519
520                SLANG_CHECK(getStats().missCount == 2);
521                SLANG_CHECK(getStats().hitCount == 0);
522                SLANG_CHECK(getStats().entryCount == 2);
523            });
524
525        // Cache is hot and we expect 2 hits.
526        runStep(
527            [this]()
528            {
529                SLANG_CHECK(
530                    runComputePipeline("shader-cache-tmp-import", "main", {1.f, 2.f, 3.f, 4.f}));
531                SLANG_CHECK(
532                    runComputePipeline("shader-cache-tmp-include", "main", {1.f, 2.f, 3.f, 4.f}));
533
534                SLANG_CHECK(getStats().missCount == 0);
535                SLANG_CHECK(getStats().hitCount == 2);
536                SLANG_CHECK(getStats().entryCount == 2);
537            });
538
539        // Change content of imported/included shader file.
540        writeShader(importedContentsB, "shader-cache-tmp-imported.slang");
541
542        // Cache is cold and we expect 2 misses.
543        runStep(
544            [this]()
545            {
546                SLANG_CHECK(
547                    runComputePipeline("shader-cache-tmp-import", "main", {2.f, 3.f, 4.f, 5.f}));
548                SLANG_CHECK(
549                    runComputePipeline("shader-cache-tmp-include", "main", {2.f, 3.f, 4.f, 5.f}));
550
551                SLANG_CHECK(getStats().missCount == 2);
552                SLANG_CHECK(getStats().hitCount == 0);
553                SLANG_CHECK(getStats().entryCount == 4);
554            });
555
556        // Cache is hot and we expect 2 hits.
557        runStep(
558            [this]()
559            {
560                SLANG_CHECK(
561                    runComputePipeline("shader-cache-tmp-import", "main", {2.f, 3.f, 4.f, 5.f}));
562                SLANG_CHECK(
563                    runComputePipeline("shader-cache-tmp-include", "main", {2.f, 3.f, 4.f, 5.f}));
564
565                SLANG_CHECK(getStats().missCount == 0);
566                SLANG_CHECK(getStats().hitCount == 2);
567                SLANG_CHECK(getStats().entryCount == 4);
568            });
569    }
570};
571
572// One shader featuring multiple kinds of shader objects that can be bound.
573struct ShaderCacheTestSpecialization : ShaderCacheTest
574{
575    slang::ProgramLayout* slangReflection;
576
577    void createComputePipeline()
578    {
579        ComPtr<IShaderProgram> shaderProgram;
580
581        GFX_CHECK_CALL_ABORT(loadComputeProgram(
582            device,
583            shaderProgram,
584            "shader-cache-specialization",
585            "computeMain",
586            slangReflection));
587
588        ComputePipelineStateDesc pipelineDesc = {};
589        pipelineDesc.program = shaderProgram.get();
590        GFX_CHECK_CALL_ABORT(
591            device->createComputePipelineState(pipelineDesc, pipelineState.writeRef()));
592    }
593
594    void dispatchComputePipeline(const char* transformerTypeName)
595    {
596        Slang::ComPtr<ITransientResourceHeap> transientHeap;
597        ITransientResourceHeap::Desc transientHeapDesc = {};
598        transientHeapDesc.constantBufferSize = 4096;
599        GFX_CHECK_CALL_ABORT(
600            device->createTransientResourceHeap(transientHeapDesc, transientHeap.writeRef()));
601
602        ICommandQueue::Desc queueDesc = {ICommandQueue::QueueType::Graphics};
603        auto queue = device->createCommandQueue(queueDesc);
604
605        auto commandBuffer = transientHeap->createCommandBuffer();
606        auto encoder = commandBuffer->encodeComputeCommands();
607
608        auto rootObject = encoder->bindPipeline(pipelineState);
609
610        Slang::ComPtr<IShaderObject> transformer;
611        slang::TypeReflection* transformerType =
612            slangReflection->findTypeByName(transformerTypeName);
613        GFX_CHECK_CALL_ABORT(device->createShaderObject(
614            transformerType,
615            ShaderObjectContainerType::None,
616            transformer.writeRef()));
617
618        float c = 5.f;
619        ShaderCursor(transformer).getPath("c").setData(&c, sizeof(float));
620
621        ShaderCursor entryPointCursor(rootObject->getEntryPoint(0));
622        entryPointCursor.getPath("buffer").setResource(bufferView);
623        entryPointCursor.getPath("transformer").setObject(transformer);
624
625        encoder->dispatchCompute(1, 1, 1);
626        encoder->endEncoding();
627        commandBuffer->close();
628        queue->executeCommandBuffer(commandBuffer);
629        queue->waitOnHost();
630    }
631
632    bool runComputePipeline(const char* transformerTypeName, const List<float>& expectedOutput)
633    {
634        createComputeResources();
635        createComputePipeline();
636        dispatchComputePipeline(transformerTypeName);
637        bool hasExpectedOutput = checkOutput(expectedOutput);
638        SLANG_CHECK(hasExpectedOutput);
639        freeComputeResources();
640        return hasExpectedOutput;
641    }
642
643    void runTests()
644    {
645        // Cache is cold and we expect 2 misses.
646        runStep(
647            [this]()
648            {
649                SLANG_CHECK(runComputePipeline("AddTransformer", {5.f, 6.f, 7.f, 8.f}));
650                SLANG_CHECK(runComputePipeline("MulTransformer", {0.f, 5.f, 10.f, 15.f}));
651
652                SLANG_CHECK(getStats().missCount == 2);
653                SLANG_CHECK(getStats().hitCount == 0);
654                SLANG_CHECK(getStats().entryCount == 2);
655            });
656
657        // Cache is hot and we expect 2 hits.
658        runStep(
659            [this]()
660            {
661                SLANG_CHECK(runComputePipeline("AddTransformer", {5.f, 6.f, 7.f, 8.f}));
662                SLANG_CHECK(runComputePipeline("MulTransformer", {0.f, 5.f, 10.f, 15.f}));
663
664                SLANG_CHECK(getStats().missCount == 0);
665                SLANG_CHECK(getStats().hitCount == 2);
666                SLANG_CHECK(getStats().entryCount == 2);
667            });
668    }
669};
670
671struct ShaderCacheTestEviction : ShaderCacheTest
672{
673    void runTests()
674    {
675        shaderCacheDesc.maxEntryCount = 2;
676
677        // Load shader A & B. Cache is cold and we expect 2 misses.
678        runStep(
679            [this]()
680            {
681                SLANG_CHECK(runComputePipeline(computeShaderA, {1.f, 2.f, 3.f, 4.f}));
682                SLANG_CHECK(runComputePipeline(computeShaderB, {2.f, 3.f, 4.f, 5.f}));
683
684                SLANG_CHECK(getStats().missCount == 2);
685                SLANG_CHECK(getStats().hitCount == 0);
686                SLANG_CHECK(getStats().entryCount == 2);
687            });
688
689        // Load shader A & B. Cache is hot and we expect 2 hits.
690        runStep(
691            [this]()
692            {
693                SLANG_CHECK(runComputePipeline(computeShaderA, {1.f, 2.f, 3.f, 4.f}));
694                SLANG_CHECK(runComputePipeline(computeShaderB, {2.f, 3.f, 4.f, 5.f}));
695
696                SLANG_CHECK(getStats().missCount == 0);
697                SLANG_CHECK(getStats().hitCount == 2);
698                SLANG_CHECK(getStats().entryCount == 2);
699            });
700
701        // Load shader C. Cache is cold and we expect 1 miss.
702        // This will evict the least frequently used entry (shader A).
703        // We expect 2 entries in the cache (shader B & C).
704        runStep(
705            [this]()
706            {
707                SLANG_CHECK(runComputePipeline(computeShaderC, {3.f, 4.f, 5.f, 6.f}));
708
709                SLANG_CHECK(getStats().missCount == 1);
710                SLANG_CHECK(getStats().hitCount == 0);
711                SLANG_CHECK(getStats().entryCount == 2);
712            });
713
714        // Load shader C. Cache is hot and we expect 1 hit.
715        runStep(
716            [this]()
717            {
718                SLANG_CHECK(runComputePipeline(computeShaderC, {3.f, 4.f, 5.f, 6.f}));
719
720                SLANG_CHECK(getStats().missCount == 0);
721                SLANG_CHECK(getStats().hitCount == 1);
722                SLANG_CHECK(getStats().entryCount == 2);
723            });
724
725        // Load shader B. Cache is hot and we expect 1 hit.
726        runStep(
727            [this]()
728            {
729                SLANG_CHECK(runComputePipeline(computeShaderB, {2.f, 3.f, 4.f, 5.f}));
730
731                SLANG_CHECK(getStats().missCount == 0);
732                SLANG_CHECK(getStats().hitCount == 1);
733                SLANG_CHECK(getStats().entryCount == 2);
734            });
735
736        // Load shader A. Cache is cold and we expect 1 miss.
737        runStep(
738            [this]()
739            {
740                SLANG_CHECK(runComputePipeline(computeShaderA, {1.f, 2.f, 3.f, 4.f}));
741
742                SLANG_CHECK(getStats().missCount == 1);
743                SLANG_CHECK(getStats().hitCount == 0);
744                SLANG_CHECK(getStats().entryCount == 2);
745            });
746    }
747};
748
749// Similar to ShaderCacheTestEntryPoint but with a source file containing a vertex and fragment
750// shader.
751struct ShaderCacheTestGraphics : ShaderCacheTest
752{
753    struct Vertex
754    {
755        float position[3];
756    };
757
758    static const int kWidth = 256;
759    static const int kHeight = 256;
760    static const Format format = Format::R32G32B32A32_FLOAT;
761
762    ComPtr<IBufferResource> vertexBuffer;
763    ComPtr<ITextureResource> colorBuffer;
764    ComPtr<IInputLayout> inputLayout;
765    ComPtr<IFramebufferLayout> framebufferLayout;
766    ComPtr<IRenderPassLayout> renderPass;
767    ComPtr<IFramebuffer> framebuffer;
768
769    ComPtr<IBufferResource> createVertexBuffer(IDevice* device)
770    {
771        const Vertex vertices[] = {
772            {0, 0, 0.5},
773            {1, 0, 0.5},
774            {0, 1, 0.5},
775        };
776
777        IBufferResource::Desc vertexBufferDesc;
778        vertexBufferDesc.type = IResource::Type::Buffer;
779        vertexBufferDesc.sizeInBytes = sizeof(vertices);
780        vertexBufferDesc.defaultState = ResourceState::VertexBuffer;
781        vertexBufferDesc.allowedStates = ResourceState::VertexBuffer;
782        ComPtr<IBufferResource> vertexBuffer =
783            device->createBufferResource(vertexBufferDesc, vertices);
784        SLANG_CHECK_ABORT(vertexBuffer != nullptr);
785        return vertexBuffer;
786    }
787
788    ComPtr<ITextureResource> createColorBuffer(IDevice* device)
789    {
790        gfx::ITextureResource::Desc colorBufferDesc;
791        colorBufferDesc.type = IResource::Type::Texture2D;
792        colorBufferDesc.size.width = kWidth;
793        colorBufferDesc.size.height = kHeight;
794        colorBufferDesc.size.depth = 1;
795        colorBufferDesc.numMipLevels = 1;
796        colorBufferDesc.format = format;
797        colorBufferDesc.defaultState = ResourceState::RenderTarget;
798        colorBufferDesc.allowedStates = {ResourceState::RenderTarget, ResourceState::CopySource};
799        ComPtr<ITextureResource> colorBuffer =
800            device->createTextureResource(colorBufferDesc, nullptr);
801        SLANG_CHECK_ABORT(colorBuffer != nullptr);
802        return colorBuffer;
803    }
804
805    void createGraphicsResources()
806    {
807        VertexStreamDesc vertexStreams[] = {
808            {sizeof(Vertex), InputSlotClass::PerVertex, 0},
809        };
810
811        InputElementDesc inputElements[] = {
812            // Vertex buffer data
813            {"POSITION", 0, Format::R32G32B32_FLOAT, offsetof(Vertex, position), 0},
814        };
815        IInputLayout::Desc inputLayoutDesc = {};
816        inputLayoutDesc.inputElementCount = SLANG_COUNT_OF(inputElements);
817        inputLayoutDesc.inputElements = inputElements;
818        inputLayoutDesc.vertexStreamCount = SLANG_COUNT_OF(vertexStreams);
819        inputLayoutDesc.vertexStreams = vertexStreams;
820        inputLayout = device->createInputLayout(inputLayoutDesc);
821        SLANG_CHECK_ABORT(inputLayout != nullptr);
822
823        vertexBuffer = createVertexBuffer(device);
824        colorBuffer = createColorBuffer(device);
825
826        IFramebufferLayout::TargetLayout targetLayout;
827        targetLayout.format = format;
828        targetLayout.sampleCount = 1;
829
830        IFramebufferLayout::Desc framebufferLayoutDesc;
831        framebufferLayoutDesc.renderTargetCount = 1;
832        framebufferLayoutDesc.renderTargets = &targetLayout;
833        framebufferLayout = device->createFramebufferLayout(framebufferLayoutDesc);
834        SLANG_CHECK_ABORT(framebufferLayout != nullptr);
835
836        IRenderPassLayout::Desc renderPassDesc = {};
837        renderPassDesc.framebufferLayout = framebufferLayout;
838        renderPassDesc.renderTargetCount = 1;
839        IRenderPassLayout::TargetAccessDesc renderTargetAccess = {};
840        renderTargetAccess.loadOp = IRenderPassLayout::TargetLoadOp::Clear;
841        renderTargetAccess.storeOp = IRenderPassLayout::TargetStoreOp::Store;
842        renderTargetAccess.initialState = ResourceState::RenderTarget;
843        renderTargetAccess.finalState = ResourceState::CopySource;
844        renderPassDesc.renderTargetAccess = &renderTargetAccess;
845        GFX_CHECK_CALL_ABORT(device->createRenderPassLayout(renderPassDesc, renderPass.writeRef()));
846
847        gfx::IResourceView::Desc colorBufferViewDesc;
848        memset(&colorBufferViewDesc, 0, sizeof(colorBufferViewDesc));
849        colorBufferViewDesc.format = format;
850        colorBufferViewDesc.renderTarget.shape = gfx::IResource::Type::Texture2D;
851        colorBufferViewDesc.type = gfx::IResourceView::Type::RenderTarget;
852        auto rtv = device->createTextureView(colorBuffer, colorBufferViewDesc);
853
854        gfx::IFramebuffer::Desc framebufferDesc;
855        framebufferDesc.renderTargetCount = 1;
856        framebufferDesc.depthStencilView = nullptr;
857        framebufferDesc.renderTargetViews = rtv.readRef();
858        framebufferDesc.layout = framebufferLayout;
859        GFX_CHECK_CALL_ABORT(device->createFramebuffer(framebufferDesc, framebuffer.writeRef()));
860    }
861
862    void freeGraphicsResources()
863    {
864        inputLayout = nullptr;
865        framebufferLayout = nullptr;
866        renderPass = nullptr;
867        framebuffer = nullptr;
868        vertexBuffer = nullptr;
869        colorBuffer = nullptr;
870        pipelineState = nullptr;
871    }
872
873    void createGraphicsPipeline()
874    {
875        ComPtr<IShaderProgram> shaderProgram;
876        slang::ProgramLayout* slangReflection;
877        GFX_CHECK_CALL_ABORT(loadGraphicsProgram(
878            device,
879            shaderProgram,
880            "shader-cache-graphics",
881            "vertexMain",
882            "fragmentMain",
883            slangReflection));
884
885        GraphicsPipelineStateDesc pipelineDesc = {};
886        pipelineDesc.program = shaderProgram.get();
887        pipelineDesc.inputLayout = inputLayout;
888        pipelineDesc.framebufferLayout = framebufferLayout;
889        pipelineDesc.depthStencil.depthTestEnable = false;
890        pipelineDesc.depthStencil.depthWriteEnable = false;
891        GFX_CHECK_CALL_ABORT(
892            device->createGraphicsPipelineState(pipelineDesc, pipelineState.writeRef()));
893    }
894
895    void dispatchGraphicsPipeline()
896    {
897        ComPtr<ITransientResourceHeap> transientHeap;
898        ITransientResourceHeap::Desc transientHeapDesc = {};
899        transientHeapDesc.constantBufferSize = 4096;
900        GFX_CHECK_CALL_ABORT(
901            device->createTransientResourceHeap(transientHeapDesc, transientHeap.writeRef()));
902
903        ICommandQueue::Desc queueDesc = {ICommandQueue::QueueType::Graphics};
904        auto queue = device->createCommandQueue(queueDesc);
905        auto commandBuffer = transientHeap->createCommandBuffer();
906
907        auto encoder = commandBuffer->encodeRenderCommands(renderPass, framebuffer);
908        auto rootObject = encoder->bindPipeline(pipelineState);
909
910        gfx::Viewport viewport = {};
911        viewport.maxZ = 1.0f;
912        viewport.extentX = (float)kWidth;
913        viewport.extentY = (float)kHeight;
914        encoder->setViewportAndScissor(viewport);
915
916        encoder->setVertexBuffer(0, vertexBuffer);
917        encoder->setPrimitiveTopology(PrimitiveTopology::TriangleList);
918
919        encoder->draw(3);
920        encoder->endEncoding();
921        commandBuffer->close();
922        queue->executeCommandBuffer(commandBuffer);
923        queue->waitOnHost();
924    }
925
926    void runGraphicsPipeline()
927    {
928        createGraphicsResources();
929        createGraphicsPipeline();
930        dispatchGraphicsPipeline();
931        freeGraphicsResources();
932    }
933
934    void runTests()
935    {
936        // Cache is cold and we expect 2 misses (2 entry points).
937        runStep(
938            [this]()
939            {
940                runGraphicsPipeline();
941
942                SLANG_CHECK(getStats().missCount == 2);
943                SLANG_CHECK(getStats().hitCount == 0);
944                SLANG_CHECK(getStats().entryCount == 2);
945            });
946
947        // Cache is hot and we expect 2 hits.
948        runStep(
949            [this]()
950            {
951                runGraphicsPipeline();
952
953                SLANG_CHECK(getStats().missCount == 0);
954                SLANG_CHECK(getStats().hitCount == 2);
955                SLANG_CHECK(getStats().entryCount == 2);
956            });
957    }
958};
959
960// Similar to ShaderCacheTestGraphics but with two separate shader files for the vertex and fragment
961// shaders.
962struct ShaderCacheTestGraphicsSplit : ShaderCacheTestGraphics
963{
964    void createGraphicsPipeline()
965    {
966        ComPtr<slang::ISession> slangSession;
967        GFX_CHECK_CALL_ABORT(device->getSlangSession(slangSession.writeRef()));
968        slang::IModule* vertexModule = slangSession->loadModule("shader-cache-graphics-vertex");
969        SLANG_CHECK_ABORT(vertexModule);
970        slang::IModule* fragmentModule = slangSession->loadModule("shader-cache-graphics-fragment");
971        SLANG_CHECK_ABORT(fragmentModule);
972
973        ComPtr<slang::IEntryPoint> vertexEntryPoint;
974        GFX_CHECK_CALL_ABORT(
975            vertexModule->findEntryPointByName("main", vertexEntryPoint.writeRef()));
976
977        ComPtr<slang::IEntryPoint> fragmentEntryPoint;
978        GFX_CHECK_CALL_ABORT(
979            fragmentModule->findEntryPointByName("main", fragmentEntryPoint.writeRef()));
980
981        Slang::List<slang::IComponentType*> componentTypes;
982        componentTypes.add(vertexModule);
983        componentTypes.add(fragmentModule);
984
985        Slang::ComPtr<slang::IComponentType> composedProgram;
986        GFX_CHECK_CALL_ABORT(slangSession->createCompositeComponentType(
987            componentTypes.getBuffer(),
988            componentTypes.getCount(),
989            composedProgram.writeRef()));
990
991        slang::ProgramLayout* slangReflection = composedProgram->getLayout();
992
993        Slang::List<slang::IComponentType*> entryPoints;
994        entryPoints.add(vertexEntryPoint);
995        entryPoints.add(fragmentEntryPoint);
996
997        gfx::IShaderProgram::Desc programDesc = {};
998        programDesc.slangGlobalScope = composedProgram.get();
999        programDesc.linkingStyle = gfx::IShaderProgram::LinkingStyle::SeparateEntryPointCompilation;
1000        programDesc.entryPointCount = 2;
1001        programDesc.slangEntryPoints = entryPoints.getBuffer();
1002
1003        ComPtr<IShaderProgram> shaderProgram = device->createProgram(programDesc);
1004
1005        GraphicsPipelineStateDesc pipelineDesc = {};
1006        pipelineDesc.program = shaderProgram.get();
1007        pipelineDesc.inputLayout = inputLayout;
1008        pipelineDesc.framebufferLayout = framebufferLayout;
1009        pipelineDesc.depthStencil.depthTestEnable = false;
1010        pipelineDesc.depthStencil.depthWriteEnable = false;
1011        GFX_CHECK_CALL_ABORT(
1012            device->createGraphicsPipelineState(pipelineDesc, pipelineState.writeRef()));
1013    }
1014
1015    void runGraphicsPipeline()
1016    {
1017        createGraphicsResources();
1018        createGraphicsPipeline();
1019        dispatchGraphicsPipeline();
1020        freeGraphicsResources();
1021    }
1022
1023    void runTests()
1024    {
1025        // Cache is cold and we expect 2 misses (2 entry points).
1026        runStep(
1027            [this]()
1028            {
1029                runGraphicsPipeline();
1030
1031                SLANG_CHECK(getStats().missCount == 2);
1032                SLANG_CHECK(getStats().hitCount == 0);
1033                SLANG_CHECK(getStats().entryCount == 2);
1034            });
1035
1036        // Cache is hot and we expect 2 hits.
1037        runStep(
1038            [this]()
1039            {
1040                runGraphicsPipeline();
1041
1042                SLANG_CHECK(getStats().missCount == 0);
1043                SLANG_CHECK(getStats().hitCount == 2);
1044                SLANG_CHECK(getStats().entryCount == 2);
1045            });
1046    }
1047};
1048
1049template<typename T>
1050void runTest(UnitTestContext* context, Slang::RenderApiFlag::Enum api)
1051{
1052    T test;
1053    test.run(context, api);
1054}
1055
1056SLANG_UNIT_TEST(shaderCacheSourceFileD3D12)
1057{
1058    runTest<ShaderCacheSourceFile>(unitTestContext, Slang::RenderApiFlag::D3D12);
1059}
1060
1061SLANG_UNIT_TEST(shaderCacheSourceFileVulkan)
1062{
1063    runTest<ShaderCacheSourceFile>(unitTestContext, Slang::RenderApiFlag::Vulkan);
1064}
1065
1066SLANG_UNIT_TEST(shaderCacheSourceStringD3D12)
1067{
1068    runTest<ShaderCacheTestSourceString>(unitTestContext, Slang::RenderApiFlag::D3D12);
1069}
1070
1071SLANG_UNIT_TEST(shaderCacheSourceStringVulkan)
1072{
1073    runTest<ShaderCacheTestSourceString>(unitTestContext, Slang::RenderApiFlag::Vulkan);
1074}
1075
1076SLANG_UNIT_TEST(shaderCacheEntryPointD3D12)
1077{
1078    runTest<ShaderCacheTestEntryPoint>(unitTestContext, Slang::RenderApiFlag::D3D12);
1079}
1080
1081SLANG_UNIT_TEST(shaderCacheEntryPointVulkan)
1082{
1083    runTest<ShaderCacheTestEntryPoint>(unitTestContext, Slang::RenderApiFlag::Vulkan);
1084}
1085
1086SLANG_UNIT_TEST(shaderCacheImportIncludeD3D12)
1087{
1088    runTest<ShaderCacheTestImportInclude>(unitTestContext, Slang::RenderApiFlag::D3D12);
1089}
1090
1091SLANG_UNIT_TEST(shaderCacheImportIncludeVulkan)
1092{
1093    runTest<ShaderCacheTestImportInclude>(unitTestContext, Slang::RenderApiFlag::Vulkan);
1094}
1095
1096SLANG_UNIT_TEST(shaderCacheSpecializationD3D12)
1097{
1098    runTest<ShaderCacheTestSpecialization>(unitTestContext, Slang::RenderApiFlag::D3D12);
1099}
1100
1101SLANG_UNIT_TEST(shaderCacheSpecializationVulkan)
1102{
1103    runTest<ShaderCacheTestSpecialization>(unitTestContext, Slang::RenderApiFlag::Vulkan);
1104}
1105
1106SLANG_UNIT_TEST(shaderCacheEvictionD3D12)
1107{
1108    runTest<ShaderCacheTestEviction>(unitTestContext, Slang::RenderApiFlag::D3D12);
1109}
1110
1111SLANG_UNIT_TEST(shaderCacheEvictionVulkan)
1112{
1113    runTest<ShaderCacheTestEviction>(unitTestContext, Slang::RenderApiFlag::Vulkan);
1114}
1115
1116SLANG_UNIT_TEST(shaderCacheGraphicsD3D12)
1117{
1118    runTest<ShaderCacheTestGraphics>(unitTestContext, Slang::RenderApiFlag::D3D12);
1119}
1120
1121SLANG_UNIT_TEST(shaderCacheGraphicsVulkan)
1122{
1123    runTest<ShaderCacheTestGraphics>(unitTestContext, Slang::RenderApiFlag::Vulkan);
1124}
1125
1126SLANG_UNIT_TEST(shaderCacheGraphicsSplitD3D12)
1127{
1128    runTest<ShaderCacheTestGraphicsSplit>(unitTestContext, Slang::RenderApiFlag::D3D12);
1129}
1130
1131SLANG_UNIT_TEST(shaderCacheGraphicsSplitVulkan)
1132{
1133    runTest<ShaderCacheTestGraphicsSplit>(unitTestContext, Slang::RenderApiFlag::Vulkan);
1134}
1135} // namespace gfx_test
1136
1137#endif