yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
13dd01489
master
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{ 54float position [3 ]; 55float 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// 78void diagnoseIfNeeded (slang::IBlob * diagnosticsBlob ) 79 { 80if (diagnosticsBlob != nullptr ) 81 { 82printf ("%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// 92Result 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. 99ComPtr < slang::ISession > slangSession ; 100slangSession = 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// 114ComPtr < slang::IBlob > diagnosticsBlob ; 115Slang ::String path = resourceBase .resolveResource ("shaders.slang" ); 116 slang::IModule * module = 117slangSession -> loadModule (path .getBuffer (),diagnosticsBlob .writeRef ()); 118diagnoseIfNeeded (diagnosticsBlob ); 119if (!module ) 120return 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// 132ComPtr < slang::IEntryPoint > vertexEntryPoint ; 133SLANG_RETURN_ON_FAIL ( 134module -> findEntryPointByName ("vertexMain" ,vertexEntryPoint .writeRef ())); 135// 136ComPtr < slang::IEntryPoint > fragmentEntryPoint ; 137SLANG_RETURN_ON_FAIL ( 138module -> 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// 153Slang ::List < slang::IComponentType *> componentTypes ; 154componentTypes .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. 160int entryPointCount = 0 ; 161int vertexEntryPointIndex = entryPointCount ++ ; 162componentTypes .add (vertexEntryPoint ); 163 164int fragmentEntryPointIndex = entryPointCount ++ ; 165componentTypes .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// 173ComPtr < slang::IComponentType > linkedProgram ; 174SlangResult result = slangSession -> createCompositeComponentType ( 175componentTypes .getBuffer (), 176componentTypes .getCount (), 177linkedProgram .writeRef (), 178diagnosticsBlob .writeRef ()); 179diagnoseIfNeeded (diagnosticsBlob ); 180SLANG_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// 187ShaderProgramDesc programDesc = {}; 188programDesc .slangGlobalScope = linkedProgram ; 189SLANG_RETURN_ON_FAIL (device -> createShaderProgram (programDesc ,outProgram )); 190 191if (isTestMode ()) 192 { 193printEntrypointHashes (entryPointCount ,1 ,linkedProgram ); 194 } 195 196return 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// 216ComPtr < IPipeline > gPipeline ; 217ComPtr < IBuffer > gVertexBuffer ; 218const 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// 224Slang ::Result initialize () 225 { 226// Create a window for our application to render into. 227// 228SLANG_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// 235InputElementDesc inputElements []= { 236 {"POSITION" ,0 ,format , offsetof(Vertex ,position )}, 237 {"COLOR" ,0 ,format , offsetof(Vertex ,color )}, 238 }; 239auto inputLayout = gDevice -> createInputLayout (sizeof (Vertex ),& inputElements [0 ],2 ); 240if (!inputLayout ) 241return SLANG_FAIL ; 242 243// Next we allocate a vertex buffer for our pre-initialized 244// vertex data. 245// 246BufferDesc vertexBufferDesc ; 247vertexBufferDesc .format = format ; 248vertexBufferDesc .size = kVertexCount * sizeof (Vertex ); 249vertexBufferDesc .elementSize = sizeof (Vertex ); 250vertexBufferDesc .usage = BufferUsage ::VertexBuffer ; 251gVertexBuffer = gDevice -> createBuffer (vertexBufferDesc ,& kVertexData [0 ]); 252if (!gVertexBuffer ) 253return SLANG_FAIL ; 254 255// Now we will use our `loadShaderProgram` function to load 256// the code from `shaders.slang` into the graphics API. 257// 258ComPtr < IShaderProgram > shaderProgram ; 259SLANG_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// 264ColorTargetDesc colorTarget ; 265colorTarget .format = format ; 266RenderPipelineDesc desc ; 267desc .inputLayout = inputLayout ; 268desc .program = shaderProgram ; 269desc .targetCount = 1 ; 270desc .targets = & colorTarget ; 271desc .depthStencil .depthTestEnable = false; 272desc .depthStencil .depthWriteEnable = false; 273desc .primitiveTopology = PrimitiveTopology ::TriangleList ; 274gPipeline = gDevice -> createRenderPipeline (desc ); 275if (!gPipeline ) 276return SLANG_FAIL ; 277 278return 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// 286virtual void renderFrame (ITexture * texture )override 287 { 288auto commandEncoder = gQueue -> createCommandEncoder (); 289 290ComPtr < ITextureView > textureView = gDevice -> createTextureView (texture , {}); 291RenderPassColorAttachment colorAttachment = {}; 292colorAttachment .view = textureView ; 293colorAttachment .loadOp = LoadOp ::Clear ; 294 295RenderPassDesc renderPass = {}; 296renderPass .colorAttachments = & colorAttachment ; 297renderPass .colorAttachmentCount = 1 ; 298 299auto renderEncoder = commandEncoder -> beginRenderPass (renderPass ); 300 301 302RenderState renderState = {}; 303renderState .viewports [0 ]= Viewport ::fromSize (windowWidth ,windowHeight ); 304renderState .viewportCount = 1 ; 305renderState .scissorRects [0 ]= ScissorRect ::fromSize (windowWidth ,windowHeight ); 306renderState .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// 338auto rootObject = 339renderEncoder -> 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// 346auto 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// 366ShaderCursor 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// 381rootCursor ["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// 402renderState .vertexBuffers [0 ]= gVertexBuffer ; 403renderState .vertexBufferCount = 1 ; 404renderEncoder -> setRenderState (renderState ); 405 406// Finally, we are ready to issue a draw call for a single triangle. 407// 408DrawArguments drawArgs = {}; 409drawArgs .vertexCount = 3 ; 410renderEncoder -> draw (drawArgs ); 411 412renderEncoder -> end (); 413gQueue -> submit (commandEncoder -> finish ()); 414 415if (!isTestMode ()) 416 { 417// With that, we are done drawing for one frame, and ready for the next. 418// 419gSurface -> present (); 420 } 421 } 422}; 423 424// This macro instantiates an appropriate main function to 425// run the application defined above. 426EXAMPLE_MAIN (innerMain < HelloWorld > );