yum-mirror/slang

Making it easier to work with shaders

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

Jay KwakAdd command-line arguments to examples (#7835)13dd01489

master
17.2 KiB429 linesraw
1// main.cpp
2
3// This file provides the application code for the `shader-toy` example.
4//
5// Much of the logic here is identical to the simpler `hello-world` example,
6// so we will not spend time commenting those parts that are identical or
7// nearly identical. Readers who want detailed comments on a simpler example
8// using Slang should look there.
9
10// This example uses the Slang C/C++ API, alonmg with its optional type
11// for managing COM-style reference-counted pointers.
12//
13#include "slang-com-ptr.h"
14#include "slang.h"
15using Slang::ComPtr;
16
17// This example uses a graphics API abstraction layer that is implemented inside
18// the Slang codebase for use in our sample programs and test cases. Use of
19// this layer is *not* required or assumed when using the Slang language,
20// compiler, and API.
21//
22#include "core/slang-basic.h"
23#include "examples/example-base/example-base.h"
24#include "platform/performance-counter.h"
25#include "platform/window.h"
26#include "slang-rhi.h"
27#include "slang-rhi/shader-cursor.h"
28
29#include <chrono>
30
31static const ExampleResources resourceBase("shader-toy");
32
33using namespace rhi;
34
35// In order to display a shader toy effect using rasterization-based shader
36// execution we need to render a full-screen triangle. We will define a
37// small helper type that defines the data for such a triangle.
38//
39struct FullScreenTriangle
40{
41    struct Vertex
42    {
43        float position[2];
44    };
45
46    enum
47    {
48        kVertexCount = 3
49    };
50
51    static const Vertex kVertices[kVertexCount];
52};
53const FullScreenTriangle::Vertex FullScreenTriangle::kVertices[FullScreenTriangle::kVertexCount] = {
54    {{-1, -1}},
55    {{-1, 3}},
56    {{3, -1}},
57};
58
59// The application itself will be encapsulated in a C++ `struct` type
60// so that it can easily scope its state without use of global variables.
61//
62struct ShaderToyApp : public WindowedAppBase
63{
64
65    // The uniform data used by the shader is defined here as a simple
66    // POD ("plain old data") type.
67    //
68    // Note: This type must match the declaration of `ShaderToyUniforms`
69    // in the file `shader-toy.slang`.
70    //
71    // An application could instead use a shared header file to define
72    // this type, or use Slang's reflection capabilities to allocate
73    // and set parameters at runtime. For this simple example we did
74    // the expedient thing of having distinct Slang and C++ declarations.
75    //
76    struct Uniforms
77    {
78        float iMouse[4];
79        float iResolution[2];
80        float iTime;
81    };
82
83    // The main interesting part of the host application code is where we
84    // load, compile, inspect, and compose the Slang shader code.
85    //
86    Result loadShaderProgram(IDevice* device, ComPtr<IShaderProgram>& outShaderProgram)
87    {
88        // We need to obatin a compilation session (`slang::ISession`) that will provide
89        // a scope to all the compilation and loading of code we do.
90        //
91        // Our example application uses the `slang-rhi` graphics API abstraction layer, which
92        // already creates a Slang compilation session for us, so we just grab and use it here.
93        ComPtr<slang::ISession> slangSession;
94        slangSession = device->getSlangSession();
95
96        // Once the session has been obtained, we can start loading code into it.
97        //
98        // The simplest way to load code is by calling `loadModule` with the name of a Slang
99        // module. A call to `loadModule("MyStuff")` will behave more or less as if you
100        // wrote:
101        //
102        //      import MyStuff;
103        //
104        // In a Slang shader file. The compiler will use its search paths to try to locate
105        // `MyModule.slang`, then compile and load that file. If a matching module had
106        // already been loaded previously, that would be used directly.
107        //
108        // Note: The only interesting wrinkle here is that our file is named `shader-toy` with
109        // a hyphen in it, so the name is not directly usable as an identifier in Slang code.
110        // Instead, when trying to import this module in the context of Slang code, a user
111        // needs to replace the hyphens with underscores:
112        //
113        //      import shader_toy;
114        //
115        ComPtr<slang::IBlob> diagnosticsBlob;
116        Slang::String shaderToyPath = resourceBase.resolveResource("shader-toy.slang");
117        slang::IModule* module =
118            slangSession->loadModule(shaderToyPath.getBuffer(), diagnosticsBlob.writeRef());
119        diagnoseIfNeeded(diagnosticsBlob);
120        if (!module)
121            return SLANG_FAIL;
122
123        // Loading the `shader-toy` module will compile and check all the shader code in it,
124        // including the shader entry points we want to use. Now that the module is loaded
125        // we can look up those entry points by name.
126        //
127        // Note: If you are using this `loadModule` approach to load your shader code it is
128        // important to tag your entry point functions with the `[shader("...")]` attribute
129        // (e.g., `[shader("vertex")] void vertexMain(...)`). Without that information there
130        // is no umambiguous way for the compiler to know which functions represent entry
131        // points when it parses your code via `loadModule()`.
132        //
133        char const* vertexEntryPointName = "vertexMain";
134        char const* fragmentEntryPointName = "fragmentMain";
135        //
136        ComPtr<slang::IEntryPoint> vertexEntryPoint;
137        SLANG_RETURN_ON_FAIL(
138            module->findEntryPointByName(vertexEntryPointName, vertexEntryPoint.writeRef()));
139        //
140        ComPtr<slang::IEntryPoint> fragmentEntryPoint;
141        SLANG_RETURN_ON_FAIL(
142            module->findEntryPointByName(fragmentEntryPointName, fragmentEntryPoint.writeRef()));
143
144        // At this point we have a few different Slang API objects that represent
145        // pieces of our code: `module`, `vertexEntryPoint`, and `fragmentEntryPoint`.
146        //
147        // A single Slang module could contain many different entry points (e.g.,
148        // four vertex entry points, three fragment entry points, and two compute
149        // shaders), and before we try to generate output code for our target API
150        // we need to identify which entry points we plan to use together.
151        //
152        // Modules and entry points are both examples of *component types* in the
153        // Slang API. The API also provides a way to build a *composite* out of
154        // other pieces, and that is what we are going to do with our module
155        // and entry points.
156        //
157        Slang::List<slang::IComponentType*> componentTypes;
158        componentTypes.add(module);
159
160        // Later on when we go to extract compiled kernel code for our vertex
161        // and fragment shaders, we will need to make use of their order within
162        // the composition, so we will record the relative ordering of the entry
163        // points here as we add them.
164        int entryPointCount = 0;
165        int vertexEntryPointIndex = entryPointCount++;
166        componentTypes.add(vertexEntryPoint);
167
168        int fragmentEntryPointIndex = entryPointCount++;
169        componentTypes.add(fragmentEntryPoint);
170
171        // Actually creating the composite component type is a single operation
172        // on the Slang session, but the operation could potentially fail if
173        // something about the composite was invalid (e.g., you are trying to
174        // combine multiple copies of the same module), so we need to deal
175        // with the possibility of diagnostic output.
176        //
177        ComPtr<slang::IComponentType> composedProgram;
178        SlangResult result = slangSession->createCompositeComponentType(
179            componentTypes.getBuffer(),
180            componentTypes.getCount(),
181            composedProgram.writeRef(),
182            diagnosticsBlob.writeRef());
183        diagnoseIfNeeded(diagnosticsBlob);
184        SLANG_RETURN_ON_FAIL(result);
185
186        // At this point, `composedProgram` represents the shader program
187        // we want to run, and the vertex and fragment shader there have
188        // been checked.
189        //
190        // We could use the Slang reflection API on `composedProgram` at this
191        // point to query things like the locations and offsets of the
192        // various uniform parameters, textures, etc.
193        //
194        // What *cannot* be done yet at this point is actually generating
195        // kernel code, because `composedProgram` includes a generic type
196        // parameter as part of the `fragmentMain` entry point:
197        //
198        //      void fragmentMain<T : IShaderToyImageShader>(...)
199        //
200        // Our next task is to load code for a type we'd like to plug in
201        // for `T` there.
202        //
203        // Because Slang supports modular programming, there is no requirement
204        // that a type we want to plug in for `T` has to come from the
205        // same module, and to demonstrate that we will load a different
206        // module to provide the effect type we will plug in.
207        //
208        const char* effectTypeName = "ExampleEffect";
209        Slang::String effectModulePath = resourceBase.resolveResource("example-effect.slang");
210        slang::IModule* effectModule =
211            slangSession->loadModule(effectModulePath.getBuffer(), diagnosticsBlob.writeRef());
212        diagnoseIfNeeded(diagnosticsBlob);
213        if (!module)
214            return SLANG_FAIL;
215
216        // Once we've loaded the code module that defines out effect type,
217        // we can look it up by name using the reflection information on
218        // the module.
219        //
220        // Note: A future version of the Slang API will support enumerating
221        // the types declared in a module so that we do not have to hard-code
222        // the name here.
223        //
224        auto effectType = effectModule->getLayout()->findTypeByName(effectTypeName);
225
226        // Now that we have the `effectType` we want to plug in to our generic
227        // shader, we need to specialize the shader to that type.
228        //
229        // Because a shader program could have zero or more specialization parameters,
230        // we need to build up an array of specialization arguments.
231        //
232        Slang::List<slang::SpecializationArg> specializationArgs;
233
234        {
235            // In our case, we only have a single specialization argument we plan
236            // to use, and it is a type argument.
237            //
238            slang::SpecializationArg effectTypeArg;
239            effectTypeArg.kind = slang::SpecializationArg::Kind::Type;
240            effectTypeArg.type = effectType;
241            specializationArgs.add(effectTypeArg);
242        }
243
244        // Specialization of a component type is a single Slang API call, but
245        // we need to deal with the possibility of diagnostic output on failure.
246        // For example, if we tried to specialize the shader program to a
247        // type like `int` that doesn't support the `IShaderToyImageShader` interface,
248        // this is the step where we'd get an error message saying so.
249        //
250        ComPtr<slang::IComponentType> specializedProgram;
251        result = composedProgram->specialize(
252            specializationArgs.getBuffer(),
253            specializationArgs.getCount(),
254            specializedProgram.writeRef(),
255            diagnosticsBlob.writeRef());
256        diagnoseIfNeeded(diagnosticsBlob);
257        SLANG_RETURN_ON_FAIL(result);
258
259        // At this point we have a specialized shader program that represents our
260        // intention to run the `vertexMain` and `fragmentMain` entry points,
261        // specialized to the `ExampleEffect` type we loaded.
262        //
263        // We can now *link* the program, which ensures that all of the code that
264        // it transitively depends on has been pulled together into a single
265        // component type.
266        //
267        ComPtr<slang::IComponentType> linkedProgram;
268        result = specializedProgram->link(linkedProgram.writeRef(), diagnosticsBlob.writeRef());
269        diagnoseIfNeeded(diagnosticsBlob);
270        SLANG_RETURN_ON_FAIL(result);
271
272        ShaderProgramDesc programDesc = {};
273        programDesc.slangGlobalScope = linkedProgram.get();
274        auto shaderProgram = device->createShaderProgram(programDesc);
275        outShaderProgram = shaderProgram;
276        return SLANG_OK;
277    }
278
279    ComPtr<IShaderProgram> gShaderProgram;
280    ComPtr<IPipeline> gPipeline;
281    ComPtr<IBuffer> gVertexBuffer;
282    const Format format = Format::RG32Float;
283
284    Result initialize()
285    {
286        SLANG_RETURN_ON_FAIL(initializeBase("Shader Toy", 1024, 768, getDeviceType()));
287
288        // We may not have a window if we're running in test mode
289        SLANG_ASSERT(isTestMode() || gWindow);
290        if (gWindow)
291        {
292            gWindow->events.mouseMove = [this](const platform::MouseEventArgs& e)
293            { handleEvent(e); };
294            gWindow->events.mouseUp = [this](const platform::MouseEventArgs& e) { handleEvent(e); };
295            gWindow->events.mouseDown = [this](const platform::MouseEventArgs& e)
296            { handleEvent(e); };
297        }
298
299        InputElementDesc inputElements[] = {
300            {"POSITION", 0, format, offsetof(FullScreenTriangle::Vertex, position)},
301        };
302        auto inputLayout = gDevice->createInputLayout(
303            sizeof(FullScreenTriangle::Vertex),
304            &inputElements[0],
305            SLANG_COUNT_OF(inputElements));
306        if (!inputLayout)
307            return SLANG_FAIL;
308
309        BufferDesc vertexBufferDesc;
310        vertexBufferDesc.size =
311            FullScreenTriangle::kVertexCount * sizeof(FullScreenTriangle::Vertex);
312        vertexBufferDesc.elementSize = sizeof(FullScreenTriangle::Vertex);
313        vertexBufferDesc.usage = BufferUsage::VertexBuffer;
314        gVertexBuffer = gDevice->createBuffer(vertexBufferDesc, &FullScreenTriangle::kVertices[0]);
315        if (!gVertexBuffer)
316            return SLANG_FAIL;
317
318        SLANG_RETURN_ON_FAIL(loadShaderProgram(gDevice, gShaderProgram));
319
320        // Create pipeline.
321        ColorTargetDesc colorTarget;
322        colorTarget.format = Format::RGBA8Unorm;
323        RenderPipelineDesc desc;
324        desc.inputLayout = inputLayout;
325        desc.program = gShaderProgram;
326        desc.targetCount = 1;
327        desc.targets = &colorTarget;
328        desc.depthStencil.depthTestEnable = false;
329        desc.depthStencil.depthWriteEnable = false;
330        desc.primitiveTopology = PrimitiveTopology::TriangleList;
331        gPipeline = gDevice->createRenderPipeline(desc);
332        if (!gPipeline)
333            return SLANG_FAIL;
334
335        return SLANG_OK;
336    }
337
338    bool wasMouseDown = false;
339    bool isMouseDown = false;
340    float lastMouseX = 0.0f;
341    float lastMouseY = 0.0f;
342    float clickMouseX = 0.0f;
343    float clickMouseY = 0.0f;
344
345    bool firstTime = true;
346    platform::TimePoint startTime;
347
348    virtual void renderFrame(ITexture* texture) override
349    {
350        auto commandEncoder = gQueue->createCommandEncoder();
351        if (firstTime)
352        {
353            startTime = platform::PerformanceCounter::now();
354            firstTime = false;
355        }
356
357        // Update uniform buffer.
358
359        Uniforms uniforms = {};
360        {
361            bool isMouseClick = isMouseDown && !wasMouseDown;
362            wasMouseDown = isMouseDown;
363
364            if (isMouseClick)
365            {
366                clickMouseX = lastMouseX;
367                clickMouseY = lastMouseY;
368            }
369
370            uniforms.iMouse[0] = lastMouseX;
371            uniforms.iMouse[1] = lastMouseY;
372            uniforms.iMouse[2] = isMouseDown ? clickMouseX : -clickMouseX;
373            uniforms.iMouse[3] = isMouseClick ? clickMouseY : -clickMouseY;
374            uniforms.iTime = platform::PerformanceCounter::getElapsedTimeInSeconds(startTime);
375            uniforms.iResolution[0] = float(windowWidth);
376            uniforms.iResolution[1] = float(windowHeight);
377        }
378
379        // Encode render commands.
380        ComPtr<ITextureView> textureView = gDevice->createTextureView(texture, {});
381        RenderPassColorAttachment colorAttachment = {};
382        colorAttachment.view = textureView;
383        colorAttachment.loadOp = LoadOp::Clear;
384
385        RenderPassDesc renderPass = {};
386        renderPass.colorAttachments = &colorAttachment;
387        renderPass.colorAttachmentCount = 1;
388
389        auto encoder = commandEncoder->beginRenderPass(renderPass);
390
391        RenderState renderState = {};
392        renderState.viewports[0] = Viewport::fromSize(windowWidth, windowHeight);
393        renderState.viewportCount = 1;
394        renderState.scissorRects[0] = ScissorRect::fromSize(windowWidth, windowHeight);
395        renderState.scissorRectCount = 1;
396
397        auto rootObject = encoder->bindPipeline(static_cast<IRenderPipeline*>(gPipeline.get()));
398        auto constantBuffer = rootObject->getObject(ShaderOffset());
399        constantBuffer->setData(ShaderOffset(), &uniforms, sizeof(uniforms));
400
401        renderState.vertexBuffers[0] = gVertexBuffer;
402        renderState.vertexBufferCount = 1;
403        encoder->setRenderState(renderState);
404
405        DrawArguments drawArgs = {};
406        drawArgs.vertexCount = 3;
407        encoder->draw(drawArgs);
408
409        encoder->end();
410
411        gQueue->submit(commandEncoder->finish());
412
413        if (!isTestMode())
414        {
415            gSurface->present();
416        }
417    }
418
419    void handleEvent(const platform::MouseEventArgs& event)
420    {
421        isMouseDown = ((int)event.buttons & (int)platform::ButtonState::Enum::LeftButton) != 0;
422        lastMouseX = (float)event.x;
423        lastMouseY = (float)event.y;
424    }
425};
426
427// This macro instantiates an appropriate main function to
428// run the application defined above.
429EXAMPLE_MAIN(innerMain<ShaderToyApp>);