From a5ac4999b4dea546a7ef824669ab1809224b6448 Mon Sep 17 00:00:00 2001 From: Yong He Date: Thu, 4 Mar 2021 16:25:58 -0800 Subject: 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 --- .../heterogeneous-hello-world/README.md | 4 + .../heterogeneous-hello-world/main.cpp | 380 +++ .../heterogeneous-hello-world/shader.cpp | 197 ++ .../heterogeneous-hello-world/shader.slang | 65 + examples/experimental/model-viewer/README.md | 25 + examples/experimental/model-viewer/cube.mtl | 35 + examples/experimental/model-viewer/main.cpp | 2446 ++++++++++++++++++++ examples/experimental/model-viewer/shaders.slang | 485 ++++ examples/gpu-printing/main.cpp | 19 +- examples/hello-world/main.cpp | 68 +- examples/heterogeneous-hello-world/README.md | 4 - examples/heterogeneous-hello-world/main.cpp | 370 --- examples/heterogeneous-hello-world/shader.cpp | 194 -- examples/heterogeneous-hello-world/shader.slang | 65 - examples/model-viewer/README.md | 25 - examples/model-viewer/cube.mtl | 35 - examples/model-viewer/cube.obj | 43 - examples/model-viewer/main.cpp | 2443 ------------------- examples/model-viewer/shaders.slang | 485 ---- examples/shader-object/main.cpp | 29 +- examples/shader-toy/main.cpp | 87 +- 21 files changed, 3766 insertions(+), 3738 deletions(-) create mode 100644 examples/experimental/heterogeneous-hello-world/README.md create mode 100644 examples/experimental/heterogeneous-hello-world/main.cpp create mode 100644 examples/experimental/heterogeneous-hello-world/shader.cpp create mode 100644 examples/experimental/heterogeneous-hello-world/shader.slang create mode 100644 examples/experimental/model-viewer/README.md create mode 100644 examples/experimental/model-viewer/cube.mtl create mode 100644 examples/experimental/model-viewer/main.cpp create mode 100644 examples/experimental/model-viewer/shaders.slang delete mode 100644 examples/heterogeneous-hello-world/README.md delete mode 100644 examples/heterogeneous-hello-world/main.cpp delete mode 100644 examples/heterogeneous-hello-world/shader.cpp delete mode 100644 examples/heterogeneous-hello-world/shader.slang delete mode 100644 examples/model-viewer/README.md delete mode 100644 examples/model-viewer/cube.mtl delete mode 100644 examples/model-viewer/cube.obj delete mode 100644 examples/model-viewer/main.cpp delete mode 100644 examples/model-viewer/shaders.slang (limited to 'examples') 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 +// +// 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 gShaderProgram; +Slang::ComPtr gRenderer; + +ComPtr gStructuredBuffer; + +ComPtr gPipelineLayout; +ComPtr gPipelineState; +ComPtr gDescriptorSetLayout; +ComPtr 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 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 _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 _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 convertBuffer_0(gfx_BufferResource_0* _0) { + RWStructuredBuffer result; + result.data = (float*)_0; + return result; +} + +gfx_BufferResource_0* unconvertBuffer_0(RWStructuredBuffer _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 operator*(Vector a, Vector b) +{ + Vector r; + r.x = a.x * b.x; + r.y = a.y * b.y; + r.z = a.z * b.z; + return r; +} + +Vector operator+(Vector a, Vector b) +{ + Vector r; + r.x = a.x + b.x; + r.y = a.y + b.y; + r.z = a.z + b.z; + return r; +} + +Vector make_VecU3(uint32_t a, uint32_t b, uint32_t c) +{ + return Vector{ 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 gridDims, + RWStructuredBuffer 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 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 _1); + + +#line 4 +RWStructuredBuffer convertBuffer_0(gfx_BufferResource_0* _0); + + +#line 40 +void printInitialValues_0(FixedArray _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 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 _S12 = make_VecU3(uint32_t(int(4)), uint32_t(int(1)), uint32_t(int(1))); + RWStructuredBuffer _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 convertBuffer(Ptr x); + +[shader("compute")] +[numthreads(4, 1, 1)] +void computeMain(uniform RWStructuredBuffer 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 loadShaderProgram(Ptr renderer); +Ptr createWindow(int gWindowWidth, int gWindowHeight); +Ptr createRenderer( + int gWindowWidth, + int gWindowHeight, + Ptr gWindow); +Ptr createStructuredBuffer(Ptr gRenderer, float[4] initialArray); +void printInitialValues(float[4] initialArray, int length); +void print_output( + Ptr gRenderer, + Ptr 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 +#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 +#include +#include +#include +#include + +// 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 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 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 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 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 = 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; + + // The entry points that comprise the program + // (e.g., both a vertex and a fragment entry point). + std::vector> 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 x; // block 0 + // ParameterBlock y; // block 1 + // ParameterBlock 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` + // then we can infer that `A` should be bound to `Bar`. + // + struct GenericParam + { + int parameterBlockIndex; + }; + std::vector genericParams; +}; +// +// As with entry points, loading a program is done with +// the help of Slang's reflection API. +// +RefPtr loadProgram( + ShaderModule* module, + int entryPointCount, + const char* const* entryPointNames) +{ + auto slangReflection = module->slangReflection; + + RefPtr 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` 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` 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 loadProgram(ShaderModule* module, char const* entryPoint0, char const* entryPoint1) +{ + char const* entryPointNames[] = { entryPoint0, entryPoint1 }; + return loadProgram(module, 2, entryPointNames); +} + +// The `ParameterBlock` 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 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` 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 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 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 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 = 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`). +// +// 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` +// 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 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 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 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 + 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 + 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 renderer; + + // The associated parameter block layout. + RefPtr layout; + + // The (optional) constant buffer that holds the values + // for any ordinay fields. This will be null if + // `layout->primaryConstantBufferSize` is zero. + ComPtr primaryConstantBuffer; + + // The graphics-API descriptor set that provides storage + // for any resource fields. + ComPtr descriptorSet; + + ParameterBlockEncoder beginEncoding(); +}; + +// Allocating a parameter block is mostly a matter of allocating +// the required graphics API objects. +// +RefPtr 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 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 = 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 allocatePersistentParameterBlock( + ParameterBlockLayout* layout) +{ + return allocateParameterBlockImpl(layout); +} +RefPtr 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; + + // Additional state corresponding to the data needed + // to create a graphics-API pipeline state object. + ComPtr 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 pipelineLayout; + ComPtr pipelineState; +}; +// +// A specialized variant is created based on a base effect +// and the types that will be bound to its parameter blocks. +// +RefPtr 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 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 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 kernelBlobs; + std::vector 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 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 > 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 getEffectVariant( + VariantKey const& key, + IFramebufferLayout* framebufferLayout) + { + RefPtr 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 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; + + // 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; + RefPtr 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 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 createParameterBlock() = 0; + + // The parameter block for a material will be stashed here + // after it is created. + RefPtr 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 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 gParameterBlockLayout; +}; +RefPtr 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; + int firstIndex; + int indexCount; +}; +struct Model : RefObject +{ + typedef ModelLoader::Vertex Vertex; + + ComPtr vertexBuffer; + ComPtr indexBuffer; + PrimitiveTopology primitiveTopology; + int vertexCount; + int indexCount; + std::vector> 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 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 +struct LightTypeImpl +{ + static LightType type; + static Light* create() { return (Light*)(new T); } +}; +#define DEFINE_LIGHT_TYPE(NAME) \ + LightType LightTypeImpl::type = { #NAME, &LightTypeImpl::create }; +template +LightType* getLightType() +{ + return &LightTypeImpl::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(); }; + + 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(); }; + + 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 lightLayout; + RefPtr arrayLayout; + Int maximumCount = 0; + }; + RefPtr module; + std::vector> lightArrayLayouts; + std::map mapLightTypeToArrayIndex; + + LightEnvLayout(ShaderModule* module) + : module(module) + {} + + void addLightType(LightType* type, Int maximumCount) + { + Int arrayIndex = (Int)lightArrayLayouts.size(); + RefPtr 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` + // + 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 + void addLightType(Int maximumCount) + { + addLightType(getLightType(), 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; + RefPtr getParameterBlockLayout() + { + if (!parameterBlockLayout) + { + parameterBlockLayout = computeParameterBlockLayout(); + } + return parameterBlockLayout; + } + + RefPtr computeParameterBlockLayout() + { + // Given a lighting environment with N light types: + // + // L0, L1, ... LN + // + // We want to compute the Slang type: + // + // LightPair>> + // + // This is most easily accomplished by doing a "fold" while + // walking the array in reverse order. + + RefPtr envLayout; + + auto arrayCount = lightArrayLayouts.size(); + for (size_t ii = arrayCount; ii--;) + { + auto arrayInfo = lightArrayLayouts[ii]; + RefPtr 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` + 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 layout; + LightEnv(RefPtr layout) + : layout(layout) + { + for (auto arrayLayout : layout->lightArrayLayouts) + { + RefPtr 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 layout; + std::vector> lights; + }; + std::vector> lightArrays; + + RefPtr getArrayForType(LightType* type) + { + auto index = layout->getArrayIndexForType(type); + return lightArrays[index]; + } + + void add(RefPtr 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 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>> + // + // 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` 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`. + // + 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`, + // in which case we need to encode the first field (the light count)... + // + lightTypeEncoder.writeField(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 gRenderer; +ComPtr gSwapchain; +ComPtr gFramebufferLayout; +Slang::List> 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 gEffect; +RefPtr gPerViewParameterBlockLayout; +RefPtr gPerModelParameterBlockLayout; + +RefPtr shaderCache; +RefPtr 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> gModels; +RefPtr 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 depthBufferResource = gRenderer->createTextureResource( + gfx::IResource::Usage::DepthWrite, depthBufferDesc, nullptr); + ComPtr 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 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 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 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 = 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 = new LightEnvLayout(shaderModule); + lightEnvLayout->addLightType(10); + lightEnvLayout->addLightType(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`, 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(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(SurfaceGeometry g, B brdf, float3 wo) + { + return intensity * brdf.evaluate(wo, direction, g.normal); + } +}; +struct PointLight : ILightEnv +{ + float3 position; + float3 intensity; + + float3 illuminate(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 : 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(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` 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 : ILightEnv +{ + T first; + U second; + + float3 illuminate(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(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 gViewParams; + +// Declaring a block for per-model parameter data is +// similarly simple. +// +struct PerModel +{ + float4x4 modelTransform; + float4x4 inverseTransposeModelTransform; +}; +ParameterBlock 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` 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 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 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); +} diff --git a/examples/gpu-printing/main.cpp b/examples/gpu-printing/main.cpp index 4f79147f6..63eb31a82 100644 --- a/examples/gpu-printing/main.cpp +++ b/examples/gpu-printing/main.cpp @@ -175,19 +175,26 @@ Result execute() printBufferViewDesc.type = IResourceView::Type::UnorderedAccess; auto printBufferView = gRenderer->createBufferView(printBuffer, printBufferViewDesc); + ICommandQueue::Desc queueDesc = {ICommandQueue::QueueType::Graphics}; + auto queue = gRenderer->createCommandQueue(queueDesc); + auto commandBuffer = queue->createCommandBuffer(); + auto encoder = commandBuffer->encodeComputeCommands(); // TODO: need to copy a zero into the start of the print buffer! gDescriptorSet->setResource(0, 0, printBufferView); - gRenderer->setDescriptorSet(PipelineType::Compute, gPipelineLayout, 0, gDescriptorSet); - - gRenderer->setPipelineState(gPipelineState); - gRenderer->dispatchCompute(1, 1, 1); + encoder->setDescriptorSet(gPipelineLayout, 0, gDescriptorSet); + encoder->setPipelineState(gPipelineState); + encoder->dispatchCompute(1, 1, 1); + encoder->endEncoding(); + commandBuffer->close(); + queue->executeCommandBuffer(commandBuffer); // TODO: need to copy from the print buffer to a staging buffer... - auto printBufferData = (uint32_t*) gRenderer->map(printBuffer, MapFlavor::HostRead); + ComPtr blob; + gRenderer->readBufferResource(printBuffer, 0, printBufferSize, blob.writeRef()); - gGPUPrinting.processGPUPrintCommands(printBufferData, printBufferSize); + gGPUPrinting.processGPUPrintCommands(blob->getBufferPointer(), printBufferSize); return SLANG_OK; } diff --git a/examples/hello-world/main.cpp b/examples/hello-world/main.cpp index f0ea3eb2a..47016b48d 100644 --- a/examples/hello-world/main.cpp +++ b/examples/hello-world/main.cpp @@ -219,6 +219,8 @@ ComPtr gRootObject; ComPtr gSwapchain; List> gFramebuffers; ComPtr gVertexBuffer; +ComPtr gRenderPass; +ComPtr gQueue; // Now that we've covered the function that actually loads and // compiles our Slang shade code, we can go through the rest @@ -246,6 +248,10 @@ Slang::Result initialize() gfx::Result res = gfxCreateRenderer(&rendererDesc, gRenderer.writeRef()); if(SLANG_FAILED(res)) return res; + ICommandQueue::Desc queueDesc = {}; + queueDesc.type = ICommandQueue::QueueType::Graphics; + gQueue = gRenderer->createCommandQueue(queueDesc); + // Now we will create objects needed to configur the "input assembler" // (IA) stage of the D3D pipeline. // @@ -316,6 +322,7 @@ Slang::Result initialize() swapchainDesc.width = gWindowWidth; swapchainDesc.height = gWindowHeight; swapchainDesc.imageCount = kSwapchainImageCount; + swapchainDesc.queue = gQueue; gSwapchain = gRenderer->createSwapchain( swapchainDesc, gfx::WindowHandle::FromHwnd(getPlatformWindowHandle(gWindow))); @@ -331,7 +338,7 @@ Slang::Result initialize() for (uint32_t i = 0; i < kSwapchainImageCount; i++) { - gfx::ITextureResource::Desc depthBufferDesc; + gfx::ITextureResource::Desc depthBufferDesc = {}; depthBufferDesc.setDefaults(gfx::IResource::Usage::DepthWrite); depthBufferDesc.init2D( gfx::IResource::Type::Texture2D, @@ -345,23 +352,21 @@ Slang::Result initialize() ComPtr colorBuffer; gSwapchain->getImage(i, colorBuffer.writeRef()); - gfx::IResourceView::Desc colorBufferViewDesc; - memset(&colorBufferViewDesc, 0, sizeof(colorBufferViewDesc)); + gfx::IResourceView::Desc colorBufferViewDesc = {}; colorBufferViewDesc.format = gSwapchain->getDesc().format; colorBufferViewDesc.renderTarget.shape = gfx::IResource::Type::Texture2D; colorBufferViewDesc.type = gfx::IResourceView::Type::RenderTarget; ComPtr rtv = gRenderer->createTextureView(colorBuffer.get(), colorBufferViewDesc); - gfx::IResourceView::Desc depthBufferViewDesc; - memset(&depthBufferViewDesc, 0, sizeof(depthBufferViewDesc)); + gfx::IResourceView::Desc depthBufferViewDesc = {}; depthBufferViewDesc.format = gfx::Format::D_Float32; depthBufferViewDesc.renderTarget.shape = gfx::IResource::Type::Texture2D; depthBufferViewDesc.type = gfx::IResourceView::Type::DepthStencil; ComPtr dsv = gRenderer->createTextureView(depthBufferResource.get(), depthBufferViewDesc); - gfx::IFramebuffer::Desc framebufferDesc; + gfx::IFramebuffer::Desc framebufferDesc = {}; framebufferDesc.renderTargetCount = 1; framebufferDesc.depthStencilView = dsv.get(); framebufferDesc.renderTargetViews = rtv.readRef(); @@ -383,6 +388,23 @@ Slang::Result initialize() gPipelineState = pipelineState; + gfx::IRenderPassLayout::Desc renderPassDesc = {}; + renderPassDesc.framebufferLayout = framebufferLayout; + renderPassDesc.renderTargetCount = 1; + IRenderPassLayout::AttachmentAccessDesc renderTargetAccess = {}; + IRenderPassLayout::AttachmentAccessDesc depthStencilAccess = {}; + renderTargetAccess.loadOp = IRenderPassLayout::AttachmentLoadOp::Clear; + renderTargetAccess.storeOp = IRenderPassLayout::AttachmentStoreOp::Store; + renderTargetAccess.initialState = ResourceState::Undefined; + renderTargetAccess.finalState = ResourceState::Present; + depthStencilAccess.loadOp = IRenderPassLayout::AttachmentLoadOp::Clear; + depthStencilAccess.storeOp = IRenderPassLayout::AttachmentStoreOp::Store; + depthStencilAccess.initialState = ResourceState::Undefined; + depthStencilAccess.finalState = ResourceState::DepthWrite; + renderPassDesc.renderTargetAccess = &renderTargetAccess; + renderPassDesc.depthStencilAccess = &depthStencilAccess; + gRenderPass = gRenderer->createRenderPassLayout(renderPassDesc); + // Once we've initialized all the graphics API objects, // it is time to show our application window and start rendering. // @@ -398,22 +420,16 @@ Slang::Result initialize() // void renderFrame() { - gRenderer->beginFrame(); uint32_t frameBufferIndex = gSwapchain->acquireNextImage(); - gRenderer->setFramebuffer(gFramebuffers[frameBufferIndex]); + + ComPtr commandBuffer = gQueue->createCommandBuffer(); + auto renderEncoder = commandBuffer->encodeRenderCommands(gRenderPass, gFramebuffers[frameBufferIndex]); gfx::Viewport viewport = {}; viewport.maxZ = 1.0f; viewport.extentX = (float)gWindowWidth; viewport.extentY = (float)gWindowHeight; - gRenderer->setViewportAndScissor(viewport); - - - // We start by clearing our framebuffer, which only has a color target. - // - static const float kClearColor[] = { 0.25, 0.25, 0.25, 1.0 }; - gRenderer->setClearColor(kClearColor); - gRenderer->clearFrame(); + renderEncoder->setViewportAndScissor(viewport); // We will update the model-view-projection matrix that is passed // into the shader code via the `Uniforms` buffer on a per-frame @@ -485,31 +501,31 @@ void renderFrame() // PSO, binding our root shader object to it (which references // the `Uniforms` buffer that will filled in above). // - gRenderer->setPipelineState(gPipelineState); - gRenderer->bindRootShaderObject(PipelineType::Graphics, gRootObject); + renderEncoder->setPipelineState(gPipelineState); + renderEncoder->bindRootShaderObject(gRootObject); // We also need to set up a few pieces of fixed-function pipeline // state that are not bound by the pipeline state above. // - gRenderer->setVertexBuffer(0, gVertexBuffer, sizeof(Vertex)); - gRenderer->setPrimitiveTopology(PrimitiveTopology::TriangleList); + renderEncoder->setVertexBuffer(0, gVertexBuffer, sizeof(Vertex)); + renderEncoder->setPrimitiveTopology(PrimitiveTopology::TriangleList); // Finally, we are ready to issue a draw call for a single triangle. // - gRenderer->draw(3); + renderEncoder->draw(3); + renderEncoder->endEncoding(); + commandBuffer->close(); + gQueue->executeCommandBuffer(commandBuffer); // With that, we are done drawing for one frame, and ready for the next. // - gRenderer->makeSwapchainImagePresentable(gSwapchain); - - gRenderer->endFrame(); - gSwapchain->present(); } void finalize() { - gRenderer->waitForGpu(); + gQueue->wait(); + gSwapchain = nullptr; destroyWindow(gWindow); } diff --git a/examples/heterogeneous-hello-world/README.md b/examples/heterogeneous-hello-world/README.md deleted file mode 100644 index 709652922..000000000 --- a/examples/heterogeneous-hello-world/README.md +++ /dev/null @@ -1,4 +0,0 @@ -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/heterogeneous-hello-world/main.cpp b/examples/heterogeneous-hello-world/main.cpp deleted file mode 100644 index 010434bfa..000000000 --- a/examples/heterogeneous-hello-world/main.cpp +++ /dev/null @@ -1,370 +0,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 -// -// 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 gShaderProgram; -Slang::ComPtr gRenderer; - -ComPtr gStructuredBuffer; - -ComPtr gPipelineLayout; -ComPtr gPipelineState; -ComPtr gDescriptorSetLayout; -ComPtr 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::IRenderer* gRenderer, - gfx::IPipelineState* gPipelineState, - gfx::IPipelineLayout* gPipelineLayout, - gfx::IDescriptorSet* gDescriptorSet, - unsigned int gridDimsX, - unsigned int gridDimsY, - unsigned int gridDimsZ) -{ - - gRenderer->setPipelineState(gPipelineState); - gRenderer->setDescriptorSet(PipelineType::Compute, gPipelineLayout, 0, gDescriptorSet); - - gRenderer->dispatchCompute(gridDimsX, gridDimsY, gridDimsZ); -} - -void print_output( - gfx::IRenderer* renderer, - gfx::IBufferResource* structuredBuffer, - int length) -{ - if (float* outputData = (float*)renderer->map(structuredBuffer, MapFlavor::HostRead)) - { - // Print out the values the the kernel produced - printf("After: \n"); - for (int i = 0; i < 4; i++) - { - printf("%f, ", outputData[i]); - } - printf("\n"); - - renderer->unmap(structuredBuffer); - } -} - -// 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 _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 _0, int32_t _1) -{ - printInitialValues((float*)&_0, _1); -} - -void dispatchComputation_0(gfx_Renderer_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::IRenderer*)_0, - (gfx::IPipelineState*)_1, - (gfx::IPipelineLayout*)_2, - (gfx::IDescriptorSet*)_3, - gridDimsX, - gridDimsY, - gridDimsZ); -} - -RWStructuredBuffer convertBuffer_0(gfx_BufferResource_0* _0) { - RWStructuredBuffer result; - result.data = (float*)_0; - return result; -} - -gfx_BufferResource_0* unconvertBuffer_0(RWStructuredBuffer _0) { - return (gfx_BufferResource_0*)(_0.data); -} - -void print_output_0(gfx_Renderer_0* _0, gfx_BufferResource_0* _1, int32_t _2) -{ - print_output((gfx::IRenderer*)_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) diff --git a/examples/heterogeneous-hello-world/shader.cpp b/examples/heterogeneous-hello-world/shader.cpp deleted file mode 100644 index 640e8aa3c..000000000 --- a/examples/heterogeneous-hello-world/shader.cpp +++ /dev/null @@ -1,194 +0,0 @@ -#include "../../prelude/slang-cpp-prelude.h" - - -#ifdef SLANG_PRELUDE_NAMESPACE -using namespace SLANG_PRELUDE_NAMESPACE; -#endif - -Vector operator*(Vector a, Vector b) -{ - Vector r; - r.x = a.x * b.x; - r.y = a.y * b.y; - r.z = a.z * b.z; - return r; -} - -Vector operator+(Vector a, Vector b) -{ - Vector r; - r.x = a.x + b.x; - r.y = a.y + b.y; - r.z = a.z + b.z; - return r; -} - -Vector make_VecU3(uint32_t a, uint32_t b, uint32_t c) -{ - return Vector{ 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 gridDims, - RWStructuredBuffer 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 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 _1); - - -#line 4 -RWStructuredBuffer convertBuffer_0(gfx_BufferResource_0* _0); - - -#line 40 -void printInitialValues_0(FixedArray _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 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_BufferResource_0* _S11 = createStructuredBuffer_0(_S10, initialArray_0); - Vector _S12 = make_VecU3(uint32_t(int(4)), uint32_t(int(1)), uint32_t(int(1))); - RWStructuredBuffer _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); - } - } - } -} diff --git a/examples/heterogeneous-hello-world/shader.slang b/examples/heterogeneous-hello-world/shader.slang deleted file mode 100644 index 47c883b39..000000000 --- a/examples/heterogeneous-hello-world/shader.slang +++ /dev/null @@ -1,65 +0,0 @@ -// shader.slang - -//TEST_INPUT:ubuffer(random(float, 4096, -1.0, 1.0), stride=4):name=ioBuffer -RWStructuredBuffer convertBuffer(Ptr x); - -[shader("compute")] -[numthreads(4, 1, 1)] -void computeMain(uniform RWStructuredBuffer 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 loadShaderProgram(Ptr renderer); -Ptr createWindow(int gWindowWidth, int gWindowHeight); -Ptr createRenderer( - int gWindowWidth, - int gWindowHeight, - Ptr gWindow); -Ptr createStructuredBuffer(Ptr gRenderer, float[4] initialArray); -void printInitialValues(float[4] initialArray, int length); -void print_output( - Ptr gRenderer, - Ptr 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/model-viewer/README.md b/examples/model-viewer/README.md deleted file mode 100644 index a350a48a2..000000000 --- a/examples/model-viewer/README.md +++ /dev/null @@ -1,25 +0,0 @@ -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/model-viewer/cube.mtl b/examples/model-viewer/cube.mtl deleted file mode 100644 index 6c8eeb10b..000000000 --- a/examples/model-viewer/cube.mtl +++ /dev/null @@ -1,35 +0,0 @@ -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/model-viewer/cube.obj b/examples/model-viewer/cube.obj deleted file mode 100644 index 2f7de8a92..000000000 --- a/examples/model-viewer/cube.obj +++ /dev/null @@ -1,43 +0,0 @@ -mtllib cube.mtl -o Cube -v 1.000000 -1.000000 -1.000000 -v 1.000000 -1.000000 1.000000 -v -1.000000 -1.000000 1.000000 -v -1.000000 -1.000000 -1.000000 -v 1.000000 1.000000 -1.000000 -v 1.000000 1.000000 1.000000 -v -1.000000 1.000000 1.000000 -v -1.000000 1.000000 -1.000000 -vn 0.000000 -1.000000 0.000000 -vn 0.000000 1.000000 0.000000 -vn 1.000000 0.000000 0.000000 -vn 0.000000 0.000000 1.000000 -vn -1.000000 0.000000 0.000000 -vn 0.000000 0.000000 -1.000000 - -v -10 -1 -10 -v 10 -1 -10 -v 10 -1 10 -v -10 -1 10 -vn 0 1 0 - -usemtl Red -s off -f 2//3 6//3 5//3 1//3 -f 4//5 8//5 7//5 3//5 - -usemtl Green -s off -f 4//1 3//1 2//1 1//1 -f 6//2 7//2 8//2 5//2 - -usemtl Blue -s off -f 3//4 7//4 6//4 2//4 -f 8//6 4//6 1//6 5//6 - -o Ground -usemtl Ground -s off -f 9//7 10//7 11//7 12//7 - diff --git a/examples/model-viewer/main.cpp b/examples/model-viewer/main.cpp deleted file mode 100644 index c9693e529..000000000 --- a/examples/model-viewer/main.cpp +++ /dev/null @@ -1,2443 +0,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 -#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 -#include -#include -#include -#include - -// 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 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 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 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 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 = 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; - - // The entry points that comprise the program - // (e.g., both a vertex and a fragment entry point). - std::vector> 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 x; // block 0 - // ParameterBlock y; // block 1 - // ParameterBlock 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` - // then we can infer that `A` should be bound to `Bar`. - // - struct GenericParam - { - int parameterBlockIndex; - }; - std::vector genericParams; -}; -// -// As with entry points, loading a program is done with -// the help of Slang's reflection API. -// -RefPtr loadProgram( - ShaderModule* module, - int entryPointCount, - const char* const* entryPointNames) -{ - auto slangReflection = module->slangReflection; - - RefPtr 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` 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` 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 loadProgram(ShaderModule* module, char const* entryPoint0, char const* entryPoint1) -{ - char const* entryPointNames[] = { entryPoint0, entryPoint1 }; - return loadProgram(module, 2, entryPointNames); -} - -// The `ParameterBlock` 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 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` 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 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 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 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 = 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`). -// -// 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` -// 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 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 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 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 - 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 - 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 renderer; - - // The associated parameter block layout. - RefPtr layout; - - // The (optional) constant buffer that holds the values - // for any ordinay fields. This will be null if - // `layout->primaryConstantBufferSize` is zero. - ComPtr primaryConstantBuffer; - - // The graphics-API descriptor set that provides storage - // for any resource fields. - ComPtr descriptorSet; - - ParameterBlockEncoder beginEncoding(); -}; - -// Allocating a parameter block is mostly a matter of allocating -// the required graphics API objects. -// -RefPtr 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 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 = 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 allocatePersistentParameterBlock( - ParameterBlockLayout* layout) -{ - return allocateParameterBlockImpl(layout); -} -RefPtr 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; - - // Additional state corresponding to the data needed - // to create a graphics-API pipeline state object. - ComPtr 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 pipelineLayout; - ComPtr pipelineState; -}; -// -// A specialized variant is created based on a base effect -// and the types that will be bound to its parameter blocks. -// -RefPtr 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 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 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 kernelBlobs; - std::vector 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 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 > 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 getEffectVariant( - VariantKey const& key, - IFramebufferLayout* framebufferLayout) - { - RefPtr 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 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; - - // 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; - RefPtr 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 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 createParameterBlock() = 0; - - // The parameter block for a material will be stashed here - // after it is created. - RefPtr 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 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 gParameterBlockLayout; -}; -RefPtr 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; - int firstIndex; - int indexCount; -}; -struct Model : RefObject -{ - typedef ModelLoader::Vertex Vertex; - - ComPtr vertexBuffer; - ComPtr indexBuffer; - PrimitiveTopology primitiveTopology; - int vertexCount; - int indexCount; - std::vector> 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 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 -struct LightTypeImpl -{ - static LightType type; - static Light* create() { return (Light*)(new T); } -}; -#define DEFINE_LIGHT_TYPE(NAME) \ - LightType LightTypeImpl::type = { #NAME, &LightTypeImpl::create }; -template -LightType* getLightType() -{ - return &LightTypeImpl::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(); }; - - 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(); }; - - 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 lightLayout; - RefPtr arrayLayout; - Int maximumCount = 0; - }; - RefPtr module; - std::vector> lightArrayLayouts; - std::map mapLightTypeToArrayIndex; - - LightEnvLayout(ShaderModule* module) - : module(module) - {} - - void addLightType(LightType* type, Int maximumCount) - { - Int arrayIndex = (Int)lightArrayLayouts.size(); - RefPtr 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` - // - 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 - void addLightType(Int maximumCount) - { - addLightType(getLightType(), 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; - RefPtr getParameterBlockLayout() - { - if (!parameterBlockLayout) - { - parameterBlockLayout = computeParameterBlockLayout(); - } - return parameterBlockLayout; - } - - RefPtr computeParameterBlockLayout() - { - // Given a lighting environment with N light types: - // - // L0, L1, ... LN - // - // We want to compute the Slang type: - // - // LightPair>> - // - // This is most easily accomplished by doing a "fold" while - // walking the array in reverse order. - - RefPtr envLayout; - - auto arrayCount = lightArrayLayouts.size(); - for (size_t ii = arrayCount; ii--;) - { - auto arrayInfo = lightArrayLayouts[ii]; - RefPtr 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` - 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 layout; - LightEnv(RefPtr layout) - : layout(layout) - { - for (auto arrayLayout : layout->lightArrayLayouts) - { - RefPtr 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 layout; - std::vector> lights; - }; - std::vector> lightArrays; - - RefPtr getArrayForType(LightType* type) - { - auto index = layout->getArrayIndexForType(type); - return lightArrays[index]; - } - - void add(RefPtr 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 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>> - // - // 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` 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`. - // - 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`, - // in which case we need to encode the first field (the light count)... - // - lightTypeEncoder.writeField(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 gRenderer; -ComPtr gSwapchain; -ComPtr gFramebufferLayout; -Slang::List> 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 gEffect; -RefPtr gPerViewParameterBlockLayout; -RefPtr gPerModelParameterBlockLayout; - -RefPtr shaderCache; -RefPtr 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> gModels; -RefPtr 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 depthBufferResource = gRenderer->createTextureResource( - gfx::IResource::Usage::DepthWrite, depthBufferDesc, nullptr); - ComPtr 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 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 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 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 = 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 = new LightEnvLayout(shaderModule); - lightEnvLayout->addLightType(10); - lightEnvLayout->addLightType(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() -{ - gRenderer->beginFrame(); - 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(); - - gui->endFrame(); - - gRenderer->makeSwapchainImagePresentable(gSwapchain); - gRenderer->endFrame(); - 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) diff --git a/examples/model-viewer/shaders.slang b/examples/model-viewer/shaders.slang deleted file mode 100644 index 15ce0120d..000000000 --- a/examples/model-viewer/shaders.slang +++ /dev/null @@ -1,485 +0,0 @@ -// 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`, 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(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(SurfaceGeometry g, B brdf, float3 wo) - { - return intensity * brdf.evaluate(wo, direction, g.normal); - } -}; -struct PointLight : ILightEnv -{ - float3 position; - float3 intensity; - - float3 illuminate(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 : 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(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` 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 : ILightEnv -{ - T first; - U second; - - float3 illuminate(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(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 gViewParams; - -// Declaring a block for per-model parameter data is -// similarly simple. -// -struct PerModel -{ - float4x4 modelTransform; - float4x4 inverseTransposeModelTransform; -}; -ParameterBlock 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` 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 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 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); -} diff --git a/examples/shader-object/main.cpp b/examples/shader-object/main.cpp index 9b1b4de72..d368cd9aa 100644 --- a/examples/shader-object/main.cpp +++ b/examples/shader-object/main.cpp @@ -136,7 +136,7 @@ int main() // interacting with the graphics API. Slang::ComPtr renderer; IRenderer::Desc rendererDesc = {}; - rendererDesc.rendererType = RendererType::CUDA; + rendererDesc.rendererType = RendererType::DirectX11; SLANG_RETURN_ON_FAIL(gfxCreateRenderer(&rendererDesc, renderer.writeRef())); // Now we can load the shader code. @@ -146,7 +146,7 @@ int main() slang::ProgramLayout* slangReflection; SLANG_RETURN_ON_FAIL(loadShaderProgram(renderer, shaderProgram, slangReflection)); - // Create a pipelien state with the loaded shader. + // Create a pipeline state with the loaded shader. gfx::ComputePipelineStateDesc pipelineDesc = {}; pipelineDesc.program = shaderProgram.get(); ComPtr pipelineState; @@ -211,17 +211,26 @@ int main() // We have set up all required parameters in entry-point object, now it is time // to bind the pipeline and root object and launch the kernel. - renderer->beginFrame(); - renderer->setPipelineState(pipelineState); - SLANG_RETURN_ON_FAIL(renderer->bindRootShaderObject(gfx::PipelineType::Compute, rootObject)); - renderer->dispatchCompute(1, 1, 1); - renderer->endFrame(); + { + ICommandQueue::Desc queueDesc = {ICommandQueue::QueueType::Graphics}; + auto queue = renderer->createCommandQueue(queueDesc); + auto commandBuffer = queue->createCommandBuffer(); + auto encoder = commandBuffer->encodeComputeCommands(); + encoder->setPipelineState(pipelineState); + encoder->bindRootShaderObject(rootObject); + encoder->dispatchCompute(1, 1, 1); + encoder->endEncoding(); + commandBuffer->close(); + queue->executeCommandBuffer(commandBuffer); + queue->wait(); + } // Read back the results. - renderer->waitForGpu(); - float* result = (float*)renderer->map(numbersBuffer, gfx::MapFlavor::HostRead); + ComPtr resultBlob; + SLANG_RETURN_ON_FAIL(renderer->readBufferResource( + numbersBuffer, 0, numberCount * sizeof(float), resultBlob.writeRef())); + auto result = reinterpret_cast(resultBlob->getBufferPointer()); for (int i = 0; i < numberCount; i++) printf("%f\n", result[i]); - renderer->unmap(numbersBuffer); return SLANG_OK; } diff --git a/examples/shader-toy/main.cpp b/examples/shader-toy/main.cpp index 697bb1044..0d058fa2c 100644 --- a/examples/shader-toy/main.cpp +++ b/examples/shader-toy/main.cpp @@ -339,6 +339,8 @@ ComPtr gDescriptorSet; ComPtr gVertexBuffer; ComPtr gSwapchain; Slang::List> gFramebuffers; +ComPtr gRenderPass; +ComPtr gQueue; Result initialize() { @@ -355,6 +357,10 @@ Result initialize() Result res = gfxCreateRenderer(&rendererDesc, gRenderer.writeRef()); if(SLANG_FAILED(res)) return res; + ICommandQueue::Desc queueDesc = {}; + queueDesc.type = ICommandQueue::QueueType::Graphics; + gQueue = gRenderer->createCommandQueue(queueDesc); + int constantBufferSize = sizeof(Uniforms); IBufferResource::Desc constantBufferDesc; @@ -423,6 +429,7 @@ Result initialize() swapchainDesc.width = gWindowWidth; swapchainDesc.height = gWindowHeight; swapchainDesc.imageCount = kSwapchainImageCount; + swapchainDesc.queue = gQueue; gSwapchain = gRenderer->createSwapchain( swapchainDesc, gfx::WindowHandle::FromHwnd(getPlatformWindowHandle(gWindow))); @@ -489,6 +496,24 @@ Result initialize() gPipelineState = pipelineState; + // Create render pass. + gfx::IRenderPassLayout::Desc renderPassDesc = {}; + renderPassDesc.framebufferLayout = framebufferLayout; + renderPassDesc.renderTargetCount = 1; + IRenderPassLayout::AttachmentAccessDesc renderTargetAccess = {}; + IRenderPassLayout::AttachmentAccessDesc depthStencilAccess = {}; + renderTargetAccess.loadOp = IRenderPassLayout::AttachmentLoadOp::Clear; + renderTargetAccess.storeOp = IRenderPassLayout::AttachmentStoreOp::Store; + renderTargetAccess.initialState = ResourceState::Undefined; + renderTargetAccess.finalState = ResourceState::Present; + depthStencilAccess.loadOp = IRenderPassLayout::AttachmentLoadOp::Clear; + depthStencilAccess.storeOp = IRenderPassLayout::AttachmentStoreOp::Store; + depthStencilAccess.initialState = ResourceState::Undefined; + depthStencilAccess.finalState = ResourceState::DepthWrite; + renderPassDesc.renderTargetAccess = &renderTargetAccess; + renderPassDesc.depthStencilAccess = &depthStencilAccess; + gRenderPass = gRenderer->createRenderPassLayout(renderPassDesc); + showWindow(gWindow); return SLANG_OK; @@ -506,26 +531,18 @@ uint64_t startTime = 0; void renderFrame() { - gRenderer->beginFrame(); auto frameIndex = gSwapchain->acquireNextImage(); - gRenderer->setFramebuffer(gFramebuffers[frameIndex]); + auto commandBuffer = gQueue->createCommandBuffer(); if( firstTime ) { startTime = getCurrentTime(); firstTime = false; } - 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(); + // Update uniform buffer. + auto uploadEncoder = commandBuffer->encodeResourceCommands(); - if(Uniforms* uniforms = (Uniforms*) gRenderer->map(gConstantBuffer, MapFlavor::WriteDiscard)) + Uniforms uniforms = {}; { bool isMouseClick = isMouseDown && !wasMouseDown; wasMouseDown = isMouseDown; @@ -536,35 +553,41 @@ void renderFrame() clickMouseY = lastMouseY; } - uniforms->iMouse[0] = lastMouseX; - uniforms->iMouse[1] = lastMouseY; - uniforms->iMouse[2] = isMouseDown ? clickMouseX : -clickMouseX; - uniforms->iMouse[3] = isMouseClick ? clickMouseY : -clickMouseY; - uniforms->iTime = float( double(getCurrentTime() - startTime) / double(getTimerFrequency()) ); - uniforms->iResolution[0] = float(gWindowWidth); - uniforms->iResolution[1] = float(gWindowHeight); + uniforms.iMouse[0] = lastMouseX; + uniforms.iMouse[1] = lastMouseY; + uniforms.iMouse[2] = isMouseDown ? clickMouseX : -clickMouseX; + uniforms.iMouse[3] = isMouseClick ? clickMouseY : -clickMouseY; + uniforms.iTime = float( double(getCurrentTime() - startTime) / double(getTimerFrequency()) ); + uniforms.iResolution[0] = float(gWindowWidth); + uniforms.iResolution[1] = float(gWindowHeight); - gRenderer->unmap(gConstantBuffer); + uploadEncoder->uploadBufferData(gConstantBuffer, 0, sizeof(Uniforms), &uniforms); } + uploadEncoder->endEncoding(); - gRenderer->setPipelineState(gPipelineState); - gRenderer->setDescriptorSet(PipelineType::Graphics, gPipelineLayout, 0, gDescriptorSet); - - gRenderer->setVertexBuffer(0, gVertexBuffer, sizeof(FullScreenTriangle::Vertex)); - gRenderer->setPrimitiveTopology(PrimitiveTopology::TriangleList); - - gRenderer->draw(3); - - gRenderer->makeSwapchainImagePresentable(gSwapchain); - - gRenderer->endFrame(); + // Encode render commands. + auto encoder = commandBuffer->encodeRenderCommands(gRenderPass, gFramebuffers[frameIndex]); + gfx::Viewport viewport = {}; + viewport.maxZ = 1.0f; + viewport.extentX = (float)gWindowWidth; + viewport.extentY = (float)gWindowHeight; + encoder->setViewportAndScissor(viewport); + encoder->setPipelineState(gPipelineState); + encoder->setDescriptorSet(gPipelineLayout, 0, gDescriptorSet); + encoder->setVertexBuffer(0, gVertexBuffer, sizeof(FullScreenTriangle::Vertex)); + encoder->setPrimitiveTopology(PrimitiveTopology::TriangleList); + encoder->draw(3); + encoder->endEncoding(); + commandBuffer->close(); + + gQueue->executeCommandBuffer(commandBuffer); gSwapchain->present(); } void finalize() { - gRenderer->waitForGpu(); + gQueue->wait(); destroyWindow(gWindow); } -- cgit v1.2.3