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
20.7 KiB592 linesraw
1// main.cpp
2
3#include "../../source/core/slang-io.h"
4#include "GFSDK_Aftermath.h"
5#include "GFSDK_Aftermath_GpuCrashDump.h"
6#include "core/slang-basic.h"
7#include "examples/example-base/example-base.h"
8#include "platform/window.h"
9#include "slang-com-ptr.h"
10#include "slang.h"
11
12#include <slang-rhi.h>
13#include <slang-rhi/shader-cursor.h>
14
15using namespace rhi;
16using namespace Slang;
17
18static const ExampleResources resourceBase("nv-aftermath-example");
19
20// This example is based on the "triangle" sample.
21//
22// This examples purpose is to show how to use the aftermath SDK to capture
23// a crash dump.
24//
25// * [nsight aftermath](https://developer.nvidia.com/nsight-aftermath)
26//
27// In addition it uses obfuscation and source maps to allow source level
28// debugging via aftermath even with obfuscation.
29//
30// * [obfuscation](https://github.com/shader-slang/slang/blob/master/docs/user-guide/a1-03-obfuscation.md)
31// * [source map](https://github.com/source-map/source-map-spec)
32
33struct Vertex
34{
35    float position[3];
36    float color[3];
37};
38
39static const int kVertexCount = 3;
40static const Vertex kVertexData[kVertexCount] = {
41    {{0, 0, 0.5}, {1, 0, 0}},
42    {{0, 1, 0.5}, {0, 0, 1}},
43    {{1, 0, 0.5}, {0, 1, 0}},
44};
45
46struct AftermathCrashExample : public WindowedAppBase
47{
48    void diagnoseIfNeeded(slang::IBlob* diagnosticsBlob);
49
50    Result loadShaderProgram(IDevice* device, IShaderProgram** outProgram);
51
52    virtual void renderFrame(ITexture* texture) override;
53
54    void onAftermathCrash(const void* data, const uint32_t dataSizeInBytes);
55
56    void onAftermathDebugInfo(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize);
57
58    void onAftermathCrashDescription(PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription description);
59
60    void onAftermathMarker(const void* pMarker, void** resolvedMarkerData, uint32_t* markerSize);
61
62    ComPtr<IRenderPipeline> m_renderPipeline;
63    ComPtr<IBuffer> m_vertexBuffer;
64
65    /// A counter such that we can make aftermath dump file names unique
66    std::atomic<int> m_uniqueId = 0;
67
68    Slang::Result initialize()
69    {
70        // Defer shader debug information callbacks until an actual GPU crash dump
71        // is generated. Increases memory footprint.
72        const uint32_t aftermathFeatureFlags =
73            GFSDK_Aftermath_GpuCrashDumpFeatureFlags_DeferDebugInfoCallbacks;
74
75        // As per docs must be called before any device is created
76        GFSDK_Aftermath_EnableGpuCrashDumps(
77            GFSDK_Aftermath_Version_API,
78            GFSDK_Aftermath_GpuCrashDumpWatchedApiFlags_DX |
79                GFSDK_Aftermath_GpuCrashDumpWatchedApiFlags_Vulkan,
80            aftermathFeatureFlags,
81            _crashCallback,
82            _debugInfoCallback,
83            _crashDescriptionCallback,
84            _markerCallback,
85            this);
86
87        SLANG_RETURN_ON_FAIL(initializeBase("autodiff-texture", 1024, 768, DeviceType::Default));
88
89
90        // We will create objects needed to configure the "input assembler"
91        // (IA) stage of the pipeline.
92        //
93        // First, we create an input layout:
94        //
95        InputElementDesc inputElements[] = {
96            {"POSITION", 0, Format::RGB32Float, offsetof(Vertex, position)},
97            {"COLOR", 0, Format::RGB32Float, offsetof(Vertex, color)},
98        };
99        auto inputLayout = gDevice->createInputLayout(sizeof(Vertex), &inputElements[0], 2);
100        if (!inputLayout)
101            return SLANG_FAIL;
102
103        // Next we allocate a vertex buffer for our pre-initialized
104        // vertex data.
105        //
106        BufferDesc vertexBufferDesc;
107        vertexBufferDesc.size = kVertexCount * sizeof(Vertex);
108        vertexBufferDesc.usage = BufferUsage::VertexBuffer;
109        vertexBufferDesc.defaultState = ResourceState::VertexBuffer;
110        m_vertexBuffer = gDevice->createBuffer(vertexBufferDesc, &kVertexData[0]);
111        if (!m_vertexBuffer)
112            return SLANG_FAIL;
113
114        // Now we will use our `loadShaderProgram` function to load
115        // the code from `shaders.slang` into the graphics API.
116        //
117        ComPtr<IShaderProgram> shaderProgram;
118        SLANG_RETURN_ON_FAIL(loadShaderProgram(device, shaderProgram.writeRef()));
119
120        // Following the D3D12/Vulkan style of API, we need a pipeline state object
121        // (PSO) to encapsulate the configuration of the overall graphics pipeline.
122        //
123        ColorTargetDesc colorTarget;
124        colorTarget.format = Format::RGBA8Unorm;
125        RenderPipelineDesc desc;
126        desc.inputLayout = inputLayout;
127        desc.program = shaderProgram;
128        desc.targetCount = 1;
129        desc.targets = &colorTarget;
130        desc.depthStencil.depthTestEnable = false;
131        desc.depthStencil.depthWriteEnable = false;
132        desc.primitiveTopology = PrimitiveTopology::TriangleList;
133        auto pipelineState = gDevice->createRenderPipeline(desc);
134        if (!pipelineState)
135            return SLANG_FAIL;
136
137        m_renderPipeline = pipelineState;
138
139        return SLANG_OK;
140    }
141};
142
143void AftermathCrashExample::diagnoseIfNeeded(slang::IBlob* diagnosticsBlob)
144{
145    if (diagnosticsBlob != nullptr)
146    {
147        printf("%s", (const char*)diagnosticsBlob->getBufferPointer());
148    }
149}
150
151void AftermathCrashExample::onAftermathCrash(const void* data, const uint32_t dataSizeInBytes)
152{
153    // NOTE! This method can be called from *any* thread.
154    const auto id = m_uniqueId++;
155
156    // Dump out as a file
157    Slang::StringBuilder filename;
158    filename << "aftermath-dump-" << id << ".bin";
159
160    File::writeAllBytes(filename, data, dataSizeInBytes);
161
162    // SLANG_BREAKPOINT(0);
163}
164
165void AftermathCrashExample::onAftermathDebugInfo(
166    const void* gpuCrashDump,
167    const uint32_t gpuCrashDumpSize)
168{
169    const auto id = m_uniqueId++;
170
171    // Dump out as a file
172    Slang::StringBuilder filename;
173    filename << "aftermath-debug-info-" << id << ".bin";
174
175    File::writeAllBytes(filename, gpuCrashDump, gpuCrashDumpSize);
176}
177
178void AftermathCrashExample::onAftermathCrashDescription(
179    PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription description)
180{
181    // Ignore for now
182}
183
184void AftermathCrashExample::onAftermathMarker(
185    const void* marker,
186    void** resolvedMarkerData,
187    uint32_t* markerSize)
188{
189    // Ignore for now
190}
191
192struct FileSystemEntry
193{
194    SlangPathType type; ///< The type of the entry
195    String path;        ///< The path to the entr
196};
197
198struct CompileProduct
199{
200    String fileName;         ///< The filename to write the compile product out to
201    ComPtr<ISlangBlob> blob; ///< A blob holding the products contents
202};
203
204/* Currently the mechanism to access the contents of a compilation that might consist of many
205products is through representing the contents as a "file system".
206
207The file system is just a somewhat convenient/simple in memory representation of the compilation
208products.
209
210This function transverses the file system and adds everything found into outEntries.
211*/
212static SlangResult _findFileSystemContents(
213    ISlangFileSystemExt* fileSystem,
214    const char* rootPath,
215    List<FileSystemEntry>& outEntries)
216{
217    {
218        SlangPathType type;
219        SLANG_RETURN_ON_FAIL(fileSystem->getPathType(rootPath, &type));
220        outEntries.add(FileSystemEntry{type, rootPath});
221    }
222
223    // A context used to hold state, when using enumeratePathContents
224    struct Context
225    {
226        List<FileSystemEntry>& entries; // The entries to be accumulated to
227        String path;                    // The path being enumerated
228    };
229
230    for (Index i = outEntries.getCount() - 1; i < outEntries.getCount(); ++i)
231    {
232        const auto& entry = outEntries[i];
233
234        // If it's a directory we want to traverse it's contents
235        if (entry.type == SLANG_PATH_TYPE_DIRECTORY)
236        {
237            Context context{outEntries, entry.path};
238
239            fileSystem->enumeratePathContents(
240                entry.path.getBuffer(),
241                [](SlangPathType pathType, const char* name, void* userData) -> void
242                {
243                    Context* context = reinterpret_cast<Context*>(userData);
244
245                    const String path = Path::simplify(Path::combine(context->path, name));
246
247                    context->entries.add({pathType, path});
248                },
249                &context);
250        }
251    }
252
253    return SLANG_OK;
254}
255
256/* This function takes a compile results file system, and finds items that should be written out.
257
258This is somewhat complicated because the names of products from different compilations might have
259the same names. So a "prefix" is passed in, and for files that don't have unique names, they are
260uniqified via the prefix.
261
262The same product may appear in multiple compilations, for example obfuscated source maps so a
263product is not added if there is already a product with the same name */
264static SlangResult _addCompileProducts(
265    ISlangFileSystemExt* fileSystem,
266    const char* prefix,
267    List<CompileProduct>& ioProducts)
268{
269    List<FileSystemEntry> fileSystemEntries;
270    SLANG_RETURN_ON_FAIL(_findFileSystemContents(fileSystem, ".", fileSystemEntries));
271
272    for (const auto& fileSystemEntry : fileSystemEntries)
273    {
274        if (fileSystemEntry.type != SLANG_PATH_TYPE_FILE)
275        {
276            continue;
277        }
278
279        const auto ext = Path::getPathExt(fileSystemEntry.path);
280
281        String outFileName;
282
283        // Some filenames need special handling, and their names are already unique
284        // Others will be the same between differen fileSystem that represent the
285        // compilation products.
286        //
287        // Source maps that are obfuscated are unique.
288        {
289            String inFileName = Path::getFileNameWithoutExt(fileSystemEntry.path);
290
291            // If it's an obfuscated source map, it's name is already unique (it includes the hash)
292            const bool isUniqueName =
293                (ext == toSlice("map") && inFileName.endsWith(toSlice("-obfuscated")));
294
295            StringBuilder buf;
296            // If it's not a uniquename make it unique via the prefix
297            if (!isUniqueName)
298            {
299                // Uniquify with the prefix
300                buf << prefix << "-";
301            }
302
303            buf << inFileName << "." << ext;
304            outFileName = buf;
305        }
306
307        // If we have an output filename
308        if (outFileName.getLength())
309        {
310            // And that filename isn't already used
311            if (ioProducts.findFirstIndex(
312                    [&](const CompileProduct& product) -> bool
313                    { return product.fileName == outFileName; }) < 0)
314            {
315                ComPtr<ISlangBlob> blob;
316                SLANG_RETURN_ON_FAIL(
317                    fileSystem->loadFile(fileSystemEntry.path.getBuffer(), blob.writeRef()));
318
319                // Add to the results
320                ioProducts.add(CompileProduct{outFileName, blob});
321            }
322        }
323    }
324
325    return SLANG_OK;
326}
327
328Result AftermathCrashExample::loadShaderProgram(IDevice* device, IShaderProgram** outProgram)
329{
330    ComPtr<slang::ISession> slangSession;
331    slangSession = gDevice->getSlangSession();
332
333    // This is a little bit of a work around.
334    //
335    // We want to set some options that are only available
336    // via processCommandLineArguments, but we need a request to be able to set them up
337    // The setting actually sets the parameters on the Linkage, so they will be used for the later
338    // actual compilation
339    {
340        ComPtr<slang::ICompileRequest> request;
341
342        SLANG_RETURN_ON_FAIL(slangSession->createCompileRequest(request.writeRef()));
343
344        // Turn on obfuscation
345        //
346        // Turns on source map as the line directive, this will lead to an "emit source map"
347        // and no #line directives in generated source.
348        //
349        // It isn't necessary to use the "source-map" line directive mode, and just use
350        // #line directives, and have source locations to obfuscated source file directly embedded.
351        //
352        // To do this replace the line below with
353        //
354        // ```
355        // const char* args[] = { "-obfuscate" };
356        // ```
357        const char* args[] = {"-obfuscate", "-line-directive-mode", "source-map"};
358
359        request->processCommandLineArguments(args, SLANG_COUNT_OF(args));
360
361        // Enable debug info
362        request->setDebugInfoLevel(SLANG_DEBUG_INFO_LEVEL_MAXIMAL);
363    }
364
365    ComPtr<slang::IBlob> diagnosticsBlob;
366    Slang::String path = resourceBase.resolveResource("shaders.slang");
367    slang::IModule* module = slangSession->loadModule(path.getBuffer(), diagnosticsBlob.writeRef());
368    diagnoseIfNeeded(diagnosticsBlob);
369    if (!module)
370        return SLANG_FAIL;
371
372    // Find the entry points
373    ComPtr<slang::IEntryPoint> vertexEntryPoint;
374    SLANG_RETURN_ON_FAIL(module->findEntryPointByName("vertexMain", vertexEntryPoint.writeRef()));
375    //
376    ComPtr<slang::IEntryPoint> fragmentEntryPoint;
377    SLANG_RETURN_ON_FAIL(
378        module->findEntryPointByName("fragmentMain", fragmentEntryPoint.writeRef()));
379
380    // At this point we have a few different Slang API objects that represent
381    // pieces of our code: `module`, `vertexEntryPoint`, and `fragmentEntryPoint`.
382    //
383    // A single Slang module could contain many different entry points (e.g.,
384    // four vertex entry points, three fragment entry points, and two compute
385    // shaders), and before we try to generate output code for our target API
386    // we need to identify which entry points we plan to use together.
387    //
388    // Modules and entry points are both examples of *component types* in the
389    // Slang API. The API also provides a way to build a *composite* out of
390    // other pieces, and that is what we are going to do with our module
391    // and entry points.
392    //
393    Slang::List<slang::IComponentType*> componentTypes;
394    componentTypes.add(module);
395
396    // Later on when we go to extract compiled kernel code for our vertex
397    // and fragment shaders, we will need to make use of their order within
398    // the composition, so we will record the relative ordering of the entry
399    // points here as we add them.
400    int entryPointCount = 0;
401    int vertexEntryPointIndex = entryPointCount++;
402    componentTypes.add(vertexEntryPoint);
403
404    int fragmentEntryPointIndex = entryPointCount++;
405    componentTypes.add(fragmentEntryPoint);
406
407    // Actually creating the composite component type is a single operation
408    // on the Slang session, but the operation could potentially fail if
409    // something about the composite was invalid (e.g., you are trying to
410    // combine multiple copies of the same module), so we need to deal
411    // with the possibility of diagnostic output.
412    //
413    ComPtr<slang::IComponentType> linkedProgram;
414    SlangResult result = slangSession->createCompositeComponentType(
415        componentTypes.getBuffer(),
416        componentTypes.getCount(),
417        linkedProgram.writeRef(),
418        diagnosticsBlob.writeRef());
419    diagnoseIfNeeded(diagnosticsBlob);
420    SLANG_RETURN_ON_FAIL(result);
421
422    const Index targetIndex = 0;
423
424    // Trigger compilation by requesting the code.
425    // Normally gfx would compile as needed.
426    {
427        ComPtr<ISlangBlob> code;
428        ComPtr<ISlangBlob> diagnostics;
429
430        SLANG_RETURN_ON_FAIL(linkedProgram->getEntryPointCode(
431            vertexEntryPointIndex,
432            targetIndex,
433            code.writeRef(),
434            diagnostics.writeRef()));
435        SLANG_RETURN_ON_FAIL(linkedProgram->getEntryPointCode(
436            fragmentEntryPointIndex,
437            targetIndex,
438            code.writeRef(),
439            diagnostics.writeRef()));
440    }
441
442    {
443        // We want to find all the compilation products. In particular we want to get the emit
444        // source map, and the obfuscated source maps
445
446        List<CompileProduct> compileProducts;
447
448        // The current mechanism for getting access to compilation products other than result
449        // blob/diagnostics is to return it as a compilation result "file system".
450
451        ComPtr<ISlangMutableFileSystem> vertexFileSystem;
452        SLANG_RETURN_ON_FAIL(linkedProgram->getResultAsFileSystem(
453            vertexEntryPointIndex,
454            targetIndex,
455            vertexFileSystem.writeRef()));
456
457        ComPtr<ISlangMutableFileSystem> fragmentFileSystem;
458        SLANG_RETURN_ON_FAIL(linkedProgram->getResultAsFileSystem(
459            fragmentEntryPointIndex,
460            targetIndex,
461            fragmentFileSystem.writeRef()));
462
463        // Add the contents of the compile result file systems into compileProducts
464        // Some products might appear in both file systems, so compileProducts is just the unique
465        // products. Additionally because some products may have the same name, we pass in a
466        // "prefix" to make the products name unique.
467        SLANG_RETURN_ON_FAIL(_addCompileProducts(vertexFileSystem, "vertex", compileProducts));
468        SLANG_RETURN_ON_FAIL(_addCompileProducts(fragmentFileSystem, "fragment", compileProducts));
469
470        // Now write all of the products out
471        for (const auto& product : compileProducts)
472        {
473            SLANG_RETURN_ON_FAIL(File::writeAllBytes(
474                product.fileName,
475                product.blob->getBufferPointer(),
476                product.blob->getBufferSize()));
477        }
478    }
479
480    // Once we've described the particular composition of entry points
481    // that we want to compile, we defer to the graphics API layer
482    // to extract compiled kernel code and load it into the API-specific
483    // program representation.
484    //
485    ShaderProgramDesc programDesc = {};
486    programDesc.slangGlobalScope = linkedProgram;
487    SLANG_RETURN_ON_FAIL(gDevice->createShaderProgram(programDesc, outProgram));
488
489    return SLANG_OK;
490}
491
492static void GFSDK_AFTERMATH_CALL
493_crashCallback(const void* gpuCrashDump, const uint32_t gpuCrashDumpSize, void* userData)
494{
495    reinterpret_cast<AftermathCrashExample*>(userData)->onAftermathCrash(
496        gpuCrashDump,
497        gpuCrashDumpSize);
498}
499
500static void GFSDK_AFTERMATH_CALL
501_debugInfoCallback(const void* gpuCrashDump, const uint32_t gpuCrashDumpSize, void* userData)
502{
503    reinterpret_cast<AftermathCrashExample*>(userData)->onAftermathDebugInfo(
504        gpuCrashDump,
505        gpuCrashDumpSize);
506}
507
508static void GFSDK_AFTERMATH_CALL _crashDescriptionCallback(
509    PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription,
510    void* userData)
511{
512    reinterpret_cast<AftermathCrashExample*>(userData)->onAftermathCrashDescription(addDescription);
513}
514
515static void GFSDK_AFTERMATH_CALL _markerCallback(
516    const void* marker,
517    void* pUserData,
518    void** resolvedMarkerData,
519    uint32_t* markerSize)
520{
521    reinterpret_cast<AftermathCrashExample*>(pUserData)->onAftermathMarker(
522        marker,
523        resolvedMarkerData,
524        markerSize);
525}
526
527void AftermathCrashExample::renderFrame(ITexture* texture)
528{
529    auto commandEncoder = gQueue->createCommandEncoder();
530
531    ComPtr<ITextureView> textureView = gDevice->createTextureView(texture, {});
532    RenderPassColorAttachment colorAttachment = {};
533    colorAttachment.view = textureView;
534    colorAttachment.loadOp = LoadOp::Clear;
535
536    RenderPassDesc renderPass = {};
537    renderPass.colorAttachments = &colorAttachment;
538    renderPass.colorAttachmentCount = 1;
539
540    auto renderEncoder = commandEncoder->beginRenderPass(renderPass);
541
542    RenderState renderState = {};
543    renderState.viewports[0] = Viewport::fromSize(windowWidth, windowHeight);
544    renderState.viewportCount = 1;
545    renderState.scissorRects[0] = ScissorRect::fromSize(windowWidth, windowHeight);
546    renderState.scissorRectCount = 1;
547
548    auto rootObject = renderEncoder->bindPipeline(m_renderPipeline);
549    ShaderCursor rootCursor(rootObject);
550
551    rootCursor["Uniforms"]["modelViewProjection"].setData(kIdentity, sizeof(float) * 16);
552
553    // We are going to extra efforts to create a shader that we know will time
554    // out because we *want* a GPU "crash", such we can capture via nsight aftermath.
555    // The failCount is just a number that is large enough to make things take too long.
556    int32_t failCount = 0x3fffffff;
557    rootCursor["Uniforms"]["failCount"].setData(&failCount, sizeof(failCount));
558
559    // We also need to set up a few pieces of fixed-function pipeline
560    // state that are not bound by the pipeline state above.
561    //
562    renderState.vertexBuffers[0] = m_vertexBuffer;
563    renderState.vertexBufferCount = 1;
564    renderEncoder->setRenderState(renderState);
565
566    // Finally, we are ready to issue a draw call for a single triangle.
567    //
568    DrawArguments drawArgs = {};
569    drawArgs.vertexCount = 3;
570    renderEncoder->draw(drawArgs);
571
572    renderEncoder->end();
573    gQueue->submit(commandEncoder->finish());
574
575    if (!isTestMode())
576    {
577        // With that, we are done drawing for one frame, and ready for the next.
578        //
579        gSurface()->present();
580    }
581
582    // If the id changes means we have a capture and so can quit.
583    // On D3D11, the first present *doesn't* appear to crash.
584    if (m_uniqueId != 0)
585    {
586        platform::Application::quit();
587    }
588}
589
590// This macro instantiates an appropriate main function to
591// run the application defined above.
592EXAMPLE_MAIN(innerMain<AftermathCrashExample>)