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
18.2 KiB426 linesraw
1// main.cpp
2
3// This file implements an extremely simple example of loading and
4// executing a Slang shader program. This is primarily an example
5// of how to use Slang as a "drop-in" replacement for an existing
6// HLSL compiler like the `D3DCompile` API. More advanced usage
7// of advanced Slang language and API features is left to the
8// next example.
9//
10// The comments in the file will attempt to explain concepts as
11// they are introduced.
12//
13// Of course, in order to use the Slang API, we need to include
14// its header. We have set up the build options for this project
15// so that it is as simple as:
16//
17#include "slang.h"
18//
19// Other build setups are possible, and Slang doesn't assume that
20// its include directory must be added to your global include
21// path.
22
23// For the purposes of keeping the demo code as simple as possible,
24// while still retaining some level of portability, our examples
25// make use of a small platform and graphics API abstraction layer,
26// which is included in the Slang source distribution under the
27// `tools/` directory.
28//
29// Applications can of course use Slang without ever touching this
30// abstraction layer, so we will not focus on it when explaining
31// examples, except in places where best practices for interacting
32// with Slang may depend on an application/engine making certain
33// design choices in their abstraction layer.
34//
35#include "core/slang-basic.h"
36#include "examples/example-base/example-base.h"
37#include "platform/window.h"
38#include "slang-com-ptr.h"
39#include "slang-rhi.h"
40
41#include <slang-rhi/shader-cursor.h>
42
43using namespace rhi;
44using namespace Slang;
45
46static const ExampleResources resourceBase("triangle");
47
48// For the purposes of a small example, we will define the vertex data for a
49// single triangle directly in the source file. It should be easy to extend
50// this example to load data from an external source, if desired.
51//
52struct Vertex
53{
54    float position[3];
55    float color[3];
56};
57
58static const int kVertexCount = 3;
59static const Vertex kVertexData[kVertexCount] = {
60    {{0, 0, 0.5}, {1, 0, 0}},
61    {{0, 1, 0.5}, {0, 0, 1}},
62    {{1, 0, 0.5}, {0, 1, 0}},
63};
64
65// The example application will be implemented as a `struct`, so that
66// we can scope the resources it allocates without using global variables.
67//
68struct HelloWorld : public WindowedAppBase
69{
70
71    // Many Slang API functions return detailed diagnostic information
72    // (error messages, warnings, etc.) as a "blob" of data, or return
73    // a null blob pointer instead if there were no issues.
74    //
75    // For convenience, we define a subroutine that will dump the information
76    // in a diagnostic blob if one is produced, and skip it otherwise.
77    //
78    void diagnoseIfNeeded(slang::IBlob* diagnosticsBlob)
79    {
80        if (diagnosticsBlob != nullptr)
81        {
82            printf("%s", (const char*)diagnosticsBlob->getBufferPointer());
83        }
84    }
85
86    // The main task an application cares about is compiling shader code
87    // from source (if needed) and loading it through the chosen graphics API.
88    //
89    // In addition, an application may want to receive reflection information
90    // about the program, which is what a `slang::ProgramLayout` provides.
91    //
92    Result loadShaderProgram(IDevice* device, IShaderProgram** outProgram)
93    {
94        // We need to obtain a compilation session (`slang::ISession`) that will provide
95        // a scope to all the compilation and loading of code we do.
96        //
97        // Our example application uses the `gfx` graphics API abstraction layer, which already
98        // creates a Slang compilation session for us, so we just grab and use it here.
99        ComPtr<slang::ISession> slangSession;
100        slangSession = device->getSlangSession();
101
102        // We can now start loading code into the slang session.
103        //
104        // The simplest way to load code is by calling `loadModule` with the name of a Slang
105        // module. A call to `loadModule("MyStuff")` will behave more or less as if you
106        // wrote:
107        //
108        //      import MyStuff;
109        //
110        // In a Slang shader file. The compiler will use its search paths to try to locate
111        // `MyModule.slang`, then compile and load that file. If a matching module had
112        // already been loaded previously, that would be used directly.
113        //
114        ComPtr<slang::IBlob> diagnosticsBlob;
115        Slang::String path = resourceBase.resolveResource("shaders.slang");
116        slang::IModule* module =
117            slangSession->loadModule(path.getBuffer(), diagnosticsBlob.writeRef());
118        diagnoseIfNeeded(diagnosticsBlob);
119        if (!module)
120            return SLANG_FAIL;
121
122        // Loading the `shaders` module will compile and check all the shader code in it,
123        // including the shader entry points we want to use. Now that the module is loaded
124        // we can look up those entry points by name.
125        //
126        // Note: If you are using this `loadModule` approach to load your shader code it is
127        // important to tag your entry point functions with the `[shader("...")]` attribute
128        // (e.g., `[shader("vertex")] void vertexMain(...)`). Without that information there
129        // is no unambiguous way for the compiler to know which functions represent entry
130        // points when it parses your code via `loadModule()`.
131        //
132        ComPtr<slang::IEntryPoint> vertexEntryPoint;
133        SLANG_RETURN_ON_FAIL(
134            module->findEntryPointByName("vertexMain", vertexEntryPoint.writeRef()));
135        //
136        ComPtr<slang::IEntryPoint> fragmentEntryPoint;
137        SLANG_RETURN_ON_FAIL(
138            module->findEntryPointByName("fragmentMain", fragmentEntryPoint.writeRef()));
139
140        // At this point we have a few different Slang API objects that represent
141        // pieces of our code: `module`, `vertexEntryPoint`, and `fragmentEntryPoint`.
142        //
143        // A single Slang module could contain many different entry points (e.g.,
144        // four vertex entry points, three fragment entry points, and two compute
145        // shaders), and before we try to generate output code for our target API
146        // we need to identify which entry points we plan to use together.
147        //
148        // Modules and entry points are both examples of *component types* in the
149        // Slang API. The API also provides a way to build a *composite* out of
150        // other pieces, and that is what we are going to do with our module
151        // and entry points.
152        //
153        Slang::List<slang::IComponentType*> componentTypes;
154        componentTypes.add(module);
155
156        // Later on when we go to extract compiled kernel code for our vertex
157        // and fragment shaders, we will need to make use of their order within
158        // the composition, so we will record the relative ordering of the entry
159        // points here as we add them.
160        int entryPointCount = 0;
161        int vertexEntryPointIndex = entryPointCount++;
162        componentTypes.add(vertexEntryPoint);
163
164        int fragmentEntryPointIndex = entryPointCount++;
165        componentTypes.add(fragmentEntryPoint);
166
167        // Actually creating the composite component type is a single operation
168        // on the Slang session, but the operation could potentially fail if
169        // something about the composite was invalid (e.g., you are trying to
170        // combine multiple copies of the same module), so we need to deal
171        // with the possibility of diagnostic output.
172        //
173        ComPtr<slang::IComponentType> linkedProgram;
174        SlangResult result = slangSession->createCompositeComponentType(
175            componentTypes.getBuffer(),
176            componentTypes.getCount(),
177            linkedProgram.writeRef(),
178            diagnosticsBlob.writeRef());
179        diagnoseIfNeeded(diagnosticsBlob);
180        SLANG_RETURN_ON_FAIL(result);
181
182        // Once we've described the particular composition of entry points
183        // that we want to compile, we defer to the graphics API layer
184        // to extract compiled kernel code and load it into the API-specific
185        // program representation.
186        //
187        ShaderProgramDesc programDesc = {};
188        programDesc.slangGlobalScope = linkedProgram;
189        SLANG_RETURN_ON_FAIL(device->createShaderProgram(programDesc, outProgram));
190
191        if (isTestMode())
192        {
193            printEntrypointHashes(entryPointCount, 1, linkedProgram);
194        }
195
196        return SLANG_OK;
197    }
198
199    //
200    // The above function shows the core of what is required to use the
201    // Slang API as a simple compiler (e.g., a drop-in replacement for
202    // fxc or dxc).
203    //
204    // The rest of this file implements an extremely simple rendering application
205    // that will execute the vertex/fragment shaders loaded with the function
206    // we have just defined.
207    //
208
209    // We will define global variables for the various platforms and
210    // graphics API objects that our application needs:
211    //
212    // As a reminder, *none* of these are Slang API objects. All
213    // of them come from the utility library we are using to simplify
214    // building an example program.
215    //
216    ComPtr<IPipeline> gPipeline;
217    ComPtr<IBuffer> gVertexBuffer;
218    const Format format = Format::RGB32Float;
219
220    // Now that we've covered the function that actually loads and
221    // compiles our Slang shade code, we can go through the rest
222    // of the application code without as much commentary.
223    //
224    Slang::Result initialize()
225    {
226        // Create a window for our application to render into.
227        //
228        SLANG_RETURN_ON_FAIL(initializeBase("triangle", 1024, 768, getDeviceType()));
229
230        // We will create objects needed to configure the "input assembler"
231        // (IA) stage of the D3D pipeline.
232        //
233        // First, we create an input layout:
234        //
235        InputElementDesc inputElements[] = {
236            {"POSITION", 0, format, offsetof(Vertex, position)},
237            {"COLOR", 0, format, offsetof(Vertex, color)},
238        };
239        auto inputLayout = gDevice->createInputLayout(sizeof(Vertex), &inputElements[0], 2);
240        if (!inputLayout)
241            return SLANG_FAIL;
242
243        // Next we allocate a vertex buffer for our pre-initialized
244        // vertex data.
245        //
246        BufferDesc vertexBufferDesc;
247        vertexBufferDesc.format = format;
248        vertexBufferDesc.size = kVertexCount * sizeof(Vertex);
249        vertexBufferDesc.elementSize = sizeof(Vertex);
250        vertexBufferDesc.usage = BufferUsage::VertexBuffer;
251        gVertexBuffer = gDevice->createBuffer(vertexBufferDesc, &kVertexData[0]);
252        if (!gVertexBuffer)
253            return SLANG_FAIL;
254
255        // Now we will use our `loadShaderProgram` function to load
256        // the code from `shaders.slang` into the graphics API.
257        //
258        ComPtr<IShaderProgram> shaderProgram;
259        SLANG_RETURN_ON_FAIL(loadShaderProgram(gDevice, shaderProgram.writeRef()));
260
261        // Following the D3D12/Vulkan style of API, we need a pipeline state object
262        // (PSO) to encapsulate the configuration of the overall graphics pipeline.
263        //
264        ColorTargetDesc colorTarget;
265        colorTarget.format = format;
266        RenderPipelineDesc desc;
267        desc.inputLayout = inputLayout;
268        desc.program = shaderProgram;
269        desc.targetCount = 1;
270        desc.targets = &colorTarget;
271        desc.depthStencil.depthTestEnable = false;
272        desc.depthStencil.depthWriteEnable = false;
273        desc.primitiveTopology = PrimitiveTopology::TriangleList;
274        gPipeline = gDevice->createRenderPipeline(desc);
275        if (!gPipeline)
276            return SLANG_FAIL;
277
278        return SLANG_OK;
279    }
280
281    // With the initialization out of the way, we can now turn our attention
282    // to the per-frame rendering logic. As with the initialization, there is
283    // nothing really Slang-specific here, so the commentary doesn't need
284    // to be very detailed.
285    //
286    virtual void renderFrame(ITexture* texture) override
287    {
288        auto commandEncoder = gQueue->createCommandEncoder();
289
290        ComPtr<ITextureView> textureView = gDevice->createTextureView(texture, {});
291        RenderPassColorAttachment colorAttachment = {};
292        colorAttachment.view = textureView;
293        colorAttachment.loadOp = LoadOp::Clear;
294
295        RenderPassDesc renderPass = {};
296        renderPass.colorAttachments = &colorAttachment;
297        renderPass.colorAttachmentCount = 1;
298
299        auto renderEncoder = commandEncoder->beginRenderPass(renderPass);
300
301
302        RenderState renderState = {};
303        renderState.viewports[0] = Viewport::fromSize(windowWidth, windowHeight);
304        renderState.viewportCount = 1;
305        renderState.scissorRects[0] = ScissorRect::fromSize(windowWidth, windowHeight);
306        renderState.scissorRectCount = 1;
307
308        // In order to bind shader parameters to the pipeline, we need
309        // to know how those parameters were assigned to locations/bindings/registers
310        // for the target graphics API.
311        //
312        // The Slang compiler assigns locations to parameters in a deterministic
313        // fashion, so it is possible for a programmer to hard-code locations
314        // into their application code that will match up with their shaders.
315        //
316        // Hard-coding of locations can become intractable as an application needs
317        // to support more different target platforms and graphics APIs, as well
318        // as more shaders with different specialized variants.
319        //
320        // Rather than rely on hard-coded locations, our examples will make use of
321        // reflection information provided by the Slang compiler (see `programLayout`
322        // above), and our example graphics API layer will translate that reflection
323        // information into a layout for a "root shader object."
324        //
325        // The root object will store values/bindings for all of the parameters in
326        // the `IShaderProgram` used to create the pipeline state. At a conceptual
327        // level we can think of `rootObject` as representing the "global scope" of
328        // the shader program that was loaded; it has entries for each global shader
329        // parameter that was declared.
330        //
331        // Readers who are familiar with D3D12 or Vulkan might think of this root
332        // layout as being similar in spirit to a "root signature" or "pipeline layout."
333        //
334        // We start parameter binding by binding the pipeline state in command encoder.
335        // This method will return a transient root shader object for us to write our
336        // shader parameters into.
337        //
338        auto rootObject =
339            renderEncoder->bindPipeline(static_cast<IRenderPipeline*>(gPipeline.get()));
340
341        // We will update the model-view-projection matrix that is passed
342        // into the shader code via the `Uniforms` buffer on a per-frame
343        // basis, even though the data that is loaded does not change
344        // per-frame (we always use an identity matrix).
345        //
346        auto deviceInfo = gDevice->getInfo();
347
348        // We know that `rootObject` is a root shader object created
349        // from our program, and that it is set up to hold values for
350        // all the parameters of that program. In order to actually
351        // set values, we need to be able to look up the location
352        // of the specific parameters that we want to set.
353        //
354        // Our example graphics API layer supports this operation
355        // with the idea of a *shader cursor* which can be thought
356        // of as pointing "into" a particular shader object at
357        // some location/offset. This design choice abstracts over
358        // the many ways that different platforms and APIs represent
359        // the necessary offset information.
360        //
361        // We construct an initial shader cursor that points at the
362        // entire shader program. You can think of this as akin to
363        // a directory path of `/` for the root directory in a file
364        // system.
365        //
366        ShaderCursor rootCursor(rootObject);
367        //
368        // Next, we use a convenience overload of `operator[]` to
369        // navigate from the root cursor down to the parameter we
370        // want to set.
371        //
372        // The operation `rootCursor["Uniforms"]` looks up the
373        // offset/location of the global shader parameter `Uniforms`
374        // (which is a uniform/constant buffer), and the subsequent
375        // `["modelViewProjection"]` step navigates from there down
376        // to the member named `modelViewProjection` in that buffer.
377        //
378        // Once we have formed a cursor that "points" at the
379        // model-view projection matrix, we can set its data directly.
380        //
381        rootCursor["Uniforms"]["modelViewProjection"].setData(kIdentity, sizeof(float) * 16);
382        //
383        // Some readers might be concerned about the performance of
384        // the above operations because of the use of strings. For
385        // those readers, here are two things to note:
386        //
387        // * While these `operator[]` steps do need to perform string
388        //   comparisons, they do *not* make copies of the strings or
389        //   perform any heap allocation.
390        //
391        // * There are other overloads of `operator[]` that use the
392        //   *index* of a parameter/field instead of its name, and those
393        //   operations have fixed/constant overhead and perform no
394        //   string comparisons. The indices used are independent of
395        //   the target platform and graphics API, and can thus be
396        //   hard-coded even in cross-platform code.
397        //
398
399        // We also need to set up a few pieces of fixed-function pipeline
400        // state that are not bound by the pipeline state above.
401        //
402        renderState.vertexBuffers[0] = gVertexBuffer;
403        renderState.vertexBufferCount = 1;
404        renderEncoder->setRenderState(renderState);
405
406        // Finally, we are ready to issue a draw call for a single triangle.
407        //
408        DrawArguments drawArgs = {};
409        drawArgs.vertexCount = 3;
410        renderEncoder->draw(drawArgs);
411
412        renderEncoder->end();
413        gQueue->submit(commandEncoder->finish());
414
415        if (!isTestMode())
416        {
417            // With that, we are done drawing for one frame, and ready for the next.
418            //
419            gSurface->present();
420        }
421    }
422};
423
424// This macro instantiates an appropriate main function to
425// run the application defined above.
426EXAMPLE_MAIN(innerMain<HelloWorld>);