summaryrefslogtreecommitdiff
path: root/examples/experimental
diff options
context:
space:
mode:
authorYong He <yonghe@outlook.com>2021-03-04 16:25:58 -0800
committerGitHub <noreply@github.com>2021-03-04 16:25:58 -0800
commita5ac4999b4dea546a7ef824669ab1809224b6448 (patch)
tree15bb22eb98a94f7f81489deef55396461501d3dc /examples/experimental
parent13ff0bd345990c0fdfb7b52ebd5339cddb04889e (diff)
Refactor `gfx` to surface `CommandBuffer` interface. (#1735)
* Refactor `gfx` to surface `CommandBuffer` interface. * Fixes. * Fix code review issues, and make vulkan runnable on devices without VK_EXT_extended_dynamic_states. * Update solution files * Move out-of-date examples to examples/experimental Co-authored-by: Yong He <yhe@nvidia.com>
Diffstat (limited to 'examples/experimental')
-rw-r--r--examples/experimental/heterogeneous-hello-world/README.md4
-rw-r--r--examples/experimental/heterogeneous-hello-world/main.cpp380
-rw-r--r--examples/experimental/heterogeneous-hello-world/shader.cpp197
-rw-r--r--examples/experimental/heterogeneous-hello-world/shader.slang65
-rw-r--r--examples/experimental/model-viewer/README.md25
-rw-r--r--examples/experimental/model-viewer/cube.mtl35
-rw-r--r--examples/experimental/model-viewer/main.cpp2446
-rw-r--r--examples/experimental/model-viewer/shaders.slang485
8 files changed, 3637 insertions, 0 deletions
diff --git a/examples/experimental/heterogeneous-hello-world/README.md b/examples/experimental/heterogeneous-hello-world/README.md
new file mode 100644
index 000000000..709652922
--- /dev/null
+++ b/examples/experimental/heterogeneous-hello-world/README.md
@@ -0,0 +1,4 @@
+Slang "CPU Hello World Heterogeneous" Example
+===============================
+
+This example is a work-in-progress to illustrate how a heterogeneous programming example might work. It should NOT be used as a reference for working Slang code yet. \ No newline at end of file
diff --git a/examples/experimental/heterogeneous-hello-world/main.cpp b/examples/experimental/heterogeneous-hello-world/main.cpp
new file mode 100644
index 000000000..372fcd615
--- /dev/null
+++ b/examples/experimental/heterogeneous-hello-world/main.cpp
@@ -0,0 +1,380 @@
+// This example is out of date and currently disabled from build.
+// The `gfx` layer has been refactored with a new command list based
+// model. The example must be updated to use the new `gfx` interface
+// before it can be included in build.
+
+#if 0
+// main.cpp
+
+// This file implements an extremely simple example of loading and
+// executing a Slang shader program. This is primarily an example
+// of how to use Slang as a "drop-in" replacement for an existing
+// HLSL compiler like the `D3DCompile` API. More advanced usage
+// of advanced Slang language and API features is left to the
+// next example.
+//
+// The comments in the file will attempt to explain concepts as
+// they are introduced.
+//
+// Of course, in order to use the Slang API, we need to include
+// its header. We have set up the build options for this project
+// so that it is as simple as:
+//
+#include <slang.h>
+//
+// Other build setups are possible, and Slang doesn't assume that
+// its include directory must be added to your global include
+// path.
+
+// For the purposes of keeping the demo code as simple as possible,
+// while still retaining some level of portability, our examples
+// make use of a small platform and graphics API abstraction layer,
+// which is included in the Slang source distribution under the
+// `tools/` directory.
+//
+// Applications can of course use Slang without ever touching this
+// abstraction layer, so we will not focus on it when explaining
+// examples, except in places where best practices for interacting
+// with Slang may depend on an application/engine making certain
+// design choices in their abstraction layer.
+//
+#include "slang-com-ptr.h"
+#include "slang-gfx.h"
+#include "tools/graphics-app-framework/window.h"
+#include "../../prelude/slang-cpp-types.h"
+#include "source/core/slang-basic.h"
+
+using namespace gfx;
+
+// We create global ref pointers to avoid dereferencing values
+//
+ComPtr<gfx::IShaderProgram> gShaderProgram;
+Slang::ComPtr<gfx::IRenderer> gRenderer;
+
+ComPtr<gfx::IBufferResource> gStructuredBuffer;
+
+ComPtr<gfx::IPipelineLayout> gPipelineLayout;
+ComPtr<gfx::IPipelineState> gPipelineState;
+ComPtr<gfx::IDescriptorSetLayout> gDescriptorSetLayout;
+ComPtr<gfx::IDescriptorSet> gDescriptorSet;
+
+// Boilerplate types to help the slan-generated file
+//
+struct gfx_Window_0;
+struct gfx_Renderer_0;
+struct gfx_BufferResource_0;
+struct gfx_ShaderProgram_0;
+struct gfx_DescriptorSetLayout_0;
+struct gfx_PipelineLayout_0;
+struct gfx_DescriptorSet_0;
+struct gfx_PipelineState_0;
+
+bool executeComputation_0();
+extern unsigned char __computeMain[];
+extern size_t __computeMainSize;
+
+gfx::IShaderProgram* loadShaderProgram(gfx::IRenderer* renderer, unsigned char computeCode[], size_t computeCodeSize)
+{
+ // We extract the begin/end pointers to the output code buffers directly
+ //
+ char unsigned const* computeCodeEnd = computeCode + computeCodeSize;
+
+ // Now we use the operations of the example graphics API abstraction
+ // layer to load shader code into the underlying API.
+ //
+ // Reminder: this section does not involve the Slang API at all.
+ //
+
+ gfx::IShaderProgram::KernelDesc kernelDescs[] =
+ {
+ { gfx::StageType::Compute, computeCode, computeCodeEnd },
+ };
+
+ gfx::IShaderProgram::Desc programDesc = {};
+ programDesc.pipelineType = gfx::PipelineType::Compute;
+ programDesc.kernels = &kernelDescs[0];
+ programDesc.kernelCount = 1;
+
+ gShaderProgram = renderer->createProgram(programDesc);
+
+ return gShaderProgram;
+}
+
+// Now that we've covered the function that actually loads and
+// compiles our Slang shade code, we can go through the rest
+// of the application code without as much commentary.
+//
+gfx::Window* createWindow(int windowWidth, int windowHeight)
+{
+ // Create a window for our application to render into.
+ //
+ WindowDesc windowDesc;
+ windowDesc.title = "Hello, World!";
+ windowDesc.width = windowWidth;
+ windowDesc.height = windowHeight;
+ return createWindow(windowDesc);
+ //return globalWindow;
+}
+
+gfx::IRenderer* createRenderer(
+ int windowWidth,
+ int windowHeight,
+ gfx::Window* window)
+{
+ // Initialize the rendering layer.
+ //
+ // Note: for now we are hard-coding logic to use the
+ // Direct3D11 back-end for the graphics API abstraction.
+ // A future version of this example may support multiple
+ // platforms/APIs.
+ //
+ IRenderer::Desc rendererDesc = {};
+ rendererDesc.rendererType = gfx::RendererType::DirectX11;
+ Result res = gfxCreateRenderer(&rendererDesc, gRenderer.writeRef());
+
+ if (SLANG_FAILED(res)) return nullptr;
+ return gRenderer;
+}
+
+gfx::IBufferResource* createStructuredBuffer(gfx::IRenderer* renderer, float* initialArray)
+{
+ // Create a structured buffer for storing the data for computation
+ //
+ int structuredBufferSize = 4 * sizeof(float);
+
+ IBufferResource::Desc structuredBufferDesc;
+ structuredBufferDesc.init(structuredBufferSize);
+ structuredBufferDesc.setDefaults(IResource::Usage::UnorderedAccess);
+ structuredBufferDesc.elementSize = 4;
+ structuredBufferDesc.cpuAccessFlags = IResource::AccessFlag::Read;
+
+ gStructuredBuffer = renderer->createBufferResource(
+ IResource::Usage::UnorderedAccess,
+ structuredBufferDesc,
+ initialArray);
+ return gStructuredBuffer;
+}
+
+gfx::IDescriptorSetLayout* buildDescriptorSetLayout(gfx::IRenderer* renderer)
+{
+ // Our example graphics API usess a "modern" D3D12/Vulkan style
+ // of resource binding, so now we will dive into describing and
+ // allocating "descriptor sets."
+ //
+ // First, we need to construct a descriptor set *layout*.
+ //
+ IDescriptorSetLayout::SlotRangeDesc slotRanges[] =
+ {
+ IDescriptorSetLayout::SlotRangeDesc(DescriptorSlotType::StorageBuffer),
+ };
+ IDescriptorSetLayout::Desc descriptorSetLayoutDesc;
+ descriptorSetLayoutDesc.slotRangeCount = 1;
+ descriptorSetLayoutDesc.slotRanges = &slotRanges[0];
+ gDescriptorSetLayout = renderer->createDescriptorSetLayout(descriptorSetLayoutDesc);
+ return gDescriptorSetLayout;
+}
+
+gfx::IPipelineLayout* buildPipeline(gfx::IRenderer* renderer, gfx::IDescriptorSetLayout* descriptorSetLayout)
+{
+ // Next we will allocate a pipeline layout, which specifies
+ // that we will render with only a single descriptor set bound.
+ //
+
+ IPipelineLayout::DescriptorSetDesc descriptorSets[] =
+ {
+ IPipelineLayout::DescriptorSetDesc(descriptorSetLayout),
+ };
+ IPipelineLayout::Desc pipelineLayoutDesc;
+ pipelineLayoutDesc.renderTargetCount = 1;
+ pipelineLayoutDesc.descriptorSetCount = 1;
+ pipelineLayoutDesc.descriptorSets = &descriptorSets[0];
+ gPipelineLayout = renderer->createPipelineLayout(pipelineLayoutDesc);
+
+ return gPipelineLayout;
+}
+
+gfx::IDescriptorSet* buildDescriptorSet(
+ gfx::IRenderer* renderer,
+ gfx::IDescriptorSetLayout* descriptorSetLayout,
+ gfx::IBufferResource* structuredBuffer)
+{
+ // Once we have the descriptor set layout, we can allocate
+ // and fill in a descriptor set to hold our parameters.
+ //
+ gDescriptorSet = renderer->createDescriptorSet(descriptorSetLayout, gfx::IDescriptorSet::Flag::Transient);
+ if(!gDescriptorSet) return nullptr;
+
+ // Once we have the bufferResource created, we can fill in
+ // a descriptor set for creating a structured buffer
+ //
+ IResourceView::Desc resourceViewDesc;
+ resourceViewDesc.type = IResourceView::Type::UnorderedAccess;
+ auto resourceView = renderer->createBufferView(structuredBuffer, resourceViewDesc);
+ gDescriptorSet->setResource(0, 0, resourceView);
+
+ return gDescriptorSet;
+}
+
+gfx::IPipelineState* buildPipelineState(
+ gfx::IShaderProgram* shaderProgram,
+ gfx::IRenderer* renderer,
+ gfx::IPipelineLayout* pipelineLayout)
+{
+ // Following the D3D12/Vulkan style of API, we need a pipeline state object
+ // (PSO) to encapsulate the configuration of the overall graphics pipeline.
+ //
+ ComputePipelineStateDesc desc;
+ desc.pipelineLayout = pipelineLayout;
+ desc.program = shaderProgram;
+ gPipelineState = renderer->createComputePipelineState(desc);
+ return gPipelineState;
+}
+
+void printInitialValues(float* initialArray, int length)
+{
+ // Print out the values before the computation
+ printf("Before:\n");
+ for (int i = 0; i < length; i++)
+ {
+ printf("%f, ", initialArray[i]);
+ }
+ printf("\n");
+}
+
+void dispatchComputation(
+ gfx::ICommandQueue* gQueue,
+ gfx::IPipelineState* gPipelineState,
+ gfx::IPipelineLayout* gPipelineLayout,
+ gfx::IDescriptorSet* gDescriptorSet,
+ unsigned int gridDimsX,
+ unsigned int gridDimsY,
+ unsigned int gridDimsZ)
+{
+ auto cmdBuf = gQueue->createCommandBuffer();
+ auto encoder = cmdBuf->encodeComputeCommands();
+ encoder->setPipelineState(gPipelineState);
+ encoder->setDescriptorSet(PipelineType::Compute, gPipelineLayout, 0, gDescriptorSet);
+ encoder->dispatchCompute(gridDimsX, gridDimsY, gridDimsZ);
+ encoder->endEncoding();
+ gQueue->executeCommandBuffer(cmdBuf);
+}
+
+void print_output(
+ gfx::IRenderer* renderer,
+ gfx::IBufferResource* structuredBuffer,
+ int length)
+{
+ ComPtr<ISlangBlob> blob;
+ renderer->readBufferResource(structuredBuffer, 0, length * sizeof(float), blob.writeRef());
+ if (float* outputData = (float*)blob->getBufferPointer())
+ {
+ // Print out the values the the kernel produced
+ printf("After: \n");
+ for (int i = 0; i < 4; i++)
+ {
+ printf("%f, ", outputData[i]);
+ }
+ printf("\n");
+ }
+}
+
+// Boilerplate functions to help the slang-generated file and types
+gfx_Window_0* createWindow_0(int32_t _0, int32_t _1)
+{
+ return (gfx_Window_0*)createWindow(_0, _1);
+}
+
+gfx_Renderer_0* createRenderer_0(int32_t _0, int32_t _1, gfx_Window_0* _2)
+{
+ return (gfx_Renderer_0*)createRenderer(_0, _1, (gfx::Window*)_2);
+}
+
+gfx_BufferResource_0* createStructuredBuffer_0(gfx_Renderer_0* _0, FixedArray<float, 4> _1)
+{
+ return (gfx_BufferResource_0*)createStructuredBuffer((gfx::IRenderer*)_0, (float*)&_1);
+}
+
+gfx_ShaderProgram_0* loadShaderProgram_0(gfx_Renderer_0* _0, unsigned char _1[], size_t _2)
+{
+ return (gfx_ShaderProgram_0*)loadShaderProgram((gfx::IRenderer*)_0, _1, _2);
+}
+
+gfx_DescriptorSetLayout_0* buildDescriptorSetLayout_0(gfx_Renderer_0* _0)
+{
+ return (gfx_DescriptorSetLayout_0*)buildDescriptorSetLayout((gfx::IRenderer*)_0);
+}
+
+gfx_PipelineLayout_0* buildPipeline_0(gfx_Renderer_0* _0, gfx_DescriptorSetLayout_0* _1)
+{
+ return (gfx_PipelineLayout_0*)buildPipeline((gfx::IRenderer*)_0, (gfx::IDescriptorSetLayout*)_1);
+}
+
+gfx_DescriptorSet_0* buildDescriptorSet_0(gfx_Renderer_0* _0, gfx_DescriptorSetLayout_0* _1, gfx_BufferResource_0* _2)
+{
+ return (gfx_DescriptorSet_0*)buildDescriptorSet(
+ (gfx::IRenderer*)_0,
+ (gfx::IDescriptorSetLayout*)_1,
+ (gfx::IBufferResource*)_2);
+}
+
+gfx_PipelineState_0* buildPipelineState_0(gfx_ShaderProgram_0* _0, gfx_Renderer_0* _1, gfx_PipelineLayout_0* _2)
+{
+ return (gfx_PipelineState_0*)buildPipelineState(
+ (gfx::IShaderProgram*)_0, (gfx::IRenderer*)_1,
+ (gfx::IPipelineLayout*)_2);
+}
+
+void printInitialValues_0(FixedArray<float, 4> _0, int32_t _1)
+{
+ printInitialValues((float*)&_0, _1);
+}
+
+void dispatchComputation_0(gfx_CommandQueue_0* _0, gfx_PipelineState_0* _1, gfx_PipelineLayout_0* _2, gfx_DescriptorSet_0* _3, unsigned int gridDimsX, unsigned int gridDimsY, unsigned int gridDimsZ)
+{
+ dispatchComputation(
+ (gfx::ICommandQueue*)_0,
+ (gfx::IPipelineState*)_1,
+ (gfx::IPipelineLayout*)_2,
+ (gfx::IDescriptorSet*)_3,
+ gridDimsX,
+ gridDimsY,
+ gridDimsZ);
+}
+
+RWStructuredBuffer<float> convertBuffer_0(gfx_BufferResource_0* _0) {
+ RWStructuredBuffer<float> result;
+ result.data = (float*)_0;
+ return result;
+}
+
+gfx_BufferResource_0* unconvertBuffer_0(RWStructuredBuffer<float> _0) {
+ return (gfx_BufferResource_0*)(_0.data);
+}
+
+void print_output_0(gfx_CommandQueue_0* _0, gfx_BufferResource_0* _1, int32_t _2)
+{
+ print_output((gfx::ICommandQueue*)_0, (gfx::IBufferResource*)_1, _2);
+}
+
+// This "inner" main function is used by the platform abstraction
+// layer to deal with differences in how an entry point needs
+// to be defined for different platforms.
+//
+void innerMain(ApplicationContext* context)
+{
+ // We construct an instance of our example application
+ // `struct` type, and then walk through the lifecyle
+ // of the application.
+
+ if (!(executeComputation_0()))
+ {
+ return exitApplication(context, 1);
+ }
+}
+
+// This macro instantiates an appropriate main function to
+// invoke the `innerMain` above.
+//
+GFX_CONSOLE_MAIN(innerMain)
+
+#endif
diff --git a/examples/experimental/heterogeneous-hello-world/shader.cpp b/examples/experimental/heterogeneous-hello-world/shader.cpp
new file mode 100644
index 000000000..5a8dd7815
--- /dev/null
+++ b/examples/experimental/heterogeneous-hello-world/shader.cpp
@@ -0,0 +1,197 @@
+#if 0
+#include "../../prelude/slang-cpp-prelude.h"
+
+
+#ifdef SLANG_PRELUDE_NAMESPACE
+using namespace SLANG_PRELUDE_NAMESPACE;
+#endif
+
+Vector<uint32_t, 3> operator*(Vector<uint32_t, 3> a, Vector<uint32_t, 3> b)
+{
+ Vector<uint32_t, 3> r;
+ r.x = a.x * b.x;
+ r.y = a.y * b.y;
+ r.z = a.z * b.z;
+ return r;
+}
+
+Vector<uint32_t, 3> operator+(Vector<uint32_t, 3> a, Vector<uint32_t, 3> b)
+{
+ Vector<uint32_t, 3> r;
+ r.x = a.x + b.x;
+ r.y = a.y + b.y;
+ r.z = a.z + b.z;
+ return r;
+}
+
+Vector<uint32_t, 3> make_VecU3(uint32_t a, uint32_t b, uint32_t c)
+{
+ return Vector<uint32_t, 3>{ a, b, c};
+}
+
+size_t __computeMainSize = 668;
+unsigned char __computeMain[] = {68, 88, 66, 67, 87, 111, 81, 164, 2, 29, 72, 42, 151, 28, 13, 217, 55, 37, 7, 95, 1, 0, 0, 0, 156, 2, 0, 0, 5, 0, 0, 0, 52, 0, 0, 0, 8, 1, 0, 0, 24, 1, 0, 0, 40, 1, 0, 0, 32, 2, 0, 0, 82, 68, 69, 70, 204, 0, 0, 0, 1, 0, 0, 0, 88, 0, 0, 0, 1, 0, 0, 0, 28, 0, 0, 0, 0, 4, 83, 67, 0, 9, 16, 0, 164, 0, 0, 0, 60, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 101, 110, 116, 114, 121, 80, 111, 105, 110, 116, 80, 97, 114, 97, 109, 115, 95, 105, 111, 66, 117, 102, 102, 101, 114, 95, 48, 0, 60, 0, 0, 0, 1, 0, 0, 0, 112, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 136, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 148, 0, 0, 0, 0, 0, 0, 0, 36, 69, 108, 101, 109, 101, 110, 116, 0, 171, 171, 171, 0, 0, 3, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 105, 99, 114, 111, 115, 111, 102, 116, 32, 40, 82, 41, 32, 72, 76, 83, 76, 32, 83, 104, 97, 100, 101, 114, 32, 67, 111, 109, 112, 105, 108, 101, 114, 32, 49, 48, 46, 49, 0, 73, 83, 71, 78, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 79, 83, 71, 78, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 83, 72, 69, 88, 240, 0, 0, 0, 64, 0, 5, 0, 60, 0, 0, 0, 106, 8, 0, 1, 158, 0, 0, 4, 0, 224, 17, 0, 0, 0, 0, 0, 4, 0, 0, 0, 95, 0, 0, 2, 18, 0, 2, 0, 104, 0, 0, 2, 1, 0, 0, 0, 155, 0, 0, 4, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 167, 0, 0, 8, 18, 0, 16, 0, 0, 0, 0, 0, 10, 0, 2, 0, 1, 64, 0, 0, 0, 0, 0, 0, 6, 224, 17, 0, 0, 0, 0, 0, 49, 0, 0, 7, 34, 0, 16, 0, 0, 0, 0, 0, 10, 0, 16, 0, 0, 0, 0, 0, 1, 64, 0, 0, 0, 0, 0, 63, 0, 0, 0, 7, 66, 0, 16, 0, 0, 0, 0, 0, 10, 0, 16, 0, 0, 0, 0, 0, 10, 0, 16, 0, 0, 0, 0, 0, 75, 0, 0, 5, 18, 0, 16, 0, 0, 0, 0, 0, 10, 0, 16, 0, 0, 0, 0, 0, 55, 0, 0, 9, 18, 0, 16, 0, 0, 0, 0, 0, 26, 0, 16, 0, 0, 0, 0, 0, 42, 0, 16, 0, 0, 0, 0, 0, 10, 0, 16, 0, 0, 0, 0, 0, 168, 0, 0, 8, 18, 224, 17, 0, 0, 0, 0, 0, 10, 0, 2, 0, 1, 64, 0, 0, 0, 0, 0, 0, 10, 0, 16, 0, 0, 0, 0, 0, 62, 0, 0, 1, 83, 84, 65, 84, 116, 0, 0, 0, 7, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+void computeMain_wrapper(gfx_Renderer_0* renderer, Vector<uint32_t, 3> gridDims,
+ RWStructuredBuffer<float> buffer)
+{
+ gfx_ShaderProgram_0* shaderProgram = loadShaderProgram_0(renderer, __computeMain, __computeMainSize);
+ gfx_DescriptorSetLayout_0* setLayout = buildDescriptorSetLayout_0(renderer);
+ gfx_PipelineLayout_0* pipelineLayout = buildPipeline_0(renderer, setLayout);
+ gfx_DescriptorSet_0* descriptorSet = buildDescriptorSet_0(renderer, setLayout, unconvertBuffer_0(buffer));
+ gfx_PipelineState_0* pipelineState = buildPipelineState_0(shaderProgram, renderer, pipelineLayout);
+ dispatchComputation_0(renderer, pipelineState, pipelineLayout, descriptorSet, gridDims.x, gridDims.y, gridDims.z);
+}
+
+#line 7 "../../examples/heterogeneous-hello-world/shader.slang"
+struct EntryPointParams_0
+{
+ RWStructuredBuffer<float> ioBuffer_0;
+};
+
+struct KernelContext_0
+{
+};
+
+
+#line 21
+struct gfx_Window_0
+{
+};
+
+
+#line 22
+struct gfx_Renderer_0
+{
+};
+
+
+#line 23
+struct gfx_BufferResource_0
+{
+};
+
+
+#line 7
+void _computeMain(void* _S1, void* entryPointParams_0, void* _S2)
+{
+ ComputeThreadVaryingInput* _S3 = ((ComputeThreadVaryingInput*)(_S1));
+ KernelContext_0 kernelContext_0;
+
+#line 9
+ uint32_t tid_0 = (*(&_S3->groupID) * make_VecU3(4U, 1U, 1U) + *(&_S3->groupThreadID)).x;
+
+ float* _S4 = &(*(&((EntryPointParams_0*)(entryPointParams_0))->ioBuffer_0))[tid_0];
+
+#line 11
+ float i_0 = *_S4;
+ bool _S5 = i_0 < 0.50000000000000000000f;
+
+#line 12
+ float _S6 = i_0 + i_0;
+
+#line 12
+ float _S7 = (F32_sqrt((i_0)));
+
+#line 12
+ float o_0 = _S5 ? _S6 : _S7;
+
+ float* _S8 = &(*(&((EntryPointParams_0*)(entryPointParams_0))->ioBuffer_0))[tid_0];
+
+#line 14
+ *_S8 = o_0;
+
+#line 7
+ return;
+}
+
+
+#line 34
+gfx_Window_0* createWindow_0(int32_t _0, int32_t _1);
+
+
+#line 35
+gfx_Renderer_0* createRenderer_0(int32_t _0, int32_t _1, gfx_Window_0* _2);
+
+
+
+gfx_BufferResource_0* createStructuredBuffer_0(gfx_Renderer_0* _0, FixedArray<float, 4> _1);
+
+
+#line 4
+RWStructuredBuffer<float> convertBuffer_0(gfx_BufferResource_0* _0);
+
+
+#line 40
+void printInitialValues_0(FixedArray<float, 4> _0, int32_t _1);
+
+
+#line 41
+void print_output_0(gfx_Renderer_0* _0, gfx_BufferResource_0* _1, int32_t _2);
+
+
+
+
+bool executeComputation_0()
+{
+
+
+
+ FixedArray<float, 4> initialArray_0 = { 3.00000000000000000000f, -20.00000000000000000000f, -6.00000000000000000000f, 8.00000000000000000000f };
+
+
+ gfx_Window_0* _S9 = createWindow_0(int(1024), int(768));
+ gfx_Renderer_0* _S10 = createRenderer_0(int(1024), int(768), _S9);
+ gfx_CommandQueue_0* _
+ gfx_BufferResource_0* _S11 = createStructuredBuffer_0(_S10, initialArray_0);
+ Vector<uint32_t, 3> _S12 = make_VecU3(uint32_t(int(4)), uint32_t(int(1)), uint32_t(int(1)));
+ RWStructuredBuffer<float> _S13 = convertBuffer_0(_S11);
+
+#line 57
+ computeMain_wrapper(_S10, _S12, _S13);
+
+ printInitialValues_0(initialArray_0, int(4));
+ print_output_0(_S10, _S11, int(4));
+
+
+ return true;
+}
+
+// [numthreads(4, 1, 1)]
+SLANG_PRELUDE_EXPORT
+void computeMain_Thread(ComputeThreadVaryingInput* varyingInput, void* entryPointParams, void* globalParams)
+{
+ _computeMain(varyingInput, entryPointParams, globalParams);
+}
+// [numthreads(4, 1, 1)]
+SLANG_PRELUDE_EXPORT
+void computeMain_Group(ComputeVaryingInput* varyingInput, void* entryPointParams, void* globalParams)
+{
+ ComputeThreadVaryingInput threadInput = {};
+ threadInput.groupID = varyingInput->startGroupID;
+ for (uint32_t x = 0; x < 4; ++x)
+ {
+ threadInput.groupThreadID.x = x;
+ _computeMain(&threadInput, entryPointParams, globalParams);
+ }
+}
+// [numthreads(4, 1, 1)]
+SLANG_PRELUDE_EXPORT
+void computeMain(ComputeVaryingInput* varyingInput, void* entryPointParams, void* globalParams)
+{
+ ComputeVaryingInput vi = *varyingInput;
+ ComputeVaryingInput groupVaryingInput = {};
+ for (uint32_t z = vi.startGroupID.z; z < vi.endGroupID.z; ++z)
+ {
+ groupVaryingInput.startGroupID.z = z;
+ for (uint32_t y = vi.startGroupID.y; y < vi.endGroupID.y; ++y)
+ {
+ groupVaryingInput.startGroupID.y = y;
+ for (uint32_t x = vi.startGroupID.x; x < vi.endGroupID.x; ++x)
+ {
+ groupVaryingInput.startGroupID.x = x;
+ computeMain_Group(&groupVaryingInput, entryPointParams, globalParams);
+ }
+ }
+ }
+}
+#endif
diff --git a/examples/experimental/heterogeneous-hello-world/shader.slang b/examples/experimental/heterogeneous-hello-world/shader.slang
new file mode 100644
index 000000000..47c883b39
--- /dev/null
+++ b/examples/experimental/heterogeneous-hello-world/shader.slang
@@ -0,0 +1,65 @@
+// shader.slang
+
+//TEST_INPUT:ubuffer(random(float, 4096, -1.0, 1.0), stride=4):name=ioBuffer
+RWStructuredBuffer<float> convertBuffer(Ptr<gfx::BufferResource> x);
+
+[shader("compute")]
+[numthreads(4, 1, 1)]
+void computeMain(uniform RWStructuredBuffer<float> ioBuffer, uint3 dispatchThreadID : SV_DispatchThreadID)
+{
+ uint tid = dispatchThreadID.x;
+
+ float i = ioBuffer[tid];
+ float o = i < 0.5 ? (i + i) : sqrt(i);
+
+ ioBuffer[tid] = o;
+}
+
+// Forward declarations of gfx types
+//
+namespace gfx {
+ struct ApplicationContext{};
+ struct Window{};
+ struct Renderer{};
+ struct BufferResource{};
+ struct PipelineLayout{};
+ struct PipelineState{};
+ struct DescriptorSetLayout{};
+ struct DescriptorSet{};
+ struct ShaderProgram{};
+}
+
+// Forward declarations of cpp functions
+//
+Ptr<gfx::ShaderProgram> loadShaderProgram(Ptr<gfx::Renderer> renderer);
+Ptr<gfx::Window> createWindow(int gWindowWidth, int gWindowHeight);
+Ptr<gfx::Renderer> createRenderer(
+ int gWindowWidth,
+ int gWindowHeight,
+ Ptr<gfx::Window> gWindow);
+Ptr<gfx::BufferResource> createStructuredBuffer(Ptr<gfx::Renderer> gRenderer, float[4] initialArray);
+void printInitialValues(float[4] initialArray, int length);
+void print_output(
+ Ptr<gfx::Renderer> gRenderer,
+ Ptr<gfx::BufferResource> gStructuredBuffer,
+ int length);
+
+public bool executeComputation() {
+ // We will hard-code the size of our rendering window and initial array.
+ //
+ int windowWidth = 1024;
+ int windowHeight = 768;
+ float initialArray[4] = { 3.0f, -20.0f, -6.0f, 8.0f };
+
+ // Declare functions
+ let window = createWindow(windowWidth, windowHeight);
+ let renderer = createRenderer(windowWidth, windowHeight, window);
+ let structuredBuffer = createStructuredBuffer(renderer, initialArray);
+ __GPU_FOREACH(renderer, uint3(4, 1, 1), LAMBDA(uint3 dispatchThreadID)
+ { computeMain(convertBuffer(structuredBuffer), dispatchThreadID) ; });
+ printInitialValues(initialArray, 4);
+ print_output(renderer, structuredBuffer, 4);
+
+
+ return true;
+}
diff --git a/examples/experimental/model-viewer/README.md b/examples/experimental/model-viewer/README.md
new file mode 100644
index 000000000..a350a48a2
--- /dev/null
+++ b/examples/experimental/model-viewer/README.md
@@ -0,0 +1,25 @@
+Model Viewer Example
+====================
+
+This example expands on the simple Slang API integration from the "Hello, World" example by actually loading and rendering model data with extremely basic surface and light shading.
+
+This time, the shader code is making use of various Slang language features, so readers may want to read through `shaders.slang` to see an example of how the various mechanisms can be used to build out a more complicated shader library.
+While the shader code in this example is still simplistic, it shows examples of:
+
+* Using multiple Slang `ParameterBlock`s to manage the space of shader parameter bindings in a graphics-API-independent fashion, while still taking advantage of the performance opportunities afforded by D3D12 and Vulkan.
+
+* Using `interface`s and generics to express multiple variations of a feature with static specialization, in place of more traditional preprocessor techniques.
+
+The application code in `main.cpp` also shows a more advanced integration of the Slang API than that in the "Hello, World" example, including examples of:
+
+* Loading a library of Slang shader code to perform reflection on its types *without* specifying a particular entry point to generate code for
+
+* Using Slang's reflection information to allocate graphics-API objects to implement parameter blocks (e.g., D3D12/Vulkan descriptor tables/sets)
+
+* Performing on-demand specialization of Slang's generics using type information from parameter blocks to achieve simple shader specialization
+
+It is perhaps worth taking note of the two things this example intentionally does *not* do:
+
+* There is no use of the C-style preprocessor in the shader code presented, in order to demonstrate that shader specialization can be achieved without preprocessor techniques.
+
+* There is no use of explicit parameter binding decorations (e.g., HLSL `regsiter` or GLSL `layout` modifiers), in order to demonstrate that these are not needed in order to achieve high-performance shader parameter binding.
diff --git a/examples/experimental/model-viewer/cube.mtl b/examples/experimental/model-viewer/cube.mtl
new file mode 100644
index 000000000..6c8eeb10b
--- /dev/null
+++ b/examples/experimental/model-viewer/cube.mtl
@@ -0,0 +1,35 @@
+newmtl Red
+Ns 95
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.30000 0.30000
+Ks 0.500000 0.200000 0.200000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Green
+Ns 20
+Ka 0.000000 0.000000 0.000000
+Kd 0.20000 0.640000 0.20000
+Ks 0.100000 0.500000 0.100000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Blue
+Ns 200
+Ka 0.000000 0.000000 0.000000
+Kd 0.10000 0.10000 0.20000
+Ks 0.200000 0.200000 0.700000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Ground
+Ns 10
+Ka 0.000000 0.000000 0.000000
+Kd 0.25 0.25 0.25
+Ks 0.1 0.1 0.1
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/examples/experimental/model-viewer/main.cpp b/examples/experimental/model-viewer/main.cpp
new file mode 100644
index 000000000..d4bc21776
--- /dev/null
+++ b/examples/experimental/model-viewer/main.cpp
@@ -0,0 +1,2446 @@
+// This example is out of date and currently disabled from build.
+// The `gfx` layer has been refactored with a new shader-object model
+// that will greatly simplify shader binding and specialization.
+// This example should be updated to use the shader-object API in `gfx`.
+
+#if 0
+// main.cpp
+
+//
+// This example is much more involved than the `hello-world` example,
+// so readers are encouraged to work through the simpler code first
+// before diving into this application. We will gloss over parts of
+// the code that are similar to the code in `hello-world`, and
+// instead focus on the new code that is required to use Slang in
+// more advanced ways.
+//
+
+// We still need to include the Slang header to use the Slang API
+//
+#include <slang.h>
+#include "slang-com-helper.h"
+// We will again make use of a simple graphics API abstraction
+// layer, just to keep the examples short and to the point.
+//
+#include "graphics-app-framework/model.h"
+#include "slang-gfx.h"
+#include "graphics-app-framework/vector-math.h"
+#include "graphics-app-framework/window.h"
+#include "graphics-app-framework/gui.h"
+using namespace gfx;
+using Slang::RefObject;
+using Slang::RefPtr;
+// We will use a few utilities from the C++ standard library,
+// just to keep the code short. Note that the Slang API does
+// not use or require any C++ standard library features.
+//
+#include <map>
+#include <memory>
+#include <string>
+#include <sstream>
+#include <vector>
+
+// A larger application will typically want to load/compile
+// multiple modules/files of shader code. When using the
+// Slang API, some one-time setup work can be amortized
+// across multiple modules by using a single Slang
+// "session" across multiple compiles.
+//
+// To that end, our application will use a function-`static`
+// variable to create a session on demand and re-use it
+// for the duration of the application.
+//
+SlangSession* getSlangSession()
+{
+ static SlangSession* slangSession = spCreateSession(NULL);
+ return slangSession;
+}
+
+// This application is going to build its own layered
+// application-specific abstractions on top of Slang,
+// so it will have its own notion of a shader "module,"
+// which comprises the results of a Slang compilation,
+// including the reflection information.
+//
+struct ShaderModule : RefObject
+{
+ // The file that the module was loaded from.
+ std::string inputPath;
+
+ // Slang compile request and reflection data.
+ SlangCompileRequest* slangRequest;
+ slang::ShaderReflection* slangReflection;
+
+ // Reference to the renderer, used to service requests
+ // that load graphics API objects based on the module.
+ Slang::ComPtr<gfx::IRenderer> renderer;
+};
+//
+// In order to load a shader module from a `.slang` file on
+// disk, we will use a Slang compile session, much like
+// how the earlier Hello World example loaded shader code.
+//
+// We will point out major differences between the earlier
+// example's `loadShaderProgram()` function, and how this function
+// loads a module for reflection purposes.
+//
+RefPtr<ShaderModule> loadShaderModule(IRenderer* renderer, char const* inputPath)
+{
+ auto slangSession = getSlangSession();
+ SlangCompileRequest* slangRequest = spCreateCompileRequest(slangSession);
+
+ // When *loading* the shader library, we will request that concrete
+ // kernel code *not* be generated, because the module might have
+ // unspecialized generic parameters. Instead, we will generate kernels
+ // on demand at runtime.
+ //
+ spSetCompileFlags(
+ slangRequest,
+ SLANG_COMPILE_FLAG_NO_CODEGEN);
+
+ // The main logic for specifying target information and loading source
+ // code is the same as before with the notable change that we are *not*
+ // specifying specific vertex/fragment entry points to compile here.
+ //
+ // Instead, the `[shader(...)]` attributes used in `shaders.slang` will
+ // identify the entry points in the shader library to the compiler with
+ // specific action needing to be taken in the application.
+ //
+ int targetIndex = spAddCodeGenTarget(slangRequest, SLANG_DXBC);
+ spSetTargetProfile(slangRequest, targetIndex, spFindProfile(slangSession, "sm_4_0"));
+ int translationUnitIndex = spAddTranslationUnit(slangRequest, SLANG_SOURCE_LANGUAGE_SLANG, nullptr);
+ spAddTranslationUnitSourceFile(slangRequest, translationUnitIndex, inputPath);
+ int compileErr = spCompile(slangRequest);
+ if(auto diagnostics = spGetDiagnosticOutput(slangRequest))
+ {
+ reportError("%s", diagnostics);
+ }
+ if(compileErr)
+ {
+ spDestroyCompileRequest(slangRequest);
+ spDestroySession(slangSession);
+ return nullptr;
+ }
+ auto slangReflection = (slang::ShaderReflection*) spGetReflection(slangRequest);
+
+ // We will not destroy the Slang compile request here, because we want to
+ // keep it around to service reflection quries made from the application code.
+ //
+ RefPtr<ShaderModule> module = new ShaderModule();
+ module->renderer = renderer;
+ module->inputPath = inputPath;
+ module->slangRequest = slangRequest;
+ module->slangReflection = slangReflection;
+ return module;
+}
+
+// Once a shader moduel has been loaded, it is possible to look up
+// individual entry points by their name to get reflection information,
+// including the stage for which the entry point was compiled.
+//
+// As with `ShaderModule` above, the `EntryPoint` type is the application's
+// wrapper around a Slang entry point. In this case it caches the
+// identity of the target stage as encoded for the graphics API.
+//
+struct EntryPoint : RefObject
+{
+ // Name of the entry point function
+ std::string name;
+
+ // Stage targetted by the entry point (Slang version)
+ SlangStage slangStage;
+
+ // Stage targetted by the entry point (graphics API version)
+ gfx::StageType apiStage;
+};
+//
+// Loading an entry point from a module is a straightforward
+// application of the Slang reflection API.
+//
+RefPtr<EntryPoint> loadEntryPoint(
+ ShaderModule* module,
+ char const* name)
+{
+ auto slangReflection = module->slangReflection;
+
+ // Look up the Slang entry point based on its name, and bail
+ // out with an error if it isn't found.
+ //
+ auto slangEntryPoint = slangReflection->findEntryPointByName(name);
+ if(!slangEntryPoint) return nullptr;
+
+ // Extract the stage of the entry point using the Slang API,
+ // and then try to map it to the corresponding stage as
+ // exposed by the graphics API.
+ //
+ auto slangStage = slangEntryPoint->getStage();
+ StageType apiStage = StageType::Unknown;
+ switch(slangStage)
+ {
+ default:
+ return nullptr;
+
+ case SLANG_STAGE_VERTEX: apiStage = gfx::StageType::Vertex; break;
+ case SLANG_STAGE_FRAGMENT: apiStage = gfx::StageType::Fragment; break;
+ }
+
+ // Allocate an application object to hold on to this entry point
+ // so that we can use it in later specialization steps.
+ //
+ RefPtr<EntryPoint> entryPoint = new EntryPoint();
+ entryPoint->name = name;
+ entryPoint->slangStage = slangEntryPoint->getStage();
+ entryPoint->apiStage = apiStage;
+ return entryPoint;
+}
+
+// In this application a `Program` represents a combination of entry
+// points that will be used together (e.g., matching vertex and fragment
+// entry points).
+//
+// Along with the entry points themselves, the `Program` object will
+// cache information gleaned from Slang's reflection interface. Notably:
+//
+// * The number of `ParameterBlock`s that the program uses
+// * Information about generic (type) parameters
+//
+struct Program : RefObject
+{
+ // The shader module that the program was loaded from.
+ RefPtr<ShaderModule> shaderModule;
+
+ // The entry points that comprise the program
+ // (e.g., both a vertex and a fragment entry point).
+ std::vector<RefPtr<EntryPoint>> entryPoints;
+
+ // The number of parameter blocks that are used by the shader
+ // program. This will be used by our rendering code later to
+ // decide how many descriptor set bindings should affect
+ // specialization/execution using this program.
+ //
+ int parameterBlockCount;
+
+ // We will store information about the generic (type) parameters
+ // of the program. In particular, for each generic parameter
+ // we are going to find a parameter block that uses that
+ // generic type parameter.
+ //
+ // E.g., given input code like:
+ //
+ // type_param A;
+ // type_param B;
+ //
+ // ParameterBlock<B> x; // block 0
+ // ParameterBlock<Foo> y; // block 1
+ // ParameterBlock<A> z; // block 2
+ //
+ // We would have two `GenericParam` entries. The first one,
+ // for `A`, would store a `parameterBlockIndex` of `2`, because
+ // `A` is used as the type of the `x` parameter block.
+ //
+ // This information will be used later when we want to specialize
+ // shader code, because if `z` is bound using a `ParameterBlock<Bar>`
+ // then we can infer that `A` should be bound to `Bar`.
+ //
+ struct GenericParam
+ {
+ int parameterBlockIndex;
+ };
+ std::vector<GenericParam> genericParams;
+};
+//
+// As with entry points, loading a program is done with
+// the help of Slang's reflection API.
+//
+RefPtr<Program> loadProgram(
+ ShaderModule* module,
+ int entryPointCount,
+ const char* const* entryPointNames)
+{
+ auto slangReflection = module->slangReflection;
+
+ RefPtr<Program> program = new Program();
+ program->shaderModule = module;
+
+ // We will loop over the entry point names that were requested,
+ // loading each and adding it to our program.
+ //
+ for(int ee = 0; ee < entryPointCount; ++ee)
+ {
+ auto entryPoint = loadEntryPoint(module, entryPointNames[ee]);
+ if(!entryPoint)
+ return nullptr;
+ program->entryPoints.push_back(entryPoint);
+ }
+
+ // Next, we will look at the reflection information to see how
+ // many generic type parameters were declared, and allocate
+ // space in the `genericParams` array for them.
+ //
+ // We don't yet have enough information to fill in the
+ // `parameterBlockIndex` field.
+ //
+ auto genericParamCount = slangReflection->getTypeParameterCount();
+ for(unsigned int pp = 0; pp < genericParamCount; ++pp)
+ {
+ auto slangGenericParam = slangReflection->getTypeParameterByIndex(pp);
+
+ Program::GenericParam genericParam = {};
+ program->genericParams.push_back(genericParam);
+ }
+
+ // We want to specialize our shaders based on what gets bound
+ // in parameter blocks, so we will scan the shader parameters
+ // looking for `ParameterBlock<G>` where `G` is one of our
+ // generic type parameters.
+ //
+ // We do this by iterating over *all* the global shader paramters,
+ // and looking for those that happen to be parameter blocks, and
+ // of those the ones where the "element type" of the parameter block
+ // is a generic type parameter.
+ //
+ auto paramCount = slangReflection->getParameterCount();
+ int parameterBlockCounter = 0;
+ for(unsigned int pp = 0; pp < paramCount; ++pp)
+ {
+ auto slangParam = slangReflection->getParameterByIndex(pp);
+
+ // Is it a parameter block? If not, skip it.
+ if(slangParam->getType()->getKind() != slang::TypeReflection::Kind::ParameterBlock)
+ continue;
+
+ // Okay, we've found another parameter block, so we can compute its zero-based index.
+ int parameterBlockIndex = parameterBlockCounter++;
+
+ // Get the element type of the parameter block, and if it isn't a generic type
+ // parameter, then skip it.
+ auto slangElementTypeLayout = slangParam->getTypeLayout()->getElementTypeLayout();
+ if(slangElementTypeLayout->getKind() != slang::TypeReflection::Kind::GenericTypeParameter)
+ continue;
+
+ // At this point we've found a `ParameterBlock<G>` where `G` is a `type_param`,
+ // so we can store the index of the parameter block back into our array of
+ // generic type parameter info.
+ //
+ auto genericParamIndex = slangElementTypeLayout->getGenericParamIndex();
+ program->genericParams[genericParamIndex].parameterBlockIndex = parameterBlockIndex;
+ }
+
+ // The above loop over the global shader parameters will have found all the
+ // parameter blocks that were specified in the shader code, so now we know
+ // how many parameter blocks are expected to be bound when this program is used.
+ //
+ program->parameterBlockCount = parameterBlockCounter;
+
+ return program;
+}
+//
+// As a convenience, we will define a simple wrapper around `loadProgram` for the case
+// where we have just two entry points, since that is what the application actually uses.
+//
+RefPtr<Program> loadProgram(ShaderModule* module, char const* entryPoint0, char const* entryPoint1)
+{
+ char const* entryPointNames[] = { entryPoint0, entryPoint1 };
+ return loadProgram(module, 2, entryPointNames);
+}
+
+// The `ParameterBlock<T>` type is supported by the Slang language and compiler,
+// but it is up to each application to map it down to whatever graphics API
+// abstraction is most fitting.
+//
+// For our application, a parameter block will be implemented as a combination
+// of Slang type reflection information (to determine the layout) plus a
+// graphics API descriptor set object.
+//
+// Note: the example graphics API abstraction we are using exposes descriptor sets
+// similar to those in Vulkan, and then maps these down to efficient alternatives
+// on other APIs including D3D12, D3D11, and OpenGL.
+//
+// Before we dive into the definition of the application's `ParameterBlock` type,
+// we will start with some underlying types.
+//
+// Every parameter block is allocated based on a particular layout, and we
+// can share the same layout across multiple blocks:
+//
+struct ParameterBlockLayout : RefObject
+{
+ // The graphics API device that should be used to allocate parameter
+ // block instances.
+ //
+ Slang::ComPtr<gfx::IRenderer> renderer;
+
+ // The name of the type, as it appears in Slang code.
+ //
+ std::string typeName;
+
+ // The Slang type layout information that will be used to decide
+ // how much space is needed in instances of this layout.
+ //
+ // If the user declares a `ParameterBlock<Batman>` parameter, then
+ // this will be the type layout information for `Batman`.
+ //
+ slang::TypeLayoutReflection* slangTypeLayout;
+
+ // The size of the "primary" constant buffer that will hold any
+ // "ordinary" (not-resource) fields in the `slangTypeLayout` above.
+ //
+ size_t primaryConstantBufferSize;
+
+ // API-specific layout information computes from `slangTypelayout`.
+ //
+ ComPtr<gfx::IDescriptorSetLayout> descriptorSetLayout;
+};
+//
+// A parameter block layout can be computed for any `struct` type
+// declared in the user's shade code. We extract the relevant
+// information from the type using the Slang reflection API.
+//
+RefPtr<ParameterBlockLayout> getParameterBlockLayout(
+ ShaderModule* module,
+ char const* name)
+{
+ auto slangReflection = module->slangReflection;
+ auto renderer = module->renderer;
+
+ // Look up the type with the given name, and bail out
+ // if no such type is found in the module.
+ //
+ auto type = slangReflection->findTypeByName(name);
+ if(!type) return nullptr;
+
+ // Request layout information for the type. Note that a single
+ // type might be laid out differently for different compilation
+ // targets, or based on how it is used (e.g., as a `cbuffer`
+ // field vs. in a `StructuredBuffer`).
+ //
+ auto typeLayout = slangReflection->getTypeLayout(type);
+ if(!typeLayout) return nullptr;
+
+ // If the type that is going in the parameter block has
+ // any ordinary data in it (as opposed to resources), then
+ // a constant buffer will be needed to hold that data.
+ //
+ // In turn any resource parameters would need to go into
+ // the descriptor set *after* this constant buffer.
+ //
+ size_t primaryConstantBufferSize = typeLayout->getSize(SLANG_PARAMETER_CATEGORY_UNIFORM);
+
+ // We need to use the Slang reflection information to
+ // create a graphics-API-level descriptor-set layout that
+ // is compatible with the original declaration.
+ //
+ std::vector<gfx::IDescriptorSetLayout::SlotRangeDesc> slotRanges;
+
+ // If the type has any ordinary data, then the descriptor set
+ // will need a constant buffer to be the first thing it stores.
+ //
+ // Note: for a renderer only targetting D3D12, it might make
+ // sense to allocate this "primary" constant buffer as a root
+ // descriptor instead of inside the descriptor set (or at least
+ // do this *if* there are no non-uniform parameters). Policy
+ // decisions like that are up to the application, not Slang.
+ // This example application just does something simple.
+ //
+ if(primaryConstantBufferSize)
+ {
+ slotRanges.push_back(
+ gfx::IDescriptorSetLayout::SlotRangeDesc(
+ gfx::DescriptorSlotType::UniformBuffer));
+ }
+
+ // Next, the application will recursively walk
+ // the structure of `typeLayout` to figure out what resource
+ // binding ranges are required for the target API.
+ //
+ // TODO: This application doesn't yet use any resource parameters,
+ // so we are skipping this step, but it is obviously needed
+ // for a fully fleshed-out example.
+
+ // Now that we've collected the graphics-API level binding
+ // information, we can construct a graphics API descriptor set
+ // layout.
+ gfx::IDescriptorSetLayout::Desc descriptorSetLayoutDesc;
+ descriptorSetLayoutDesc.slotRangeCount = slotRanges.size();
+ descriptorSetLayoutDesc.slotRanges = slotRanges.data();
+ auto descriptorSetLayout = renderer->createDescriptorSetLayout(descriptorSetLayoutDesc);
+ if(!descriptorSetLayout) return nullptr;
+
+ RefPtr<ParameterBlockLayout> parameterBlockLayout = new ParameterBlockLayout();
+ parameterBlockLayout->renderer = renderer;
+ parameterBlockLayout->primaryConstantBufferSize = primaryConstantBufferSize;
+ parameterBlockLayout->typeName = name;
+ parameterBlockLayout->slangTypeLayout = typeLayout;
+ parameterBlockLayout->descriptorSetLayout = descriptorSetLayout;
+ return parameterBlockLayout;
+}
+//
+// In some cases, we may want to create a parameter block based
+// on a *generic* type in the shader code (e.g., `LightPair<A,B>`).
+//
+// The current Slang API re-uses the `findTypeByName()` operation to
+// support specialization of types, by allowing the user to pass in
+// the string name of a sepcialized type and have the Slang runtime
+// system parse it.
+//
+// Note: a future version of the Slang API may streamline this operation
+// so that less application code is needed.
+//
+// In order to construct the string name of a type like `LightArray<X,3>`
+// we need a uniform encoding of the generic *arguments* `X` and `3`.
+// We use the `SpecializationArg` for this:
+//
+struct SpecializationArg
+{
+ // A `SpecializationArg` is just a thing wrapper around a string,
+ // with support for implicit conversions from the values we might
+ // use as specialization arguments.
+
+ SpecializationArg(Int val)
+ {
+ str = std::to_string(val);
+ }
+ SpecializationArg(RefPtr<ParameterBlockLayout> layout)
+ {
+ str = layout->typeName;
+ }
+
+ std::string str;
+};
+//
+// Now, given the name of a type to specialize and its specialization
+// arguments, we can easily construct the string name of the specialized
+// type and defer to the existing `getParameterBlockLayout()`.
+//
+RefPtr<ParameterBlockLayout> getSpecializedParameterBlockLayout(
+ ShaderModule* module,
+ char const* name,
+ Int argCount,
+ SpecializationArg const* args)
+{
+ std::stringstream stream;
+ stream << name << "<";
+ for (Int aa = 0; aa < argCount; ++aa)
+ {
+ if (aa != 0) stream << ",";
+ stream << args[aa].str;
+ }
+ stream << ">";
+
+ std::string specializedName = stream.str();
+ return getParameterBlockLayout(module, specializedName.c_str());
+}
+RefPtr<ParameterBlockLayout> getSpecializedParameterBlockLayout(
+ ShaderModule* module,
+ char const* name,
+ SpecializationArg const& arg0,
+ SpecializationArg const& arg1)
+{
+ SpecializationArg args[] = { arg0, arg1 };
+ return getSpecializedParameterBlockLayout(module, name, 2, args);
+}
+
+// In order to allow parameter blocks to be filled in conveniently,
+// we will introduce a helper type for "encoding" parameter blocks
+// (those familiar with the Metal API may recognize a similarity
+// to the `MTLArgumentEncoder` type).
+//
+struct ParameterBlockEncoder
+{
+ // The parameter block being filled in (if this is
+ // a "top-level" encoder.
+ //
+ struct ParameterBlock* parameterBlock = nullptr;
+
+ // A top-level encoder will unmap the underlying constant
+ // buffer (if any) when it goes out of scope.
+ //
+ void finishEncoding();
+
+ // The underlying descriptor set being filled in.
+ //
+ gfx::IDescriptorSet* descriptorSet = nullptr;
+
+ // The Slang type information for the part of the
+ // block that we are filling in. This might be the
+ // type stored in the whole block, the type of a single
+ // field, or anything in between.
+ //
+ slang::TypeLayoutReflection* slangTypeLayout = nullptr;
+
+ // A pointer to the uniform data for the (sub)block
+ // being filled in, as well as offsets for the resource
+ // binding ranges.
+ //
+ char* uniformData = nullptr;
+ Int rangeOffset = 0;
+ Int rangeArrayIndex = 0;
+
+ // Assuming we have an encoder for a `struct` type,
+ // return an encoder for a single field by its index.
+ //
+ ParameterBlockEncoder beginField(Int fieldIndex)
+ {
+ assert(slangTypeLayout->getKind() == slang::TypeReflection::Kind::Struct);
+
+ auto slangField = slangTypeLayout->getFieldByIndex((unsigned int)fieldIndex);
+ auto fieldUniformOffset = slangField->getOffset();
+
+ // TODO: this type needs to be extended to handle resource fields.
+ size_t fieldRangeOffset = 0;
+
+ ParameterBlockEncoder subEncoder;
+ subEncoder.descriptorSet = descriptorSet;
+ subEncoder.slangTypeLayout = slangField->getTypeLayout();
+ subEncoder.uniformData = uniformData + fieldUniformOffset;
+ subEncoder.rangeOffset = rangeOffset + fieldRangeOffset;
+ subEncoder.rangeArrayIndex = rangeArrayIndex;
+ return subEncoder;
+ }
+
+ // Assuming we have an encoder for an array type, return an
+ // encoder for an element of that array.
+ //
+ ParameterBlockEncoder beginArrayElement(Int index)
+ {
+ assert(slangTypeLayout->getKind() == slang::TypeReflection::Kind::Array);
+
+ auto uniformStride = slangTypeLayout->getElementStride(slang::ParameterCategory::Uniform);
+ auto slangElementTypeLayout = slangTypeLayout->getElementTypeLayout();
+
+ ParameterBlockEncoder subEncoder;
+ subEncoder.descriptorSet = descriptorSet;
+ subEncoder.slangTypeLayout = slangElementTypeLayout;
+ subEncoder.uniformData = uniformData + index * uniformStride;
+ subEncoder.rangeOffset = rangeOffset;
+ subEncoder.rangeArrayIndex = index;
+ return subEncoder;
+ }
+
+ // Write uniform data into this encoder.
+ //
+ void writeUniform(const void* data, size_t dataSize)
+ {
+ memcpy(uniformData, data, dataSize);
+ }
+ template<typename T>
+ void write(T const& value)
+ {
+ writeUniform(&value, sizeof(value));
+ }
+
+ // As a convenience, create a sub-encoder for a single field,
+ // and write a single value into it.
+ //
+ template<typename T>
+ void writeField(Int fieldIndex, T const& value)
+ {
+ beginField(fieldIndex).write(value);
+ }
+};
+
+// With the layout and encoder types dealt with, we are now
+// prepared to
+// A `ParameterBlock` abstracts over the allocated storage
+// for a descriptor set, based on some `ParameterBlockLayout`
+//
+struct ParameterBlock : RefObject
+{
+ // The graphics API device used to allocate this block.
+ Slang::ComPtr<gfx::IRenderer> renderer;
+
+ // The associated parameter block layout.
+ RefPtr<ParameterBlockLayout> layout;
+
+ // The (optional) constant buffer that holds the values
+ // for any ordinay fields. This will be null if
+ // `layout->primaryConstantBufferSize` is zero.
+ ComPtr<IBufferResource> primaryConstantBuffer;
+
+ // The graphics-API descriptor set that provides storage
+ // for any resource fields.
+ ComPtr<gfx::IDescriptorSet> descriptorSet;
+
+ ParameterBlockEncoder beginEncoding();
+};
+
+// Allocating a parameter block is mostly a matter of allocating
+// the required graphics API objects.
+//
+RefPtr<ParameterBlock> allocateParameterBlockImpl(
+ ParameterBlockLayout* layout)
+{
+ auto renderer = layout->renderer;
+
+ // A descriptor set is then used to provide the storage for all
+ // resource parameters (including the primary constant buffer, if any).
+ //
+ auto descriptorSet = renderer->createDescriptorSet(
+ layout->descriptorSetLayout, gfx::IDescriptorSet::Flag::Transient);
+
+ // If the parameter block has any ordinary data, then it requires
+ // a "primary" constant buffer to hold that data.
+ //
+ ComPtr<gfx::IBufferResource> primaryConstantBuffer = nullptr;
+ if(auto primaryConstantBufferSize = layout->primaryConstantBufferSize)
+ {
+ gfx::IBufferResource::Desc bufferDesc;
+ bufferDesc.init(primaryConstantBufferSize);
+ bufferDesc.setDefaults(gfx::IResource::Usage::ConstantBuffer);
+ bufferDesc.cpuAccessFlags = gfx::IResource::AccessFlag::Write;
+ primaryConstantBuffer = renderer->createBufferResource(
+ gfx::IResource::Usage::ConstantBuffer,
+ bufferDesc);
+
+ // The primary constant buffer will always be the first thing
+ // stored in the descriptor set for a parameter block.
+ //
+ descriptorSet->setConstantBuffer(0, 0, primaryConstantBuffer);
+ }
+
+ // Now that we've allocated the graphics API objects, we can just
+ // allocate our application-side wrapper object to tie everything
+ // together.
+ //
+ RefPtr<ParameterBlock> parameterBlock = new ParameterBlock();
+ parameterBlock->renderer = renderer;
+ parameterBlock->layout = layout;
+ parameterBlock->primaryConstantBuffer = primaryConstantBuffer;
+ parameterBlock->descriptorSet = descriptorSet;
+ return parameterBlock;
+}
+
+// A full-featured high-performance application would likely draw
+// a distinction between "persistent" parameter blocks that are
+// filled in once and then used over many frames, and "transient"
+// blocks that are allocated, filled in, and discarded within
+// a single frame.
+//
+// These two cases warrant very different allocation strategies,
+// but for now we are using the same logic in both cases.
+//
+RefPtr<ParameterBlock> allocatePersistentParameterBlock(
+ ParameterBlockLayout* layout)
+{
+ return allocateParameterBlockImpl(layout);
+}
+RefPtr<ParameterBlock> allocateTransientParameterBlock(
+ ParameterBlockLayout* layout)
+{
+ return allocateParameterBlockImpl(layout);
+}
+
+// In order to fill in a parameter block, the application
+// will create an encoder pointing at the mapped uniform
+// data for the block:
+//
+ParameterBlockEncoder ParameterBlock::beginEncoding()
+{
+ ParameterBlockEncoder encoder;
+ encoder.parameterBlock = this;
+ encoder.descriptorSet = descriptorSet;
+ encoder.slangTypeLayout = layout->slangTypeLayout;
+ encoder.uniformData = primaryConstantBuffer ?
+ (char*) renderer->map(
+ primaryConstantBuffer,
+ MapFlavor::WriteDiscard)
+ : nullptr;
+ encoder.rangeOffset = 0;
+ encoder.rangeArrayIndex = 0;
+ return encoder;
+}
+
+void ParameterBlockEncoder::finishEncoding()
+{
+ if (parameterBlock && uniformData)
+ {
+ parameterBlock->renderer->unmap(
+ parameterBlock->primaryConstantBuffer);
+ }
+}
+
+// The core of our application's rendering abstraction is
+// the notion of an "effect," which ties together a particular
+// set of shader entry points (as a `Program`), with graphics
+// API state objects for the fixed-function parts of the pipeline.
+//
+// Note that the program here is an *unspecialized* program,
+// which might have unbound global `type_param`s. Thus the
+// `Effect` type here is not one-to-one with a "pipeline state
+// object," because the same effect could be used to instantiate
+// multiple pipeline state objects based on how things get
+// specialized.
+//
+struct Effect : RefObject
+{
+ // The shader program entry point(s) to execute
+ RefPtr<Program> program;
+
+ // Additional state corresponding to the data needed
+ // to create a graphics-API pipeline state object.
+ ComPtr<gfx::IInputLayout> inputLayout;
+ Int renderTargetCount;
+};
+
+// In order to render using the `Effect` abstraction, our
+// application will be creating various specialized
+// shader kernels and pipeline states on-demand.
+//
+// We'll start with the representation of a specialized
+// "variant" of an effect.
+//
+struct EffectVariant : RefObject
+{
+ // The graphics API pipeline layout and state
+ // that need to be bound in order to use this
+ // effect.
+ //
+ ComPtr<gfx::IPipelineLayout> pipelineLayout;
+ ComPtr<gfx::IPipelineState> pipelineState;
+};
+//
+// A specialized variant is created based on a base effect
+// and the types that will be bound to its parameter blocks.
+//
+RefPtr<EffectVariant> createEffectVaraint(
+ Effect* effect,
+ UInt parameterBlockCount,
+ ParameterBlockLayout* const* parameterBlockLayouts,
+ IFramebufferLayout* framebufferLayout)
+{
+ // One note to make at the very start is that the creation
+ // of a specialized variant is based on the *layout* of
+ // the parameter blocks in use and not on the particular
+ // parameter blocks themselves. This is important because
+ // it means that, e.g., two materials that use the same code,
+ // but different parameter values (different textures, colors,
+ // etc.) do *not* require switching between different
+ // shader code or specialized PSOs.
+
+ // We'll start by extracting some of the pieces of
+ // information taht we need into local variables,
+ // just to simplify the remaining code.
+ //
+ auto program = effect->program;
+ auto shaderModule = program->shaderModule;
+ auto renderer = shaderModule->renderer;
+
+ // Our specialized effect is going to need a few things:
+ //
+ // 1. A specialized pipeline layout, based on the layout
+ // of the bound parameter blocks.
+ //
+ // 2. Specialized shader kernels, based on "plugging in"
+ // the parameter block types for generic type parameters
+ // as needed.
+ //
+ // 3. A specialized pipeline state object that ties the
+ // above items together with the fixed-function state
+ // already specified in the effect.
+ //
+ // We will now go through these steps in order.
+
+ // (1) The pipline layout (aka D3D12 "root signature") will
+ // be determined based on the descriptor-set layouts
+ // already cached in the given parameter block layouts.
+ //
+ std::vector<IPipelineLayout::DescriptorSetDesc> descriptorSets;
+ for(UInt pp = 0; pp < parameterBlockCount; ++pp)
+ {
+ descriptorSets.emplace_back(
+ parameterBlockLayouts[pp]->descriptorSetLayout);
+ }
+ IPipelineLayout::Desc pipelineLayoutDesc;
+ pipelineLayoutDesc.renderTargetCount = 1;
+ pipelineLayoutDesc.descriptorSetCount = descriptorSets.size();
+ pipelineLayoutDesc.descriptorSets = descriptorSets.data();
+ auto pipelineLayout = renderer->createPipelineLayout(pipelineLayoutDesc);
+
+ // (2) The final shader kernels to bind will be computed
+ // from the kernels we extracted into an application `EntryPoint`
+ // plus the types of the bound paramter blocks, as needed.
+ //
+ // We will "infer" a type argument for each of the generic
+ // parameters of our shader program by looking for a
+ // parameter block that is declared using that generic
+ // type.
+ //
+ std::vector<const char*> genericArgs;
+ for(auto gp : program->genericParams)
+ {
+ int parameterBlockIndex = gp.parameterBlockIndex;
+ auto typeName = parameterBlockLayouts[parameterBlockIndex]->typeName.c_str();
+ genericArgs.push_back(typeName);
+ }
+
+ // Now that we are ready to generate specialized shader code,
+ // we wil invoke the Slang compiler again. This time we leave
+ // full code generation turned on, and we also specify the
+ // entry points that we want explicitly (so that we don't
+ // generate code for any other entry points).
+ //
+ auto slangSession = getSlangSession();
+ SlangCompileRequest* slangRequest = spCreateCompileRequest(slangSession);
+ int targetIndex = spAddCodeGenTarget(slangRequest, SLANG_DXBC);
+ spSetTargetProfile(slangRequest, targetIndex, spFindProfile(slangSession, "sm_4_0"));
+ int translationUnitIndex = spAddTranslationUnit(slangRequest, SLANG_SOURCE_LANGUAGE_SLANG, nullptr);
+ spAddTranslationUnitSourceFile(slangRequest, translationUnitIndex, program->shaderModule->inputPath.c_str());
+
+ // Because our shader code uses global generic parameters for
+ // specialization, we need to specify the concrete argument
+ // types for the compiler to use when generating code.
+ //
+ spSetGlobalGenericArgs(
+ slangRequest,
+ int(genericArgs.size()),
+ genericArgs.data());
+
+ // Next we tell the Slang compiler about all of the entry points
+ // we plan to use.
+ //
+ const int entryPointCount = int(program->entryPoints.size());
+ for(int ii = 0; ii < entryPointCount; ++ii)
+ {
+ auto entryPoint = program->entryPoints[ii];
+ spAddEntryPoint(
+ slangRequest,
+ translationUnitIndex,
+ entryPoint->name.c_str(),
+ entryPoint->slangStage);
+ }
+
+ // We expect compilation to go through without a hitch, because the
+ // code was already statically checked back in `loadShaderModule()`.
+ // It is still possible for errors to arise if, e.g., the application
+ // tries to specialize code based on a type that doesn't implement
+ // a required interface.
+ //
+ int compileErr = spCompile(slangRequest);
+ if(auto diagnostics = spGetDiagnosticOutput(slangRequest))
+ {
+ reportError("%s", diagnostics);
+ }
+ if(compileErr)
+ {
+ spDestroyCompileRequest(slangRequest);
+ assert(!"unexected");
+ return nullptr;
+ }
+
+ // Once compilation is done we can extract the kernel code
+ // for each of the entry points, and set them up for passing
+ // to the graphics APIs loading logic.
+ //
+ std::vector<ISlangBlob*> kernelBlobs;
+ std::vector<gfx::IShaderProgram::KernelDesc> kernelDescs;
+ for(int ii = 0; ii < entryPointCount; ++ii)
+ {
+ auto entryPoint = program->entryPoints[ii];
+
+ ISlangBlob* blob = nullptr;
+ spGetEntryPointCodeBlob(slangRequest, ii, 0, &blob);
+
+ kernelBlobs.push_back(blob);
+
+ IShaderProgram::KernelDesc kernelDesc;
+
+ char const* codeBegin = (char const*) blob->getBufferPointer();
+ char const* codeEnd = codeBegin + blob->getBufferSize();
+
+ kernelDesc.stage = entryPoint->apiStage;
+ kernelDesc.codeBegin = codeBegin;
+ kernelDesc.codeEnd = codeEnd;
+
+ kernelDescs.push_back(kernelDesc);
+ }
+
+ // Once we've extracted the "blobs" of compiled code,
+ // we are done with the Slang compilation request.
+ //
+ // Note that all of our reflection was performed on the unspecialized
+ // shader code at load time, but we know that information is still
+ // applicable to specialized kernels because of the guarantees
+ // the Slang compiler makes about type layout.
+ //
+ spDestroyCompileRequest(slangRequest);
+
+ // We use the graphics API to load a program into the GPU
+ gfx::IShaderProgram::Desc programDesc = {};
+ programDesc.pipelineType = gfx::PipelineType::Graphics;
+ programDesc.kernels = kernelDescs.data();
+ programDesc.kernelCount = kernelDescs.size();
+ auto specializedProgram = renderer->createProgram(programDesc);
+
+ // Then we unload our "blobs" of kernel code once the graphics
+ // API is doen with their data.
+ //
+ for(auto blob : kernelBlobs)
+ {
+ blob->release();
+ }
+
+ // (3) We construct a full graphics API pipeline state
+ // object that combines our new program and pipeline layout
+ // with the other state objects from the `Effect`.
+ //
+ gfx::GraphicsPipelineStateDesc pipelineStateDesc = {};
+ pipelineStateDesc.program = specializedProgram;
+ pipelineStateDesc.pipelineLayout = pipelineLayout;
+ pipelineStateDesc.inputLayout = effect->inputLayout;
+ pipelineStateDesc.framebufferLayout = framebufferLayout;
+ auto pipelineState = renderer->createGraphicsPipelineState(pipelineStateDesc);
+
+ RefPtr<EffectVariant> variant = new EffectVariant();
+ variant->pipelineLayout = pipelineLayout;
+ variant->pipelineState = pipelineState;
+ return variant;
+}
+
+// A more advanced application might add logic to
+// pre-populate the shader cache with shader variants
+// that were compiled offline.
+//
+struct ShaderCache : RefObject
+{
+ struct VariantKey
+ {
+ Effect* effect;
+ UInt parameterBlockCount;
+ ParameterBlockLayout* parameterBlockLayouts[8];
+
+ // In order to be used as a hash-table key, our
+ // variant key representation must support
+ // equality comparison and a matching hashin function.
+
+ bool operator==(VariantKey const& other) const
+ {
+ if(effect != other.effect) return false;
+ if(parameterBlockCount != other.parameterBlockCount) return false;
+ for( UInt ii = 0; ii < parameterBlockCount; ++ii )
+ {
+ if(parameterBlockLayouts[ii] != other.parameterBlockLayouts[ii]) return false;
+ }
+ return true;
+ }
+
+ Slang::HashCode getHashCode() const
+ {
+ auto hash = Slang::getHashCode(effect);
+ hash = Slang::combineHash(hash, Slang::getHashCode(parameterBlockCount));
+ for( UInt ii = 0; ii < parameterBlockCount; ++ii )
+ {
+ hash = Slang::combineHash(hash, Slang::getHashCode(parameterBlockLayouts[ii]));
+ }
+ return hash;
+ }
+ };
+
+ // The shader cache is mostly just a dictionary mapping
+ // variant keys to the associated variant, generated on-demand.
+ //
+ Slang::Dictionary<VariantKey, RefPtr<EffectVariant> > variants;
+
+ // Getting a variant is just a matter of looking for an
+ // existing entry in the dictionary, and creating one
+ // on demand in case of a miss.
+ //
+ RefPtr<EffectVariant> getEffectVariant(
+ VariantKey const& key,
+ IFramebufferLayout* framebufferLayout)
+ {
+ RefPtr<EffectVariant> variant;
+ if(variants.TryGetValue(key, variant))
+ return variant;
+
+ variant = createEffectVaraint(
+ key.effect,
+ key.parameterBlockCount,
+ key.parameterBlockLayouts,
+ framebufferLayout);
+
+ variants.Add(key, variant);
+ return variant;
+ }
+
+ // We support clearign the shader cache, which can serve
+ // as a kind of "hot reload" action, because subsequent
+ // rendering work will need to re-compile shader variants
+ // from scratch.
+ //
+ void clear()
+ {
+ variants.Clear();
+ }
+};
+
+
+// In order to render using the `Effect` abstraction, our
+// application will use its own rendering context type
+// to manage the state that it is binding. This layer
+// performs a small amount of shadowing on top of the
+// underlying graphics API.
+//
+// Note: for the purposes of our examples the "graphcis API"
+// in a cross-platform abstraction over multiple APIs, but
+// we do not actually advocate that real applications should
+// be built in terms of distinct layers for cross-platform
+// GPU API abstraction and "effect" state management.
+//
+// A high-performance application built on top of this approach
+// would instead implement the concepts like `ParameterBlock`
+// and `RenderContext` on a per-API basis, making use of
+// whatever is most efficeint on that API without any
+// additional abstraction layers in between.
+//
+// We've done things differently in this example program in
+// order to avoid getting bogged down in the specifics of
+// any one GPU API.
+//
+// With that disclaimer out of the way, let's talk through
+// the `RenderContext` type in this application.
+//
+struct RenderContext
+{
+private:
+ // The `RenderContext` type is used to wrap the graphics
+ // API "context" or "command list" type for submission.
+ // Our current abstraction layer lumps this all together
+ // with the "device."
+ //
+ Slang::ComPtr<gfx::IRenderer> renderer;
+
+ // We also retain a pointer to the shader cache, which
+ // will be used to implement lookup of the right
+ // effect variant to execute based on bound parameter
+ // blocks.
+ //
+ RefPtr<ShaderCache> shaderCache;
+
+ // We will establish a small upper bound on how many
+ // parameter blocks can be used simultaneously. In
+ // practice, most shaders won't need more than about
+ // four parameter blocks, and attempting to use more
+ // than that under Vulkan can cause portability issues.
+ //
+ enum { kMaxParameterBlocks = 8 };
+
+ // The overall "state" of the rendering context consists of:
+ //
+ // * The currently selected "effect"
+ // * The parameter blocks that are used to specialize and
+ // provide parameters for that effects.
+ //
+ RefPtr<Effect> effect;
+ RefPtr<ParameterBlock> parameterBlocks[kMaxParameterBlocks];
+
+ // Along with the retained state above, we also store
+ // state in exactly the form required for looking up
+ // an effect variant in our shader cache, to minimize
+ // the work that needs to be done when looking up state.
+ //
+ ShaderCache::VariantKey variantKey;
+
+ // When state gets changed, we track a few dirty flags rather than
+ // flush changes to the GPU right away.
+
+ // Tracks whether any state has changed in a way that requires computing
+ // and binding a new GPU pipeline state object (PSO).
+ //
+ // E.g., changing the current effect would set this flag, but changing
+ // a parameter block binding to one with a new layout would also set the flag.
+ bool pipelineStateDirty = true;
+
+ // The `minDirtyBlockBinding` flag tracks the lowest-numbered parameter
+ // block binding that needs to be flushed to the GPU. That is, if
+ // parameters blocks [0,N) have been bound to the GPU, and then the user
+ // tries to set block K, then the range [0,K-1) will be left alone,
+ // while the range [K,N) needs to be set again.
+ //
+ // This is an optimization that can be exploited on the Vulkan API
+ // (and potentially others) if switching pipeline layouts doesn't invalidate
+ // all currently-bound descriptor sets.
+ //
+ int minDirtyBlockBinding = 0;
+
+ // Finally, we cache the specialized effect variant that has been
+ // most recently bound to the GPU state, so that we can use the
+ // information it stores (specifically the pipeline layout) when
+ // binding descriptor sets.
+ //
+ RefPtr<EffectVariant> currentEffectVariant;
+
+public:
+ // Initializing a render context just sets its pointer to the GPU API device
+ RenderContext(
+ gfx::IRenderer* renderer,
+ ShaderCache* shaderCache)
+ : renderer(renderer)
+ , shaderCache(shaderCache)
+ {}
+
+ void setEffect(
+ Effect* inEffect)
+ {
+ // Bail out if nothing is changing.
+ if( inEffect == effect )
+ return;
+
+ effect = inEffect;
+ variantKey.effect = effect;
+ variantKey.parameterBlockCount = effect->program->parameterBlockCount;
+
+ // Binding a new effect invalidates the current state object, since
+ // it will be a specialization of some other effect.
+ //
+ pipelineStateDirty = true;
+ }
+
+ void setParameterBlock(
+ int index,
+ ParameterBlock* parameterBlock)
+ {
+ // Bail out if nothing is changing.
+ if(parameterBlock == parameterBlocks[index])
+ return;
+
+ parameterBlocks[index] = parameterBlock;
+
+ // This parameter block needs to be bound to the GPU, and any
+ // parameter blocks after it in the list will also get re-bound
+ // (even if they haven't changed). This is a reasonable choice
+ // if parameter blocks are ordered based on expected frequency
+ // of update (so that lower-numbered blocks change less often).
+ //
+ minDirtyBlockBinding = std::min(index, minDirtyBlockBinding);
+
+ // Next, check if the layout for the block we just bound
+ // is different than the one that was in place before,
+ // as stored in the "variant key"
+ //
+ auto layout = parameterBlock->layout;
+ if(layout.Ptr() == variantKey.parameterBlockLayouts[index])
+ return;
+
+ variantKey.parameterBlockLayouts[index] = layout;
+
+ // Changing the layout of a parameter block (which includes
+ // the underlying Slang type) requires computing a new
+ // pipeline state object, because it may lead to differently
+ // specialized code being generated.
+ //
+ pipelineStateDirty = true;
+ }
+
+ void flushState(IFramebufferLayout* framebufferLayout)
+ {
+ // The `flushState()` operation must be used by the application
+ // any time it binds a different effect or parameter block(s),
+ // to ensure that the GPU state is fully configured for rendering.
+ // It is thus important that this function do as little work
+ // as possible, especially in the common case where state
+ // doesn't actually need to change.
+ //
+ // The first check we do is to see if any change might require
+ // a different set of shader kernels.
+ //
+ if(pipelineStateDirty)
+ {
+ pipelineStateDirty = false;
+
+ // Almost all of the logic for retrieving or creating
+ // a new pipeline state with specialized kernels is
+ // handled by our shader cache.
+ //
+ // In the common case, the desired variant will already
+ // be present in the cache, and this function returns
+ // without much effort.
+ //
+ auto variant = shaderCache->getEffectVariant(variantKey, framebufferLayout);
+
+ // In order to adapt to a change in shader variant,
+ // we simply bind its PSO into the GPU state, and
+ // remember the variant we've selected.
+ //
+ renderer->setPipelineState(variant->pipelineState);
+ currentEffectVariant = variant;
+ }
+
+ // Even if the current pipeline state was fine, we may need to
+ // bind one or more descriptor sets. We do this by walking
+ // from our lowest-numbered "dirty" set up to the number
+ // of sets expected by the current effect and binding them.
+ //
+ // If `minDirtyBlockBinding` is greater than or equal to the
+ // `parameterBlockCount` of the currently bound effect, then
+ // this will be a no-op.
+ //
+ // The common case in a tight drawing loop will be that only
+ // the last block will be dirty, and we will only execute
+ // one iteration of this loop.
+ //
+ auto program = effect->program;
+ auto parameterBlockCount = program->parameterBlockCount;
+ auto pipelineLayout = currentEffectVariant->pipelineLayout;
+ for(int ii = minDirtyBlockBinding; ii < parameterBlockCount; ++ii)
+ {
+ renderer->setDescriptorSet(
+ PipelineType::Graphics,
+ pipelineLayout,
+ ii,
+ parameterBlocks[ii]->descriptorSet);
+ }
+ minDirtyBlockBinding = parameterBlockCount;
+ }
+};
+
+//
+// The above types represent a core set of abstractions for working
+// with rendering effects and their parameters, while performing
+// static specialization to maintain GPU efficiency.
+//
+// We will now turn our attention to application-side abstractions
+// for lights and materials that will match up with our shader-side
+// interface definitions.
+//
+// For example, our application code has a rudimentary material system,
+// to match the `IMaterial` abstraction used in the shade code.
+//
+struct Material : RefObject
+{
+ // The key feature of a matrial in our application is that
+ // it can provide a parameter block that describes it and
+ // its parameters. The contents of the parameter block will
+ // be any colors, textures, etc. that the material needs,
+ // while the Slang type that was used to allocate the
+ // block will be an implementation of `IMaterial` that
+ // provides the evaluation logic for the material.
+
+ // Each subclass of `Material` will provide a routine to
+ // create a parameter block of its chosen type/layout.
+ virtual RefPtr<ParameterBlock> createParameterBlock() = 0;
+
+ // The parameter block for a material will be stashed here
+ // after it is created.
+ RefPtr<ParameterBlock> parameterBlock;
+};
+
+// For now we have only a single implementation of `Material`,
+// which corresponds to the `SimpleMaterial` type in our shader
+// code.
+//
+struct SimpleMaterial : Material
+{
+ glm::vec3 diffuseColor;
+ glm::vec3 specularColor;
+ float specularity;
+
+ // When asked to create a parameter block, the `SimpleMaterial`
+ // type will allocate a block based on the corresponding
+ // shader type, and fill it in based on the data in the C++
+ // object.
+ //
+ RefPtr<ParameterBlock> createParameterBlock() override
+ {
+ auto parameterBlockLayout = gParameterBlockLayout;
+ auto parameterBlock = allocatePersistentParameterBlock(
+ parameterBlockLayout);
+
+ ParameterBlockEncoder encoder = parameterBlock->beginEncoding();
+ encoder.writeField(0, diffuseColor);
+ encoder.writeField(1, specularColor);
+ encoder.writeField(2, specularity);
+ encoder.finishEncoding();
+
+ return parameterBlock;
+ }
+
+ // We cache the corresponding parameter block layout for
+ // `SimpleMaterial` in a static variable so that we don't
+ // load it more than once.
+ //
+ static RefPtr<ParameterBlockLayout> gParameterBlockLayout;
+};
+RefPtr<ParameterBlockLayout> SimpleMaterial::gParameterBlockLayout;
+
+// With the `Material` abstraction defined, we can go on to define
+// the representation for loaded models that we will use.
+//
+// A `Model` will own vertex/index buffers, along with a list of meshes,
+// while each `Mesh` will own a material and a range of indices.
+// For this example we will be loading models from `.obj` files, but
+// that is just a simple lowest-common-denominator choice.
+//
+struct Mesh : RefObject
+{
+ RefPtr<Material> material;
+ int firstIndex;
+ int indexCount;
+};
+struct Model : RefObject
+{
+ typedef ModelLoader::Vertex Vertex;
+
+ ComPtr<IBufferResource> vertexBuffer;
+ ComPtr<IBufferResource> indexBuffer;
+ PrimitiveTopology primitiveTopology;
+ int vertexCount;
+ int indexCount;
+ std::vector<RefPtr<Mesh>> meshes;
+};
+//
+// Loading a model from disk is done with the help of some utility
+// code for parsing the `.obj` file format, so that the application
+// mostly just registers some callbacks to allocate the objects
+// used for its representation.
+//
+RefPtr<Model> loadModel(
+ IRenderer* renderer,
+ char const* inputPath,
+ ModelLoader::LoadFlags loadFlags = 0,
+ float scale = 1.0f)
+{
+ // The model loading interface using a C++ interface of
+ // callback functions to handle creating the application-specific
+ // representation of meshes, materials, etc.
+ //
+ struct Callbacks : ModelLoader::ICallbacks
+ {
+ void* createMaterial(MaterialData const& data) override
+ {
+ SimpleMaterial* material = new SimpleMaterial();
+ material->diffuseColor = data.diffuseColor;
+ material->specularColor = data.specularColor;
+ material->specularity = data.specularity;
+
+ material->parameterBlock = material->createParameterBlock();
+
+ return material;
+ }
+
+ void* createMesh(MeshData const& data) override
+ {
+ Mesh* mesh = new Mesh();
+ mesh->firstIndex = data.firstIndex;
+ mesh->indexCount = data.indexCount;
+ mesh->material = (Material*)data.material;
+ return mesh;
+ }
+
+ void* createModel(ModelData const& data) override
+ {
+ Model* model = new Model();
+ model->vertexBuffer = data.vertexBuffer;
+ model->indexBuffer = data.indexBuffer;
+ model->primitiveTopology = data.primitiveTopology;
+ model->vertexCount = data.vertexCount;
+ model->indexCount = data.indexCount;
+
+ int meshCount = data.meshCount;
+ for (int ii = 0; ii < meshCount; ++ii)
+ model->meshes.push_back((Mesh*)data.meshes[ii]);
+
+ return model;
+ }
+ };
+ Callbacks callbacks;
+
+ // We instantiate a model loader object and then use it to
+ // try and load a model from the chosen path.
+ //
+ ModelLoader loader;
+ loader.renderer = renderer;
+ loader.loadFlags = loadFlags;
+ loader.scale = scale;
+ loader.callbacks = &callbacks;
+ Model* model = nullptr;
+ if (SLANG_FAILED(loader.load(inputPath, (void**)&model)))
+ {
+ log("failed to load '%s'\n", inputPath);
+ return nullptr;
+ }
+
+ return model;
+}
+
+// Along with materials, our application needs to be able to represent
+// multiple light sources in the scene. For this task we will use a C++
+// inheritance hierarchy rooted at `Light` to match the `ILight`
+// interface in Slang.
+//
+// Unlike how materials are currently being handled, we will use a
+// quick-and-dirty "RTTI" system for lights to allow some of the application
+// code to abstract over particular light types.
+//
+struct Light;
+struct LightType
+{
+ // A light type needs to know both the name of the type (e.g., so that
+ // we can load shader code), and must also provide a factory function
+ // to create lights on demand (e.g., when the user requests that one
+ // be added in a UI).
+ //
+ char const* name;
+ Light* (*createLight)();
+};
+//
+// The following is some crud to bootstrap the rudimentary RTTI system
+// for lights. Each concrete subclass of `Light` needs to use the
+// `DEFINE_LIGHT_TYPE` macro to set up its RTTI info.
+//
+template<typename T>
+struct LightTypeImpl
+{
+ static LightType type;
+ static Light* create() { return (Light*)(new T); }
+};
+#define DEFINE_LIGHT_TYPE(NAME) \
+ LightType LightTypeImpl<NAME>::type = { #NAME, &LightTypeImpl<NAME>::create };
+template<typename T>
+LightType* getLightType()
+{
+ return &LightTypeImpl<T>::type;
+}
+
+struct Light : RefObject
+{
+ // A light must be able to return its type information.
+ virtual LightType* getType() = 0;
+
+ // A light must be able to write a representation of itself into
+ // a parameter block, or a part of one.
+ virtual void fillInParameterBlock(ParameterBlockEncoder& encoder) = 0;
+
+ // For this application, a light must be able to present a user
+ // interface for people to modify its properties.
+ virtual void doUI() = 0;
+};
+
+// We will provide two nearly trivial implementations of `Light` for now,
+// to show the kind of application code needed to line up with the corresponding
+// types defined in the Slang shader code for this application.
+
+struct DirectionalLight : Light
+{
+ glm::vec3 direction = normalize(glm::vec3(1));
+ glm::vec3 color = glm::vec3(1);
+ float intensity = 1;
+
+ LightType* getType() override { return getLightType<DirectionalLight>(); };
+
+ void fillInParameterBlock(ParameterBlockEncoder& encoder) override
+ {
+ encoder.writeField(0, direction);
+ encoder.writeField(1, color*intensity);
+ }
+
+ void doUI() override
+ {
+ if (ImGui::SliderFloat3("direction", &direction[0], -1, 1))
+ {
+ direction = normalize(direction);
+ }
+ ImGui::ColorEdit3("color", &color[0]);
+ ImGui::DragFloat("intensity", &intensity, 1.0f, 0.0f, 10000.0f, "%.3f", 2.0f);
+ }
+};
+DEFINE_LIGHT_TYPE(DirectionalLight);
+
+struct PointLight : Light
+{
+ glm::vec3 position = glm::vec3(0);
+ glm::vec3 color = glm::vec3(1);
+ float intensity = 10;
+
+ LightType* getType() override { return getLightType<PointLight>(); };
+
+ void fillInParameterBlock(ParameterBlockEncoder& encoder) override
+ {
+ encoder.writeField(0, position);
+ encoder.writeField(1, color*intensity);
+ }
+
+ void doUI() override
+ {
+ ImGui::DragFloat3("position", &position[0], 0.1f);
+ ImGui::ColorEdit3("color", &color[0]);
+ ImGui::DragFloat("intensity", &intensity, 1.0f, 0.0f, 10000.0f, "%.3f", 2.0f);
+ }
+};
+DEFINE_LIGHT_TYPE(PointLight);
+
+// Rendering is usually done with collections of lights rather than single
+// lights. This application will use a concept of "light environments" to
+// group together lights for rendering.
+//
+// We want to be *able* to specialize our shader code based on the particular
+// types of lights in a scene, but we also do not want to over-specialize
+// and, e.g., use differnt specialized shaders for a scene with 99 point
+// lights vs. 100.
+//
+// This particular application will use a notion of a "layout" for a lighting
+// environment, which specifies the allowed types of lights, and the maximum
+// number of lights of each type. Different lighting environment layouts
+// will yield different specialized code.
+
+struct LightEnvLayout : public RefObject
+{
+ // Our lighting environment layout will track layout
+ // information for several different arrays: one
+ // for each supported light type.
+ //
+ struct LightArrayLayout : RefObject
+ {
+ LightType* type;
+ RefPtr<ParameterBlockLayout> lightLayout;
+ RefPtr<ParameterBlockLayout> arrayLayout;
+ Int maximumCount = 0;
+ };
+ RefPtr<ShaderModule> module;
+ std::vector<RefPtr<LightArrayLayout>> lightArrayLayouts;
+ std::map<LightType*, Int> mapLightTypeToArrayIndex;
+
+ LightEnvLayout(ShaderModule* module)
+ : module(module)
+ {}
+
+ void addLightType(LightType* type, Int maximumCount)
+ {
+ Int arrayIndex = (Int)lightArrayLayouts.size();
+ RefPtr<LightArrayLayout> layout = new LightArrayLayout();
+ layout->type = type;
+ layout->lightLayout = ::getParameterBlockLayout(module, type->name);
+ layout->maximumCount = maximumCount;
+
+ // When the user adds a light type `X` to a light-env layout,
+ // we need to compute the corresponding Slang type and
+ // layout information to use. If only a single light is
+ // supported, this will just be the type `X`, while for
+ // any other count this will be a `LightArray<X, maximumCount>`
+ //
+ if (maximumCount <= 1)
+ {
+ layout->arrayLayout = layout->lightLayout;
+ }
+ else
+ {
+ layout->arrayLayout = getSpecializedParameterBlockLayout(
+ module, "LightArray", layout->lightLayout, maximumCount);
+ }
+
+ lightArrayLayouts.push_back(layout);
+ mapLightTypeToArrayIndex.insert(std::make_pair(type, arrayIndex));
+ }
+ template<typename T>
+ void addLightType(Int maximumCount)
+ {
+ addLightType(getLightType<T>(), maximumCount);
+ }
+
+ Int getArrayIndexForType(LightType* type)
+ {
+ auto iter = mapLightTypeToArrayIndex.find(type);
+ if (iter != mapLightTypeToArrayIndex.end())
+ return iter->second;
+
+ return -1;
+ }
+
+ // We will compute a parameter block layout for the
+ // whole lighting environment on demand, and then
+ // cache it thereafter.
+ //
+ RefPtr<ParameterBlockLayout> parameterBlockLayout;
+ RefPtr<ParameterBlockLayout> getParameterBlockLayout()
+ {
+ if (!parameterBlockLayout)
+ {
+ parameterBlockLayout = computeParameterBlockLayout();
+ }
+ return parameterBlockLayout;
+ }
+
+ RefPtr<ParameterBlockLayout> computeParameterBlockLayout()
+ {
+ // Given a lighting environment with N light types:
+ //
+ // L0, L1, ... LN
+ //
+ // We want to compute the Slang type:
+ //
+ // LightPair<L0, LightPair<L1, ... LightPair<LN-1, LN>>>
+ //
+ // This is most easily accomplished by doing a "fold" while
+ // walking the array in reverse order.
+
+ RefPtr<ParameterBlockLayout> envLayout;
+
+ auto arrayCount = lightArrayLayouts.size();
+ for (size_t ii = arrayCount; ii--;)
+ {
+ auto arrayInfo = lightArrayLayouts[ii];
+ RefPtr<ParameterBlockLayout> arrayLayout = arrayInfo->arrayLayout;
+
+ if (!envLayout)
+ {
+ // The is the right-most entry, so it is the base case for our "fold"
+ envLayout = arrayLayout;
+ }
+ else
+ {
+ // Fold one entry: `envLayout = LightPair<a, envLayout>`
+ envLayout = getSpecializedParameterBlockLayout(
+ module, "LightPair", arrayLayout, envLayout);
+ }
+ }
+
+ if (!envLayout)
+ {
+ // Handle the special case of *zero* light types.
+ envLayout = ::getParameterBlockLayout(module, "EmptyLightEnv");
+ }
+
+ return envLayout;
+ }
+};
+
+// A `LightEnv` follows the structure of a `LightEnvLayout`,
+// and provides storage for zero or more lights of various
+// different types (up to the limits imposed by the layout).
+//
+struct LightEnv : public RefObject
+{
+ // A light environment is always created from a fixed layout
+ // in this application, so the constructor allocates an array
+ // for the per-light-type data.
+ //
+ // A more complex example might dynamically determine the
+ // layout based on the number of lights of each type active
+ // in the scene, with some quantization applied to avoid
+ // generating too many shader specializations.
+ //
+ // Note: the kind of specialization going on here would also
+ // be applicable to a deferred or "forward+" renderer, insofar
+ // as it sets the bounds on the total set of lights for
+ // a scene/frame, while per-tile/-cluster light lists would
+ // probably just be indices into the global structure.
+ //
+ RefPtr<LightEnvLayout> layout;
+ LightEnv(RefPtr<LightEnvLayout> layout)
+ : layout(layout)
+ {
+ for (auto arrayLayout : layout->lightArrayLayouts)
+ {
+ RefPtr<LightArray> lightArray = new LightArray();
+ lightArray->layout = arrayLayout;
+ lightArrays.push_back(lightArray);
+ }
+ }
+
+ // For each light type, we track the layout information,
+ // plus the list of active lights of that type.
+ //
+ struct LightArray : RefObject
+ {
+ RefPtr<LightEnvLayout::LightArrayLayout> layout;
+ std::vector<RefPtr<Light>> lights;
+ };
+ std::vector<RefPtr<LightArray>> lightArrays;
+
+ RefPtr<LightArray> getArrayForType(LightType* type)
+ {
+ auto index = layout->getArrayIndexForType(type);
+ return lightArrays[index];
+ }
+
+ void add(RefPtr<Light> light)
+ {
+ auto array = getArrayForType(light->getType());
+ array->lights.push_back(light);
+ }
+
+ virtual void doUI()
+ {
+ if (ImGui::Button("Add"))
+ {
+ ImGui::OpenPopup("AddLight");
+ }
+ if (ImGui::BeginPopup("AddLight"))
+ {
+ for (auto array : lightArrays)
+ {
+ if (ImGui::MenuItem(
+ array->layout->type->name,
+ nullptr,
+ nullptr,
+ array->lights.size() < (size_t)array->layout->maximumCount))
+ {
+ auto light = array->layout->type->createLight();
+ array->lights.push_back(light);
+ }
+ }
+ ImGui::EndPopup();
+ }
+
+ for (auto array : lightArrays)
+ {
+ auto lightCount = array->lights.size();
+ auto maxLightCount = array->layout->maximumCount;
+ if (ImGui::TreeNode(
+ array.Ptr(),
+ "%s (%d/%d)",
+ array->layout->type->name,
+ (int)lightCount,
+ (int)maxLightCount))
+ {
+ size_t lightCounter = 0;
+ for (auto light : array->lights)
+ {
+ size_t lightIndex = lightCounter++;
+ if (ImGui::TreeNode(light.Ptr(), "%d", (int)lightIndex))
+ {
+ light->doUI();
+ ImGui::TreePop();
+ }
+ }
+ ImGui::TreePop();
+ }
+ }
+ }
+
+ // Because the lighting environment will often change between frames,
+ // we will not try to optimize for the case where it doesn't change,
+ // and will instead fill in a "transient" parameter block from
+ // scratch every frame.
+ //
+ RefPtr<ParameterBlock> createParameterBlock()
+ {
+ auto parameterBlockLayout = layout->getParameterBlockLayout();
+ auto parameterBlock = allocateTransientParameterBlock(parameterBlockLayout);
+
+ ParameterBlockEncoder encoder = parameterBlock->beginEncoding();
+ fillInParameterBlock(encoder);
+ encoder.finishEncoding();
+
+ return parameterBlock;
+ }
+ void fillInParameterBlock(ParameterBlockEncoder& inEncoder)
+ {
+ // When filling in the parameter block for a lighting
+ // environment, we mostly follow the structure of
+ // the type that was computed by the `LightEnvLayout`:
+ //
+ // LightPair<A, LightPair<B, ... LightPair<Y, Z>>>
+ //
+ // we will keep `encoder` pointed at the "spine" of this
+ // structure (so at an element that represents a `LightPair`,
+ // except for the special case of the last item like `Z` above).
+ //
+ // For each light type, we will then encode the data as
+ // needed for the light type (`A` then `B` then ...)
+ //
+ auto encoder = inEncoder;
+ size_t lightTypeCount = lightArrays.size();
+ for (size_t tt = 0; tt < lightTypeCount; ++tt)
+ {
+ // The encoder for the very last item will
+ // just be the one on the "spine" of the list.
+ auto lightTypeEncoder = encoder;
+ if (tt != lightTypeCount - 1)
+ {
+ // In the common case `encoder` is set up
+ // for writing to a `LightPair<X, Y>` so
+ // we ant to set up the `lightTypeEncoder`
+ // for writing to an `X` (which is the first
+ // field of `LightPair`, and then have
+ // `encoder` move on to the `Y` (the rest
+ // of the list of light types).
+ //
+ lightTypeEncoder = encoder.beginField(0);
+ encoder = encoder.beginField(1);
+ }
+
+ auto& lightTypeArray = lightArrays[tt];
+ size_t lightCount = lightTypeArray->lights.size();
+ size_t maxLightCount = lightTypeArray->layout->maximumCount;
+
+ // Recall that we are representing the data for a single
+ // light type `L` as either an instance of type `L` (if
+ // only a single light is supported), or as an instance
+ // of the type `LightArray<L,N>`.
+ //
+ if (maxLightCount == 1)
+ {
+ // This is the case where the maximu number of lights of
+ // the given type was set as one, so we just have a value
+ // of type `L`, and can tell the first light in our application-side
+ // array to encode itself into that location.
+
+ if (lightCount > 0)
+ {
+ lightTypeArray->lights[0]->fillInParameterBlock(lightTypeEncoder);
+ }
+ else
+ {
+ // We really ought to zero out the entry in this case
+ // (under the assumption that all zeros will represent
+ // an inactive light).
+ }
+ }
+ else
+ {
+ // The more interesting case is when we have a `LightArray<L,N>`,
+ // in which case we need to encode the first field (the light count)...
+ //
+ lightTypeEncoder.writeField<int32_t>(0, int32_t(lightTypeArray->lights.size()));
+ //
+ // ... followed by an array of values of type `L` in the second field.
+ // We will only write to the first `lightCount` entries, which may be
+ // less than `N`. We will rely on dynamic looping in the shader to
+ // not access the entries past that point.
+ //
+ ParameterBlockEncoder arrayEncoder = lightTypeEncoder.beginField(1);
+ for (size_t ii = 0; ii < lightCount; ++ii)
+ {
+ lightTypeArray->lights[ii]->fillInParameterBlock(arrayEncoder.beginArrayElement(ii));
+ }
+ }
+ }
+ }
+};
+
+// Now that we've written all the required infrastructure code for
+// the application's renderer and shader library, we can move on
+// to the main logic.
+//
+// We will again structure our example application as a C++ `struct`,
+// so that we can scope its allocations for easy cleanup, rather than
+// use global variables.
+//
+struct ModelViewer {
+
+Window* gWindow;
+Slang::ComPtr<gfx::IRenderer> gRenderer;
+ComPtr<gfx::ISwapchain> gSwapchain;
+ComPtr<IFramebufferLayout> gFramebufferLayout;
+Slang::List<ComPtr<gfx::IFramebuffer>> gFramebuffers;
+
+// We keep a pointer to the one effect we are using (for a forward
+// rendering pass), plus the parameter-block layouts for our `PerView`
+// and `PerModel` shader types.
+//
+RefPtr<Effect> gEffect;
+RefPtr<ParameterBlockLayout> gPerViewParameterBlockLayout;
+RefPtr<ParameterBlockLayout> gPerModelParameterBlockLayout;
+
+RefPtr<ShaderCache> shaderCache;
+RefPtr<GUI> gui;
+
+// Most of the application state is stored in the list of loaded models,
+// as well as the active light source (a single light for now).
+//
+std::vector<RefPtr<Model>> gModels;
+RefPtr<LightEnv> lightEnv;
+
+
+// During startup the application will load one or more models and
+// add them to the `gModels` list.
+//
+void loadAndAddModel(
+ char const* inputPath,
+ ModelLoader::LoadFlags loadFlags = 0,
+ float scale = 1.0f)
+{
+ auto model = loadModel(gRenderer, inputPath, loadFlags, scale);
+ if(!model) return;
+ gModels.push_back(model);
+}
+
+int gWindowWidth = 1024;
+int gWindowHeight = 768;
+const uint32_t kSwapchainImageCount = 2;
+
+// Our "simulation" state consists of just a few values.
+//
+uint64_t lastTime = 0;
+
+//glm::vec3 lightDir = normalize(glm::vec3(10, 10, 10));
+//glm::vec3 lightColor = glm::vec3(1, 1, 1);
+
+glm::vec3 cameraPosition = glm::vec3(1.75, 1.25, 5);
+glm::quat cameraOrientation = glm::quat(1, glm::vec3(0));
+
+float translationScale = 0.5f;
+float rotationScale = 0.025f;
+
+
+// In order to control camera movement, we will
+// use good old WASD
+bool wPressed = false;
+bool aPressed = false;
+bool sPressed = false;
+bool dPressed = false;
+
+bool isMouseDown = false;
+float lastMouseX;
+float lastMouseY;
+
+void handleEvent(Event const& event)
+{
+ switch( event.code )
+ {
+ case EventCode::KeyDown:
+ case EventCode::KeyUp:
+ {
+ bool isDown = event.code == EventCode::KeyDown;
+ switch(event.u.key)
+ {
+ default:
+ break;
+
+ case KeyCode::W: wPressed = isDown; break;
+ case KeyCode::A: aPressed = isDown; break;
+ case KeyCode::S: sPressed = isDown; break;
+ case KeyCode::D: dPressed = isDown; break;
+ }
+ }
+ break;
+
+ case EventCode::MouseDown:
+ {
+ isMouseDown = true;
+ lastMouseX = event.u.mouse.x;
+ lastMouseY = event.u.mouse.y;
+ }
+ break;
+
+ case EventCode::MouseMoved:
+ {
+ if( isMouseDown )
+ {
+ float deltaX = event.u.mouse.x - lastMouseX;
+ float deltaY = event.u.mouse.y - lastMouseY;
+
+ cameraOrientation = glm::rotate(cameraOrientation, -deltaX * rotationScale, glm::vec3(0,1,0));
+ cameraOrientation = glm::rotate(cameraOrientation, -deltaY * rotationScale, glm::vec3(1,0,0));
+
+ cameraOrientation = normalize(cameraOrientation);
+
+ lastMouseX = event.u.mouse.x;
+ lastMouseY = event.u.mouse.y;
+ }
+ }
+ break;
+
+ case EventCode::MouseUp:
+ isMouseDown = false;
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void _handleEvent(Event const& event)
+{
+ ModelViewer* app = (ModelViewer*) getUserData(event.window);
+ app->handleEvent(event);
+}
+
+// The overall initialization logic is quite similar to
+// the earlier example. The biggest difference is that we
+// create instances of our application-specific parameter
+// block layout and effect types instead of just creating
+// raw graphics API objects.
+//
+Result initialize()
+{
+ WindowDesc windowDesc;
+ windowDesc.title = "Model Viewer";
+ windowDesc.width = gWindowWidth;
+ windowDesc.height = gWindowHeight;
+ windowDesc.eventHandler = &_handleEvent;
+ windowDesc.userData = this;
+ gWindow = createWindow(windowDesc);
+
+ IRenderer::Desc rendererDesc = {};
+ rendererDesc.rendererType = gfx::RendererType::DirectX11;
+ gfxCreateRenderer(&rendererDesc, gRenderer.writeRef());
+
+ InputElementDesc inputElements[] = {
+ {"POSITION", 0, Format::RGB_Float32, offsetof(Model::Vertex, position) },
+ {"NORMAL", 0, Format::RGB_Float32, offsetof(Model::Vertex, normal) },
+ {"UV", 0, Format::RG_Float32, offsetof(Model::Vertex, uv) },
+ };
+ auto inputLayout = gRenderer->createInputLayout(
+ &inputElements[0],
+ 3);
+ if(!inputLayout) return SLANG_FAIL;
+
+ // Create swapchain and framebuffers.
+ gfx::ISwapchain::Desc swapchainDesc = {};
+ swapchainDesc.format = gfx::Format::RGBA_Unorm_UInt8;
+ swapchainDesc.width = gWindowWidth;
+ swapchainDesc.height = gWindowHeight;
+ swapchainDesc.imageCount = kSwapchainImageCount;
+ gSwapchain = gRenderer->createSwapchain(
+ swapchainDesc, gfx::WindowHandle::FromHwnd(getPlatformWindowHandle(gWindow)));
+
+ IFramebufferLayout::AttachmentLayout renderTargetLayout = {gSwapchain->getDesc().format, 1};
+ IFramebufferLayout::AttachmentLayout depthLayout = {gfx::Format::D_Float32, 1};
+ IFramebufferLayout::Desc framebufferLayoutDesc;
+ framebufferLayoutDesc.renderTargetCount = 1;
+ framebufferLayoutDesc.renderTargets = &renderTargetLayout;
+ framebufferLayoutDesc.depthStencil = &depthLayout;
+ SLANG_RETURN_ON_FAIL(
+ gRenderer->createFramebufferLayout(framebufferLayoutDesc, gFramebufferLayout.writeRef()));
+
+ for (uint32_t i = 0; i < kSwapchainImageCount; i++)
+ {
+ gfx::ITextureResource::Desc depthBufferDesc;
+ depthBufferDesc.setDefaults(gfx::IResource::Usage::DepthWrite);
+ depthBufferDesc.init2D(
+ gfx::IResource::Type::Texture2D,
+ gfx::Format::D_Float32,
+ gSwapchain->getDesc().width,
+ gSwapchain->getDesc().height,
+ 0);
+
+ ComPtr<gfx::ITextureResource> depthBufferResource = gRenderer->createTextureResource(
+ gfx::IResource::Usage::DepthWrite, depthBufferDesc, nullptr);
+ ComPtr<gfx::ITextureResource> colorBuffer;
+ gSwapchain->getImage(i, colorBuffer.writeRef());
+
+ gfx::IResourceView::Desc colorBufferViewDesc;
+ memset(&colorBufferViewDesc, 0, sizeof(colorBufferViewDesc));
+ colorBufferViewDesc.format = gSwapchain->getDesc().format;
+ colorBufferViewDesc.renderTarget.shape = gfx::IResource::Type::Texture2D;
+ colorBufferViewDesc.type = gfx::IResourceView::Type::RenderTarget;
+ ComPtr<gfx::IResourceView> rtv =
+ gRenderer->createTextureView(colorBuffer.get(), colorBufferViewDesc);
+
+ gfx::IResourceView::Desc depthBufferViewDesc;
+ memset(&depthBufferViewDesc, 0, sizeof(depthBufferViewDesc));
+ depthBufferViewDesc.format = gfx::Format::D_Float32;
+ depthBufferViewDesc.renderTarget.shape = gfx::IResource::Type::Texture2D;
+ depthBufferViewDesc.type = gfx::IResourceView::Type::DepthStencil;
+ ComPtr<gfx::IResourceView> dsv =
+ gRenderer->createTextureView(depthBufferResource.get(), depthBufferViewDesc);
+
+ gfx::IFramebuffer::Desc framebufferDesc;
+ framebufferDesc.renderTargetCount = 1;
+ framebufferDesc.depthStencilView = dsv.get();
+ framebufferDesc.renderTargetViews = rtv.readRef();
+ framebufferDesc.layout = gFramebufferLayout;
+ ComPtr<gfx::IFramebuffer> frameBuffer = gRenderer->createFramebuffer(framebufferDesc);
+ gFramebuffers.add(frameBuffer);
+ }
+
+ // Unlike the earlier example, we will not generate final shader kernel
+ // code during initialization. Instead, we simply load the shader module
+ // so that we can perform reflection and allocate resources.
+ //
+ auto shaderModule = loadShaderModule(gRenderer, "shaders.slang");
+ if(!shaderModule) return SLANG_FAIL;
+
+ // Once the shader code has been loaded, we can look up types declared
+ // in the shader code by name and perform reflection on them to determine
+ // parameter block layouts, etc.
+ //
+ // A more advanced application might load this information on-demand
+ // and potentially tie into an application-level reflection system
+ // that already knows the string names of its types (e.g., to connect
+ // the `PerView` type in shader code to the `PerView` type declared
+ // in the application code).
+ //
+ gPerViewParameterBlockLayout = getParameterBlockLayout(
+ shaderModule, "PerView");
+ gPerModelParameterBlockLayout = getParameterBlockLayout(
+ shaderModule, "PerModel");
+ //
+ // Note how we are able to load the type definition for `SimpleMaterial`
+ // from the Slang shader module even though the `SimpleMaterial` type
+ // is not actually *used* by any entry point in the file.
+ //
+ SimpleMaterial::gParameterBlockLayout = getParameterBlockLayout(
+ shaderModule, "SimpleMaterial");
+
+ // We also load a shader program based on vertex/fragment shaders in our
+ // module, and then use this to create an application-level effect.
+ //
+ // Note that the `loadProgram` operation here does *not* invoke any
+ // Slang compilation, because the shader module was already completely
+ // parsed, checked, etc. by the logic in `loadShaderModule()` above.
+ //
+ auto program = loadProgram(shaderModule, "vertexMain", "fragmentMain");
+ if(!program) return SLANG_FAIL;
+
+ RefPtr<Effect> effect = new Effect();
+ effect->program = program;
+ effect->inputLayout = inputLayout;
+ effect->renderTargetCount = 1;
+ gEffect = effect;
+
+ // In order to create specialized variants of the effect(s) that
+ // get used for rendering, we will use a shader cache.
+ //
+ shaderCache = new ShaderCache();
+
+ // We will create a lighting environment layout that can hold a few point
+ // and directional lights, and then initialize a lighting environment
+ // with just a single point light.
+ //
+ RefPtr<LightEnvLayout> lightEnvLayout = new LightEnvLayout(shaderModule);
+ lightEnvLayout->addLightType<PointLight>(10);
+ lightEnvLayout->addLightType<DirectionalLight>(2);
+
+ lightEnv = new LightEnv(lightEnvLayout);
+ lightEnv->add(new PointLight());
+
+ // Once we have created all our graphcis API and application resources,
+ // we can start to load models. For now we are keeping things extremely
+ // simple by using a trivial `.obj` file that can be checked into source
+ // control.
+ //
+ // Support for loading more interesting/complex models will be added
+ // to this example over time (although model loading is *not* the focus).
+ //
+ loadAndAddModel("cube.obj");
+
+ // We will do some GUI rendering in this app, using "Dear, IMGUI",
+ // so we need to do the appropriate initialization work here.
+ gui = new GUI(gWindow, gRenderer, gFramebufferLayout);
+
+ showWindow(gWindow);
+
+ return SLANG_OK;
+}
+
+// With the setup work done, we can look at the per-frame rendering
+// logic to see how the application will drive the `RenderContext`
+// type to perform both shader parameter binding and code specialization.
+//
+void renderFrame()
+{
+ gui->beginFrame();
+
+ // In order to see that things are rendering properly we need some
+ // kind of animation, so we will compute a crude delta-time value here.
+ //
+ if(!lastTime) lastTime = getCurrentTime();
+ uint64_t currentTime = getCurrentTime();
+ float deltaTime = float(double(currentTime - lastTime) / double(getTimerFrequency()));
+ lastTime = currentTime;
+
+ // We will use the GLM library to do the matrix math required
+ // to set up our various transformation matrices.
+ //
+ glm::mat4x4 identity = glm::mat4x4(1.0f);
+ glm::mat4x4 projection = glm::perspective(
+ glm::radians(60.0f),
+ float(gWindowWidth) / float(gWindowHeight),
+ 0.1f,
+ 1000.0f);
+
+ // We are implementing a *very* basic 6DOF first-person
+ // camera movement model.
+ //
+ glm::mat3x3 cameraOrientationMat(cameraOrientation);
+ glm::vec3 forward = -cameraOrientationMat[2];
+ glm::vec3 right = cameraOrientationMat[0];
+
+ glm::vec3 movement = glm::vec3(0);
+ if(wPressed) movement += forward;
+ if(sPressed) movement -= forward;
+ if(aPressed) movement -= right;
+ if(dPressed) movement += right;
+
+ cameraPosition += deltaTime * translationScale * movement;
+
+ glm::mat4x4 view = identity;
+ view *= glm::mat4x4(inverse(cameraOrientation));
+ view = glm::translate(view, -cameraPosition);
+
+ glm::mat4x4 viewProjection = projection * view;
+
+ // Some of the basic rendering setup is identical to the previous example.
+ //
+ auto frameIndex = gSwapchain->acquireNextImage();
+ gRenderer->setFramebuffer(gFramebuffers[frameIndex]);
+
+ gfx::Viewport viewport = {};
+ viewport.maxZ = 1.0f;
+ viewport.extentX = (float)gWindowWidth;
+ viewport.extentY = (float)gWindowHeight;
+ gRenderer->setViewportAndScissor(viewport);
+
+ static const float kClearColor[] = { 0.25, 0.25, 0.25, 1.0 };
+ gRenderer->setClearColor(kClearColor);
+ gRenderer->clearFrame();
+ gRenderer->setPrimitiveTopology(PrimitiveTopology::TriangleList);
+
+ // Now we will start in on the more interesting rendering logic,
+ // by creating the `RenderContext` we will use for submission.
+ //
+ // Note: in a multi-threaded submission case, the application would
+ // need to use a distinct `RenderContext` on each thread.
+ //
+ RenderContext context(gRenderer, shaderCache);
+
+ // Next we set the effect that we will use for our forward rendering
+ // pass. Note that an example with multiple passes would use a
+ // distinct effect for each pass.
+ //
+ context.setEffect(gEffect);
+
+ // We are only rendering one view, so we can fill in a per-view
+ // parameter block once and use it across all draw calls.
+ // This parameter block will be different every frame, so we
+ // allocate a transient parameter block rather than try to
+ // carefully track and re-use an allocation.
+ //
+ auto viewParameterBlock = allocateTransientParameterBlock(
+ gPerViewParameterBlockLayout);
+ {
+ auto encoder = viewParameterBlock->beginEncoding();
+ encoder.writeField(0, viewProjection);
+ encoder.writeField(1, cameraPosition);
+ encoder.finishEncoding();
+ }
+ //
+ // Note: the assignment of indices to parameter blocks is driven
+ // by their order of declaration in the shader code, so we know
+ // that the per-view parameter block has index zero. Alternatively,
+ // an application could use reflection API operations to look up
+ // the index of a parameter block based on its name.
+ //
+ context.setParameterBlock(0, viewParameterBlock);
+
+ // Our `LightEnv` type knows how to turn itself into a parameter
+ // block, so we just create and bind it here.
+ //
+ auto lightEnvParameterBlock = lightEnv->createParameterBlock();
+ context.setParameterBlock(2, lightEnvParameterBlock);
+
+ // The majority of our rendering logic is handled as a loop
+ // over the models in the scene, and their meshes.
+ //
+ for(auto& model : gModels)
+ {
+ gRenderer->setVertexBuffer(0, model->vertexBuffer, sizeof(Model::Vertex));
+ gRenderer->setIndexBuffer(model->indexBuffer, Format::R_UInt32);
+
+ // For each model we provide a parameter
+ // block that holds the per-model transformation
+ // parameters, corresponding to the `PerModel` type
+ // in the shader code.
+ //
+ // Like the view parameter block, it makes sense
+ // to allocate this block as a transient allocation,
+ // since its contents would be different on the next
+ // frame anyway.
+ //
+ glm::mat4x4 modelTransform = identity;
+ glm::mat4x4 inverseTransposeModelTransform = inverse(transpose(modelTransform));
+
+ auto modelParameterBlock = allocateTransientParameterBlock(
+ gPerModelParameterBlockLayout);
+ {
+ auto encoder = modelParameterBlock->beginEncoding();
+ encoder.writeField(0, modelTransform);
+ encoder.writeField(1, inverseTransposeModelTransform);
+ encoder.finishEncoding();
+ }
+ context.setParameterBlock(1, modelParameterBlock);
+
+ // Now we loop over the meshes in the model.
+ //
+ // A more advanced rendering loop would sort things by material
+ // rather than by model, to avoid overly frequent state changes.
+ // We are just doing something simple for the purposes of an
+ // exmple program.
+ //
+ for(auto& mesh : model->meshes)
+ {
+ // Each mesh has a material, and each material has its own
+ // parameter block that was created at load time, so we
+ // can just re-use the persistent parameter block for the
+ // chosen material.
+ //
+ // Note that binding the material parameter block here is
+ // both selecting the values to use for various material
+ // parameters as well as the *code* to use for material
+ // evaluation (based on the concrete shader type that
+ // is implementing the `IMaterial` interface).
+ //
+ context.setParameterBlock(
+ 3,
+ mesh->material->parameterBlock);
+
+ // Once we've set up all the parameter blocks needed
+ // for a given drawing operation, we need to flush
+ // any pending state changes (e.g., if the type of
+ // material changed, a shader switch might be
+ // required).
+ //
+ context.flushState(gFramebufferLayout);
+
+ gRenderer->drawIndexed(mesh->indexCount, mesh->firstIndex);
+ }
+ }
+
+ ImGui::Begin("Slang Model Viewer Example");
+ ImGui::Text("Average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
+ if (ImGui::Button("Reload Shaders"))
+ {
+ shaderCache->clear();
+ }
+ if( ImGui::CollapsingHeader("Lights") )
+ {
+ lightEnv->doUI();
+ }
+ if (ImGui::CollapsingHeader("Camera"))
+ {
+ ImGui::InputFloat3("position", &cameraPosition[0]);
+ ImGui::InputFloat3("orientation[0]", &cameraOrientationMat[0][0]);
+ ImGui::InputFloat3("orientation[1]", &cameraOrientationMat[1][0]);
+ ImGui::InputFloat3("orientation[2]", &cameraOrientationMat[2][0]);
+ }
+
+ ImGui::End();
+
+ gSwapchain->present();
+
+}
+
+void finalize()
+{
+ // Because we've stored a reference to some graphics API objects
+ // in a class-static variable (effectively a global) we need
+ // to clear those out before tearing down the application so
+ // that we aren't relying on C++ global destructors to tear
+ // down our application cleanly.
+ //
+ gRenderer->waitForGpu();
+ SimpleMaterial::gParameterBlockLayout = nullptr;
+ destroyWindow(gWindow);
+}
+
+};
+
+void innerMain(ApplicationContext* context)
+{
+ ModelViewer app;
+ if(SLANG_FAILED(app.initialize()))
+ {
+ exitApplication(context, 1);
+ }
+
+ while(dispatchEvents(context))
+ {
+ app.renderFrame();
+ }
+
+ app.finalize();
+}
+GFX_UI_MAIN(innerMain)
+
+#endif
diff --git a/examples/experimental/model-viewer/shaders.slang b/examples/experimental/model-viewer/shaders.slang
new file mode 100644
index 000000000..15ce0120d
--- /dev/null
+++ b/examples/experimental/model-viewer/shaders.slang
@@ -0,0 +1,485 @@
+// shaders.slang
+
+//
+// This example builds on the simplistic shaders presented in the
+// "Hello, World" example by adding support for (intentionally
+// simplistic) surface materil and light shading.
+//
+// The code here is not meant to exemplify state-of-the-art material
+// and lighting techniques, but rather to show how a shader
+// library can be developed in a modular fashion without reliance
+// on the C preprocessor manual parameter-binding decorations.
+//
+
+// We are going to define a simple model for surface material shading.
+//
+// The first building block in our model will be the representation of
+// the geometry attributes of a surface as fed into the material.
+//
+struct SurfaceGeometry
+{
+ float3 position;
+ float3 normal;
+
+ // TODO: tangent vectors would be the natural next thing to add here,
+ // and would be required for anisotropic materials. However, the
+ // simplistic model loading code we are currently using doesn't
+ // produce tangents...
+ //
+ // float3 tangentU;
+ // float3 tangentV;
+
+ // We store a single UV parameterization in these geometry attributes.
+ // A more complex renderer might need support for multiple UV sets,
+ // and indeed it might choose to use interfaces and generics to capture
+ // the different requirements that different materials impose on
+ // the available surface attributes. We won't go to that kind of
+ // trouble for such a simple example.
+ //
+ float2 uv;
+};
+//
+// Next, we want to define the fundamental concept of a refletance
+// function, so that we can use it as a building block for other
+// parts of the system. This is a case where we are trying to
+// show how a proper physically-based renderer (PBR) might
+// decompose the problem using Slang, even though our simple
+// example is *not* physically based.
+//
+interface IBRDF
+{
+ // Technically, a BRDF is only a function of the incident
+ // (`wi`) and exitant (`wo`) directions, but for simplicity
+ // we are passing in the surface normal (`N`) as well.
+ //
+ float3 evaluate(float3 wo, float3 wi, float3 N);
+};
+//
+// We can now define various implemntations of the `IBRDF` interface
+// that represent different reflectance functions we want to support.
+// For now we keep things simple by defining about the simplest
+// reflectance function we can think of: the Blinn-Phong reflectance
+// model:
+//
+struct BlinnPhong : IBRDF
+{
+ // Blinn-Phong needs diffuse and specular reflectances, plus
+ // a specular exponent value (which relates to "roughness"
+ // in more modern physically-based models).
+ //
+ float3 kd;
+ float3 ks;
+ float specularity;
+
+ // Here we implement the one requirement of the `IBRDF` interface
+ // for our concrete implementation, using a textbook definition
+ // of Blinng-Phong shading.
+ //
+ // Note: our "BRDF" definition here folds the N-dot-L term into
+ // the evlauation of the reflectance function in case there are
+ // useful algebraic simplifications this enables.
+ //
+ float3 evaluate(float3 V, float3 L, float3 N)
+ {
+ float nDotL = saturate(dot(N, L));
+ float3 H = normalize(L + V);
+ float nDotH = saturate(dot(N, H));
+
+ return kd*nDotL + ks*pow(nDotH, specularity);
+ }
+};
+//
+// It is important to note that a reflectance function is *not*
+// a "material." In most cases, a material will have spatially-varying
+// properties so that it cannot be summarized as a single `IBRDF`
+// instance.
+//
+// Thus a "material" is a value that can produce a BRDF for any point
+// on a surface (e.g., by sampling texture maps, etc.).
+//
+interface IMaterial
+{
+ // Different concrete material implementations might yield BRDF
+ // values with different types. E.g., one material might yield
+ // reflectance functions using `BlinnPhong` while another uses
+ // a much more complicated/accurate representation.
+ //
+ // We encapsulate the choice of BRDF parameters/evaluation in
+ // our material interface with an "associated type." In the
+ // simplest terms, think of this as an interface requirement
+ // that is a type, instead of a method.
+ //
+ // (If you are C++-minded, you might think of this as akin to
+ // how every container provided an `iterator` type, but different
+ // containers may have different types of iterators)
+ //
+ associatedtype BRDF : IBRDF;
+
+ // For our simple example program, it is enough for a material to
+ // be able to return a BRDF given a point on the surface.
+ //
+ // A more complex implementation of material shading might also
+ // have the material return updated surface geometry to reflect
+ // the result of normal mapping, occlusion mapping, etc. or
+ // return an opacity/coverage value for partially transparent
+ // surfaces.
+ //
+ BRDF prepare(SurfaceGeometry geometry);
+};
+
+// We will now define a trivial first implementation of the material
+// interface, which uses our Blinn-Phong BRDF with uniform values
+// for its parameters.
+//
+// Note that this implemetnation is being provided *after* the
+// shader parameter `gMaterial` is declared, so that there is no
+// assumption in the shader code that `gMaterial` will be plugged
+// in using an instance of `SimpleMaterial`
+//
+//
+struct SimpleMaterial : IMaterial
+{
+ // We declare the properties we need as fields of the material type.
+ // When `SimpleMaterial` is used for `TMaterial` above, then
+ // `gMaterial` will be a `ParameterBlock<SimpleMaterial>`, and these
+ // parameters will be allocated to a constant buffer that is part of
+ // that parameter block.
+ //
+ // TODO: A future version of this example will include texture parameters
+ // here to show that they are declared just like simple uniforms.
+ //
+ float3 diffuseColor;
+ float3 specularColor;
+ float specularity;
+
+ // To satisfy the requirements of the `IMaterial` interface, our
+ // material type needs to provide a suitable `BRDF` type. We
+ // do this by using a simple `typedef`, although a nested
+ // `struct` type can also satisfy an associated type requirement.
+ //
+ // A future version of the Slang compiler may allow the "right"
+ // associated type definition to be inferred from the signature
+ // of the `prepare()` method below.
+ //
+ typedef BlinnPhong BRDF;
+
+ BlinnPhong prepare(SurfaceGeometry geometry)
+ {
+ BlinnPhong brdf;
+ brdf.kd = diffuseColor;
+ brdf.ks = specularColor;
+ brdf.specularity = specularity;
+ return brdf;
+ }
+};
+//
+// Note that no other code in this file statically
+// references the `SimpleMaterial` type, and instead
+// it is up to the application to "plug in" this type,
+// or another `IMaterial` implementation for the
+// `TMaterial` parameter.
+//
+
+// A light, or an entire lighting *environment* is an object
+// that can illuminate a surface using some BRDF implemented
+// with our abstractions above.
+//
+interface ILightEnv
+{
+ // The `illuminate` method is intended to integrate incoming
+ // illumination from this light (environment) incident at the
+ // surface point given by `g` (which has the reflectance function
+ // `brdf`) and reflected into the outgoing direction `wo`.
+ //
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo);
+ //
+ // Note that the `illuminate()` method is allowed as an interface
+ // requirement in Slang even though it is a generic. Constract that
+ // with C++ where a `template` method cannot be `virtual`.
+};
+
+// Given the `ILightEnv` interface, we can write up almost textbook
+// definition of directional and point lights.
+
+struct DirectionalLight : ILightEnv
+{
+ float3 direction;
+ float3 intensity;
+
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ return intensity * brdf.evaluate(wo, direction, g.normal);
+ }
+};
+struct PointLight : ILightEnv
+{
+ float3 position;
+ float3 intensity;
+
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ float3 delta = position - g.position;
+ float d = length(delta);
+ float3 direction = normalize(delta);
+ float3 illuminance = intensity / (d*d);
+ return illuminance * brdf.evaluate(wo, direction, g.normal);
+ }
+};
+
+// In most cases, a shader entry point will only be specialized for a single
+// material, but interesting rendering almost always needs multiple lights.
+// For that reason we will next define types to represent *composite* lighting
+// environment with multiple lights.
+//
+// A naive approach might be to have a single undifferntiated list of lights
+// where any type of light may appear at any index, but this would lose all
+// of the benefits of static specialization: we would have to perform dynamic
+// branching to determine what kind of light is stored at each index.
+//
+// Instead, we will start with a type for *homogeneous* arrays of lights:
+//
+struct LightArray<L : ILightEnv, let N : int> : ILightEnv
+{
+ // The `LightArray` type has two generic parameters:
+ //
+ // - `L` is a type parameter, representing the type of lights that will be in our array
+ // - `N` is a generic *value* parameter, representing the maximum number of lights allowed
+ //
+ // Slang's support for generic value parameters is currently experimental,
+ // and the syntax might change.
+
+ int count;
+ L lights[N];
+
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ // Our light array integrates illumination by naively summing
+ // contributions from all the lights in the array (up to `count`).
+ //
+ // A more advanced renderer might try apply sampling techniques
+ // to pick a subset of lights to sample.
+ //
+ float3 sum = 0;
+ for( int ii = 0; ii < count; ++ii )
+ {
+ sum += lights[ii].illuminate(g, brdf, wo);
+ }
+ return sum;
+ }
+};
+
+// `LightArray` can handle multiple lights as long as they have the
+// same type, but we need a way to have a scene with multiple lights
+// of different types *without* losing static specialization.
+//
+// The `LightPair<T,U>` type supports this in about the simplest way
+// possible, by aggregating a light (environment) of type `T` and
+// one of type `U`. Those light environments might themselves be
+// `LightArray`s or `LightPair`s, so that arbitrarily complex
+// environments can be created from just these two composite types.
+//
+// This is probably a good place to insert a reminder the Slang's
+// generics are *not* C++ templates, so that the error messages
+// produced when working with these types are in general reasonable,
+// and this is *not* any form of "template metaprogramming."
+//
+// That said, we expect that future versions of Slang will make
+// defining composite types light this a bit less cumbersome.
+//
+struct LightPair<T : ILightEnv, U : ILightEnv> : ILightEnv
+{
+ T first;
+ U second;
+
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ return first.illuminate(g, brdf, wo)
+ + second.illuminate(g, brdf, wo);
+ }
+};
+
+// As a final (degenerate) case, we will define a light
+// environment with *no* lights, which contributes no illumination.
+//
+struct EmptyLightEnv : ILightEnv
+{
+ float3 illuminate<B:IBRDF>(SurfaceGeometry g, B brdf, float3 wo)
+ {
+ return 0;
+ }
+};
+
+// The code above constitutes the "shader library" for our
+// application, while the code below this point is the
+// implementation of a simple forward rendering pass
+// using that library.
+//
+// While the shader library has used many of Slang's advanced
+// mechanisms, the vertex and fragment shaders will be
+// much more modest, and hopefully easier to follow.
+
+
+// We will start with a `struct` for per-view parameters that
+// will be allocated into a `ParameterBlock`.
+//
+// As written, this isn't very different from using an HLSL
+// `cbuffer` declaration, but importantly this code will
+// continue to work if we add one or more resources (e.g.,
+// an enironment map texture) to the `PerView` type.
+//
+struct PerView
+{
+ float4x4 viewProjection;
+ float3 eyePosition;
+};
+ParameterBlock<PerView> gViewParams;
+
+// Declaring a block for per-model parameter data is
+// similarly simple.
+//
+struct PerModel
+{
+ float4x4 modelTransform;
+ float4x4 inverseTransposeModelTransform;
+};
+ParameterBlock<PerModel> gModelParams;
+
+// We want our shader to work with any kind of lighting environment
+// - that is, and type that implements `ILightEnv`. Furthermore,
+// we want the parameters of that lighting environment to be passed
+// as parameter block - `ParameterBlock<L>` for some type `L`.
+//
+// We handle this by defining a global generic type parameter for
+// our shader, and constrainting it to implement `ILightEnv`...
+//
+type_param TLightEnv : ILightEnv;
+//
+// ... and then defining a parameter block that uses that type
+// parameter as the "element type" of the block:
+//
+ParameterBlock<TLightEnv> gLightEnv;
+
+// Our handling of the material parameter for our shader
+// is quite similar to the case for the lighting environment:
+//
+type_param TMaterial : IMaterial;
+ParameterBlock<TMaterial> gMaterial;
+
+// Our vertex shader entry point is only marginally more
+// complicated than the Hello World example. We will
+// start by declaring the various "connector" `struct`s.
+//
+struct AssembledVertex
+{
+ float3 position : POSITION;
+ float3 normal : NORMAL;
+ float2 uv : UV;
+};
+struct CoarseVertex
+{
+ float3 worldPosition;
+ float3 worldNormal;
+ float2 uv;
+};
+struct VertexStageOutput
+{
+ CoarseVertex coarseVertex : CoarseVertex;
+ float4 sv_position : SV_Position;
+};
+
+// Perhaps most interesting new feature of the entry
+// point decalrations is that we use a `[shader(...)]`
+// attribute (as introduced in HLSL Shader Model 6.x)
+// in order to tag our entry points.
+//
+// This attribute informs the Slang compiler which
+// functions are intended to be compiled as shader
+// entry points (and what stage they target), so that
+// the programmer no longer needs to specify the
+// entry point name/stage through the API (or on
+// the command line when using `slangc`).
+//
+// While HLSL added this feature only in newer versions,
+// the Slang compiler supports this attribute across
+// *all* targets, so that it is okay to use whether you
+// want DXBC, DXIL, or SPIR-V output.
+//
+[shader("vertex")]
+VertexStageOutput vertexMain(
+ AssembledVertex assembledVertex)
+{
+ VertexStageOutput output;
+
+ float3 position = assembledVertex.position;
+ float3 normal = assembledVertex.normal;
+ float2 uv = assembledVertex.uv;
+
+ float3 worldPosition = mul(gModelParams.modelTransform, float4(position, 1.0)).xyz;
+ float3 worldNormal = mul(gModelParams.inverseTransposeModelTransform, float4(normal, 0.0)).xyz;
+
+ output.coarseVertex.worldPosition = worldPosition;
+ output.coarseVertex.worldNormal = worldNormal;
+ output.coarseVertex.uv = uv;
+
+ output.sv_position = mul(gViewParams.viewProjection, float4(worldPosition, 1.0));
+
+ return output;
+}
+
+// Our fragment shader is almost trivial, with the most interesting
+// thing being how it uses the `TMaterial` type parameter (through the
+// value stored in the `gMaterial` parameter block) to dispatch to
+// the correct implementation of the `getDiffuseColor()` method
+// in the `IMaterial` interface.
+//
+// The `gMaterial` parameter block declaration thus serves not only
+// to group certain shader parameters for efficient CPU-to-GPU
+// communication, but also to select the code that will execute
+// in specialized versions of the `fragmentMain` entry point.
+//
+[shader("fragment")]
+float4 fragmentMain(
+ CoarseVertex coarseVertex : CoarseVertex) : SV_Target
+{
+ // We start by using our interpolated vertex attributes
+ // to construct the local surface geometry that we will
+ // use for material evaluation.
+ //
+ SurfaceGeometry g;
+ g.position = coarseVertex.worldPosition;
+ g.normal = normalize(coarseVertex.worldNormal);
+ g.uv = coarseVertex.uv;
+
+ float3 V = normalize(gViewParams.eyePosition - g.position);
+
+ // Next we prepare the material, which involves running
+ // any "pattern generation" logic of the material (e.g.,
+ // sampling and blending texture layers), to produce
+ // a BRDF suitable for evaluating under illumination
+ // from different light sources.
+ //
+ // Note that the return type here is `TMaterial.BRDF`,
+ // which is the `BRDF` type *associated* with the (unknown)
+ // `TMaterial` type. When `TMaterial` gets substituted for
+ // a concrete type later (e.g., `SimpleMaterial`) this
+ // will resolve to a concrete type too (e.g., `SimpleMaterial.BRDF`
+ // which is an alias for `BlinnPhong`).
+ //
+ TMaterial.BRDF brdf = gMaterial.prepare(g);
+
+ // Now that we've done the first step of material evaluation
+ // and sampled texture maps, etc., it is time to start
+ // integrating incident light at our surface point.
+ //
+ // Because we've wrapped up the lighting environment as
+ // a single (composite) object, this is as simple as calling
+ // its `illuminate()` method. Our particular fragment shader
+ // is thus abstracted from how the renderer chooses to structure
+ // this integration step, somewhat similar to how an
+ // `illuminance` loop in RenderMan Shading Language works.
+ //
+
+ float3 color = gLightEnv.illuminate(g, brdf, V);
+
+ return float4(color, 1);
+}