diff options
| author | Theresa Foley <10618364+tangent-vector@users.noreply.github.com> | 2025-01-27 18:31:04 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-27 18:31:04 -0800 |
| commit | 31bb5eaa094821145fe235f9a13817b0b3b2bdee (patch) | |
| tree | 8d25b49516dfd74deb80e113ce2af8180a106e08 /examples/reflection-parameter-blocks | |
| parent | f030a5ab62235152055fe8a616dd6d0d7ba1c275 (diff) | |
Add an example for reflection of parameter blocks (#6161)
* Add an example for reflection of parameter blocks
The example loads a shader program, compiles it for Vulkan, and then uses reflection information to set up descriptor set layouts and a pipeline layout.
It then validates that the pipeline layout that is created is compatible with the compiled code, by using them together to make a pipeline state object with the validation layer enabled.
The basic flow of the application follows the presentation in the companion article, and references its sections.
This is example could be expanded in a few ways:
* A D3D12 path could be added, to show the comparable operations for creating a root signature from reflection data
* The existing shader could be modified to touch/use more of its parameter data, to help ensure that any errors would be caught by the validation layer
* The set of shader files/modules that get loaded automatically could be expanded, to test more cases
* The code could be expanded to handle more than just compute shaders, by allowing for multiple-entry-point files to be loaded for rasterization or ray-tracing pipelines
* format code
* fixup: build errors
* format code
* fixups for build
* fixup
* fixup
---------
Co-authored-by: slangbot <186143334+slangbot@users.noreply.github.com>
Co-authored-by: Yong He <yonghe@outlook.com>
Diffstat (limited to 'examples/reflection-parameter-blocks')
| -rw-r--r-- | examples/reflection-parameter-blocks/README.md | 4 | ||||
| -rw-r--r-- | examples/reflection-parameter-blocks/common.slang | 113 | ||||
| -rw-r--r-- | examples/reflection-parameter-blocks/main.cpp | 694 | ||||
| -rw-r--r-- | examples/reflection-parameter-blocks/shader.slang | 26 | ||||
| -rw-r--r-- | examples/reflection-parameter-blocks/vulkan-api.cpp | 285 | ||||
| -rw-r--r-- | examples/reflection-parameter-blocks/vulkan-api.h | 128 |
6 files changed, 1250 insertions, 0 deletions
diff --git a/examples/reflection-parameter-blocks/README.md b/examples/reflection-parameter-blocks/README.md new file mode 100644 index 000000000..74f08cf0b --- /dev/null +++ b/examples/reflection-parameter-blocks/README.md @@ -0,0 +1,4 @@ +Setting up Parameter Blocks Using Reflection +============================================ + +This example shows how to make use of the `ParameterBlock<>` type in Slang shader code, in conjunction with the Slang reflection API, in order to easily set up descriptor sets for Vulkan and descriptor tables for D3D12. diff --git a/examples/reflection-parameter-blocks/common.slang b/examples/reflection-parameter-blocks/common.slang new file mode 100644 index 000000000..d8f812246 --- /dev/null +++ b/examples/reflection-parameter-blocks/common.slang @@ -0,0 +1,113 @@ +// shader.slang + +// This module is part of the `reflection-parameter-blocks` +// example program. +// +// This module is split out from the files that define +// individual programs, so that we can share some type +// definitions and utility functions between all of +// the programs and keep them focused on just defining +// the shader entry points. + +struct Mesh +{ + float4x4 modelToWorld; + float4x4 modelToWorld_inverseTranspose; +} + +struct Material +{ + Texture2D albedoMap; + Texture2D glossMap; + Texture2D normalMap; + SamplerState sampler; +} + +interface ILight +{ +} + +struct DirectionalLight : ILight +{ + float3 dir; + float3 intensity; +} + +struct ShadowedLight<L : ILight> : ILight +{ + L light; + Texture2D shadowMap; + SamplerComparisonState shadowSampler; + float4x4 worldToShadow; +} + +struct EnvironmentMap +{ + TextureCube texture; + SamplerState sampler; +} + +struct Environment +{ + ShadowedLight<DirectionalLight> sunLight; + EnvironmentMap envMap; + RWStructuredBuffer<float4> output; +} + +struct View +{ + float4x4 worldToView; + float4x4 viewToProj; +} + +// While the Slang compilation library will *reflect* all of +// the shader parameters that a program declares, +// back-ends (such as the SPIR-V code generator) will often +// strip out parameters that are not used as part of the +// computation that a shader performs. +// +// When shader parameters are stripped from the output +// binary code, the runtime system for a particular API +// (e.g., the Vulkan validation layer) cannot check +// whether a program is correctly handling the binding +// of those parameters. +// +// Our example entry points will thus make use of some +// utility routines that serve the purpose of allowing +// us to ensure that specific parameters are seen as +// "used" during code generation. + +void use(inout float4 r, float4 v) { r += v; } +void use(inout float4 r, float3 v) { r.xyz += v; } + +void use(inout float4 r, Texture2D t, SamplerState s) +{ + use(r, t.SampleLevel(s, r.xy, 0)); +} + +void use(inout float4 r, RWStructuredBuffer<float4> b) +{ + use(r, b[int(r.x)]); + b[int(r.x)] = r; +} + +void use(inout float4 r, Environment e) +{ + use(r, e.sunLight.light.dir); + use(r, e.output); +} + +void use(inout float4 r, View v) +{ + use(r, v.worldToView[0]); +} + +void use(inout float4 r, Material m) +{ + use(r, m.normalMap, m.sampler); +} + +void use(inout float4 r, Mesh m) +{ + use(r, m.modelToWorld[0]); +} diff --git a/examples/reflection-parameter-blocks/main.cpp b/examples/reflection-parameter-blocks/main.cpp new file mode 100644 index 000000000..cc20de281 --- /dev/null +++ b/examples/reflection-parameter-blocks/main.cpp @@ -0,0 +1,694 @@ +// main.cpp + +// Using Parameter Blocks With Reflection +// ====================================== +// +// This example program is a companion to the article +// Using Slang Parameter Blocks, and specifically +// the section of that article called Using Parameter +// Blocks With Reflection. +// +// Where possible, the code is presented in the +// same order as the code in the article, so that the +// two can be read in parallel. When code relates to +// a sub-section of the article, a comment will be used +// to reference the relevant section. +// +// Boilerplate +// =========== +// +// As is typical for our example programs, this one starts +// with a certain amount of boilerplate that isn't especially +// interesting to discuss. + +#include "slang-com-ptr.h" +#include "slang.h" +typedef SlangResult Result; + +#include "core/slang-basic.h" +#include "examples/example-base/example-base.h" +using Slang::ComPtr; +using Slang::String; +using Slang::List; + +// The example code currently only supports Vulkan, but the +// code is factored with the intention that it could be extended +// to support D3D12 as well. + +#define ENABLE_VULKAN 1 +#define ENABLE_D3D12 0 + +#if ENABLE_VULKAN +#include "vulkan-api.h" +#endif + +static const ExampleResources resourceBase("reflection-parameter-blocks"); +static const char* kSourceFileName = "shader.slang"; + +struct PipelineLayoutReflectionContext +{ + gfx::IDevice* _gfxDevice = nullptr; + slang::ISession* _slangSession = nullptr; + slang::ProgramLayout* _slangProgramLayout = nullptr; + slang::IBlob* _slangCompiledProgramBlob = nullptr; +}; + +struct PipelineLayoutReflectionContext_Vulkan : PipelineLayoutReflectionContext +{ + // What Goes Into a Pipeline Layout? + // ================================= + + struct PipelineLayoutBuilder + { + std::vector<VkDescriptorSetLayout> descriptorSetLayouts; + std::vector<VkPushConstantRange> pushConstantRanges; + }; + + // Unlike how things are presented in the document, we do not + // nest most of the functions under the `*Builder` types, in + // order to allow for more flexibility in the order of + // presentation. For example, instead of a + // `PipelineLayoutBuilder::finishBuilding()` method, we instead + // have a `finishBuildingPipelineLayout` function: + + Result finishBuildingPipelineLayout( + PipelineLayoutBuilder& builder, + VkPipelineLayout* outPipelineLayout) + { + filterOutEmptyDescriptorSets(builder); + + VkPipelineLayoutCreateInfo pipelineLayoutInfo = { + VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + + pipelineLayoutInfo.setLayoutCount = builder.descriptorSetLayouts.size(); + pipelineLayoutInfo.pSetLayouts = builder.descriptorSetLayouts.data(); + + pipelineLayoutInfo.pushConstantRangeCount = builder.pushConstantRanges.size(); + pipelineLayoutInfo.pPushConstantRanges = builder.pushConstantRanges.data(); + + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + vkAPI.vkCreatePipelineLayout(vkAPI.device, &pipelineLayoutInfo, nullptr, &pipelineLayout); + + *outPipelineLayout = pipelineLayout; + return SLANG_OK; + } + + // What Goes Into a Descriptor Set Layout? + // ======================================= + + struct DescriptorSetLayoutBuilder + { + std::vector<VkDescriptorSetLayoutBinding> descriptorRanges; + + int setIndex = -1; + }; + + + // Once we are done traversing the contents of a parameter + // block to collect bindings into a `DescriptorSetLayoutBuilder`, + // it is a simple matter to create a descriptor set layout using + // the Vulkan API, and to install it into the `setLayouts` array + // at the index that was reserved. + // + void finishBuildingDescriptorSetLayout( + PipelineLayoutBuilder& pipelineLayoutBuilder, + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder) + { + if (descriptorSetLayoutBuilder.descriptorRanges.empty()) + return; + + VkDescriptorSetLayoutCreateInfo descriptorSetLayoutInfo = { + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + + descriptorSetLayoutInfo.bindingCount = descriptorSetLayoutBuilder.descriptorRanges.size(); + descriptorSetLayoutInfo.pBindings = descriptorSetLayoutBuilder.descriptorRanges.data(); + + VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; + vkAPI.vkCreateDescriptorSetLayout( + vkAPI.device, + &descriptorSetLayoutInfo, + nullptr, + &descriptorSetLayout); + + pipelineLayoutBuilder.descriptorSetLayouts[descriptorSetLayoutBuilder.setIndex] = + descriptorSetLayout; + } + + // Parameter Blocks + // ================ + + void addDescriptorSetForParameterBlock( + PipelineLayoutBuilder& pipelineLayoutBuilder, + slang::TypeLayoutReflection* parameterBlockTypeLayout) + { + DescriptorSetLayoutBuilder descriptorSetLayoutBuilder; + startBuildingDescriptorSetLayout(pipelineLayoutBuilder, descriptorSetLayoutBuilder); + + addRangesForParameterBlockElement( + pipelineLayoutBuilder, + descriptorSetLayoutBuilder, + parameterBlockTypeLayout->getElementTypeLayout()); + + finishBuildingDescriptorSetLayout(pipelineLayoutBuilder, descriptorSetLayoutBuilder); + } + + // Automatically-Introduced Uniform Buffer + // --------------------------------------- + + void addRangesForParameterBlockElement( + PipelineLayoutBuilder& pipelineLayoutBuilder, + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder, + slang::TypeLayoutReflection* elementTypeLayout) + { + if (elementTypeLayout->getSize() > 0) + { + addAutomaticallyIntroducedUniformBuffer(descriptorSetLayoutBuilder); + } + + // Once we have accounted for the possibility of an implicitly-introduced + // constant buffer, we can move on and add bindings based on whatever + // non-ordinary data (textures, buffers, etc.) is in the element type: + // + addRanges(pipelineLayoutBuilder, descriptorSetLayoutBuilder, elementTypeLayout); + } + + void addAutomaticallyIntroducedUniformBuffer( + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder) + { + auto vulkanBindingIndex = descriptorSetLayoutBuilder.descriptorRanges.size(); + + VkDescriptorSetLayoutBinding binding = {}; + binding.stageFlags = VK_SHADER_STAGE_ALL; + binding.binding = vulkanBindingIndex; + binding.descriptorCount = 1; + binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + + descriptorSetLayoutBuilder.descriptorRanges.push_back(binding); + } + + // Ordering of Nested Parameter Blocks + // ----------------------------------- + + void startBuildingDescriptorSetLayout( + PipelineLayoutBuilder& pipelineLayoutBuilder, + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder) + { + descriptorSetLayoutBuilder.setIndex = pipelineLayoutBuilder.descriptorSetLayouts.size(); + pipelineLayoutBuilder.descriptorSetLayouts.push_back(VK_NULL_HANDLE); + } + + // Empty Ranges + // ------------ + + void filterOutEmptyDescriptorSets(PipelineLayoutBuilder& builder) + { + std::vector<VkDescriptorSetLayout> filteredDescriptorSetLayouts; + for (auto descriptorSetLayout : builder.descriptorSetLayouts) + { + if (!descriptorSetLayout) + continue; + filteredDescriptorSetLayouts.push_back(descriptorSetLayout); + } + std::swap(builder.descriptorSetLayouts, filteredDescriptorSetLayouts); + } + + // Descritpor Ranges + // ================= + + void addDescriptorRanges( + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder, + slang::TypeLayoutReflection* typeLayout) + { + int relativeSetIndex = 0; + int rangeCount = typeLayout->getDescriptorSetDescriptorRangeCount(relativeSetIndex); + + for (int rangeIndex = 0; rangeIndex < rangeCount; ++rangeIndex) + { + addDescriptorRange( + descriptorSetLayoutBuilder, + typeLayout, + relativeSetIndex, + rangeIndex); + } + } + + void addDescriptorRange( + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder, + slang::TypeLayoutReflection* typeLayout, + int relativeSetIndex, + int rangeIndex) + { + slang::BindingType bindingType = + typeLayout->getDescriptorSetDescriptorRangeType(relativeSetIndex, rangeIndex); + auto descriptorCount = typeLayout->getDescriptorSetDescriptorRangeDescriptorCount( + relativeSetIndex, + rangeIndex); + + // Some Ranges Need to Be Skipped + // ------------------------------ + // + switch (bindingType) + { + default: + break; + + case slang::BindingType::PushConstant: + return; + } + + auto bindingIndex = descriptorSetLayoutBuilder.descriptorRanges.size(); + + VkDescriptorSetLayoutBinding vulkanBindingRange = {}; + vulkanBindingRange.binding = bindingIndex; + vulkanBindingRange.descriptorCount = descriptorCount; + vulkanBindingRange.stageFlags = _currentStageFlags; + vulkanBindingRange.descriptorType = mapSlangBindingTypeToVulkanDescriptorType(bindingType); + + descriptorSetLayoutBuilder.descriptorRanges.push_back(vulkanBindingRange); + } + + VkDescriptorType mapSlangBindingTypeToVulkanDescriptorType(slang::BindingType bindingType) + { + switch (bindingType) + { +#define CASE(FROM, TO) \ + case slang::BindingType::FROM: \ + return VK_DESCRIPTOR_TYPE_##TO + + CASE(Sampler, SAMPLER); + CASE(CombinedTextureSampler, COMBINED_IMAGE_SAMPLER); + CASE(Texture, SAMPLED_IMAGE); + CASE(MutableTexture, STORAGE_IMAGE); + CASE(TypedBuffer, UNIFORM_TEXEL_BUFFER); + CASE(MutableTypedBuffer, STORAGE_TEXEL_BUFFER); + CASE(ConstantBuffer, UNIFORM_BUFFER); + CASE(RawBuffer, STORAGE_BUFFER); + CASE(MutableRawBuffer, STORAGE_BUFFER); + CASE(InputRenderTarget, INPUT_ATTACHMENT); + CASE(InlineUniformData, INLINE_UNIFORM_BLOCK); + CASE(RayTracingAccelerationStructure, ACCELERATION_STRUCTURE_KHR); + +#undef CASE + + default: + return VkDescriptorType(-1); + } + } + + // Sub-Object Ranges + // ================= + + void addRanges( + PipelineLayoutBuilder& pipelineLayoutBuilder, + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder, + slang::TypeLayoutReflection* typeLayout) + { + addDescriptorRanges(descriptorSetLayoutBuilder, typeLayout); + addSubObjectRanges(pipelineLayoutBuilder, typeLayout); + } + + void addSubObjectRanges( + PipelineLayoutBuilder& pipelineLayoutBuilder, + slang::TypeLayoutReflection* typeLayout) + { + int subObjectRangeCount = typeLayout->getSubObjectRangeCount(); + for (int subObjectRangeIndex = 0; subObjectRangeIndex < subObjectRangeCount; + ++subObjectRangeIndex) + { + addSubObjectRange(pipelineLayoutBuilder, typeLayout, subObjectRangeIndex); + } + } + + void addSubObjectRange( + PipelineLayoutBuilder& pipelineLayoutBuilder, + slang::TypeLayoutReflection* typeLayout, + int subObjectRangeIndex) + { + auto bindingRangeIndex = + typeLayout->getSubObjectRangeBindingRangeIndex(subObjectRangeIndex); + auto bindingType = typeLayout->getBindingRangeType(bindingRangeIndex); + switch (bindingType) + { + default: + return; + + // Nested Parameter Blocks + // ----------------------- + + case slang::BindingType::ParameterBlock: + { + auto parameterBlockTypeLayout = + typeLayout->getBindingRangeLeafTypeLayout(bindingRangeIndex); + addDescriptorSetForParameterBlock(pipelineLayoutBuilder, parameterBlockTypeLayout); + } + break; + + // Push-Constant Ranges + // -------------------- + + case slang::BindingType::PushConstant: + { + auto constantBufferTypeLayout = + typeLayout->getBindingRangeLeafTypeLayout(bindingRangeIndex); + addPushConstantRangeForConstantBuffer( + pipelineLayoutBuilder, + constantBufferTypeLayout); + } + break; + } + } + + void addPushConstantRangeForConstantBuffer( + PipelineLayoutBuilder& pipelineLayoutBuilder, + slang::TypeLayoutReflection* pushConstantBufferTypeLayout) + { + auto elementTypeLayout = pushConstantBufferTypeLayout->getElementTypeLayout(); + auto elementSize = elementTypeLayout->getSize(); + + if (elementSize == 0) + return; + + VkPushConstantRange pushConstantRange = {}; + pushConstantRange.stageFlags = _currentStageFlags; + pushConstantRange.offset = 0; + pushConstantRange.size = elementSize; + + pipelineLayoutBuilder.pushConstantRanges.push_back(pushConstantRange); + } + + // Creating a Pipeline Layout for a Program + // ======================================== + + Result createPipelineLayout( + slang::ProgramLayout* programLayout, + VkPipelineLayout* outPipelineLayout) + { + PipelineLayoutBuilder pipelineLayoutBuilder; + + DescriptorSetLayoutBuilder defaultDescriptorSetLayoutBuilder; + startBuildingDescriptorSetLayout(pipelineLayoutBuilder, defaultDescriptorSetLayoutBuilder); + + addGlobalScopeParameters( + pipelineLayoutBuilder, + defaultDescriptorSetLayoutBuilder, + programLayout); + + addEntryPointParameters( + pipelineLayoutBuilder, + defaultDescriptorSetLayoutBuilder, + programLayout); + + finishBuildingDescriptorSetLayout(pipelineLayoutBuilder, defaultDescriptorSetLayoutBuilder); + finishBuildingPipelineLayout(pipelineLayoutBuilder, outPipelineLayout); + + return SLANG_OK; + } + + // Global Scope + // ------------ + + void addGlobalScopeParameters( + PipelineLayoutBuilder& pipelineLayoutBuilder, + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder, + slang::ProgramLayout* programLayout) + { + _currentStageFlags = VK_SHADER_STAGE_ALL; + addRangesForParameterBlockElement( + pipelineLayoutBuilder, + descriptorSetLayoutBuilder, + programLayout->getGlobalParamsTypeLayout()); + } + + // Entry Points + // ------------ + + void addEntryPointParameters( + PipelineLayoutBuilder& pipelineLayoutBuilder, + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder, + slang::ProgramLayout* programLayout) + { + int entryPointCount = _slangProgramLayout->getEntryPointCount(); + for (int i = 0; i < entryPointCount; ++i) + { + auto entryPointLayout = _slangProgramLayout->getEntryPointByIndex(i); + addEntryPointParameters( + pipelineLayoutBuilder, + descriptorSetLayoutBuilder, + entryPointLayout); + } + } + + void addEntryPointParameters( + PipelineLayoutBuilder& pipelineLayoutBuilder, + DescriptorSetLayoutBuilder& descriptorSetLayoutBuilder, + slang::EntryPointLayout* entryPointLayout) + { + _currentStageFlags = getShaderStageFlags(entryPointLayout->getStage()); + addRangesForParameterBlockElement( + pipelineLayoutBuilder, + descriptorSetLayoutBuilder, + entryPointLayout->getTypeLayout()); + } + + VkShaderStageFlags _currentStageFlags = VK_SHADER_STAGE_ALL; + VkShaderStageFlags getShaderStageFlags(SlangStage stage) + { + switch (stage) + { +#define CASE(FROM, TO) \ + case SLANG_STAGE_##FROM: \ + return VK_SHADER_STAGE_##TO + + CASE(VERTEX, VERTEX_BIT); + CASE(HULL, TESSELLATION_CONTROL_BIT); + CASE(DOMAIN, TESSELLATION_EVALUATION_BIT); + CASE(GEOMETRY, GEOMETRY_BIT); + CASE(FRAGMENT, FRAGMENT_BIT); + CASE(COMPUTE, COMPUTE_BIT); + CASE(RAY_GENERATION, RAYGEN_BIT_KHR); + CASE(ANY_HIT, ANY_HIT_BIT_KHR); + CASE(CLOSEST_HIT, CLOSEST_HIT_BIT_KHR); + CASE(MISS, MISS_BIT_KHR); + CASE(INTERSECTION, INTERSECTION_BIT_KHR); + CASE(CALLABLE, CALLABLE_BIT_KHR); + CASE(MESH, MESH_BIT_EXT); + CASE(AMPLIFICATION, TASK_BIT_EXT); + +#undef CASE + default: + return VK_SHADER_STAGE_ALL; + } + } + + // Validation + // ========== + // + // The published article covers how to create a pipeline layout + // using the reflection API, but for the purposes of an example + // program, we should make sure that we validate that the layout + // that results from that code is *actually* compatible with the + // shader program. + // + // The remaining operations inside this type provide the support + // code to create and validate a pipeline layout based on a + // particular compiled compute program. Mismatches between the + // pipeline layout and the program should be diagnosed by the + // Vulkan validation layer when we attempt to create a pipeline + // that uses the two together. + + Result validatePipelineLayout(VkPipelineLayout pipelineLayout) + { + VkShaderModuleCreateInfo shaderModuleInfo = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + shaderModuleInfo.pCode = (uint32_t const*)_slangCompiledProgramBlob->getBufferPointer(); + shaderModuleInfo.codeSize = _slangCompiledProgramBlob->getBufferSize(); + + VkShaderModule vkShaderModule; + vkAPI.vkCreateShaderModule(vkAPI.device, &shaderModuleInfo, nullptr, &vkShaderModule); + + VkComputePipelineCreateInfo pipelineInfo = {VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.layout = pipelineLayout; + pipelineInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + pipelineInfo.stage.module = vkShaderModule; + pipelineInfo.stage.pName = "main"; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + + VkPipeline pipeline; + vkAPI.vkCreateComputePipelines( + vkAPI.device, + VK_NULL_HANDLE, + 1, + &pipelineInfo, + nullptr, + &pipeline); + + vkAPI.vkDestroyPipeline(vkAPI.device, pipeline, nullptr); + + return SLANG_OK; + } + + Result createAndValidatePipelineLayout() + { + // Here we do a little bit of complicated interaction with + // the `gfx` library to allow us to call raw Vulkan API + // functions on the same device that `gfx` kindly set up + // for us. + // + gfx::IDevice::InteropHandles handles; + SLANG_RETURN_ON_FAIL(_gfxDevice->getNativeDeviceHandles(&handles)); + + vkAPI.instance = (VkInstance)handles.handles[0].handleValue; + vkAPI.physicalDevice = (VkPhysicalDevice)handles.handles[1].handleValue; + vkAPI.device = (VkDevice)handles.handles[2].handleValue; + + vkAPI.initGlobalProcs(); + vkAPI.initInstanceProcs(); + vkAPI.initDeviceProcs(); + + // Once the setup is dealt with, we can go ahead and + // create the pipeline layout, before validating that + // it can be used together with the compiled SPIR-V + // binary for the program. + // + VkPipelineLayout pipelineLayout; + SLANG_RETURN_ON_FAIL(createPipelineLayout(_slangProgramLayout, &pipelineLayout)); + SLANG_RETURN_ON_FAIL(validatePipelineLayout(pipelineLayout)); + + vkAPI.vkDestroyPipelineLayout(vkAPI.device, pipelineLayout, nullptr); + + return SLANG_OK; + } + + VulkanAPI vkAPI; +}; + +// More Boilerplate +// ================ +// +// The logic below this point is just about setting up the necessary state +// in the example application for the code above to be run on a simple +// shader. Nothing here is especially relevant to the task of creating +// a pipeline layout from Slang reflection information. + +struct ReflectionParameterBlocksExampleApp : public TestBase +{ + Result execute(int argc, char** argv) + { + parseOption(argc, argv); + + // We start by initializing the `gfx` system, so that + // it can handle most of the details of getting a + // Vulkan device up and running. + +#ifdef _DEBUG + gfx::gfxEnableDebugLayer(); +#endif + gfx::IDevice::Desc deviceDesc = {}; + deviceDesc.deviceType = gfx::DeviceType::Vulkan; + + ComPtr<gfx::IDevice> gfxDevice; + SLANG_RETURN_ON_FAIL(gfxCreateDevice(&deviceDesc, gfxDevice.writeRef())); + + // The `gfx` library also creates a Slang session as + // part of its startup, so we will use the session + // it already created for the compilation in + // this example. + // + auto slangSession = gfxDevice->getSlangSession(); + + // Next we go through the fairly routine steps needed to + // compile a Slang program from source. + // + ComPtr<slang::IBlob> diagnostics; + Result result = SLANG_OK; + + // We load the source file as a module of Slang code. + // + String sourceFilePath = resourceBase.resolveResource(kSourceFileName); + ComPtr<slang::IModule> module; + module = slangSession->loadModule(sourceFilePath.getBuffer(), diagnostics.writeRef()); + diagnoseIfNeeded(diagnostics); + if (!module) + return SLANG_FAIL; + + // Next we will collect all of the entry points defined in the module, + // to form the list of components we want to link together to form + // a program. + // + List<ComPtr<slang::IComponentType>> componentsToLink; + int definedEntryPointCount = module->getDefinedEntryPointCount(); + for (int i = 0; i < definedEntryPointCount; i++) + { + ComPtr<slang::IEntryPoint> entryPoint; + SLANG_RETURN_ON_FAIL(module->getDefinedEntryPoint(i, entryPoint.writeRef())); + componentsToLink.add(ComPtr<slang::IComponentType>(entryPoint.get())); + } + + // Once we've collected the list of entry points we want to compose, + // we use the Slang compilation API to compose them. + // + ComPtr<slang::IComponentType> composed; + result = slangSession->createCompositeComponentType( + (slang::IComponentType**)componentsToLink.getBuffer(), + componentsToLink.getCount(), + composed.writeRef(), + diagnostics.writeRef()); + diagnoseIfNeeded(diagnostics); + SLANG_RETURN_ON_FAIL(result); + + // As the final compilation step, we will use the compilation API + // to link the composed code. Think of this as equivalent to + // applying the linker to a bunch of `.o` and/or `.a` files to + // produce a binary (executable or shared library). + // + ComPtr<slang::IComponentType> program; + result = composed->link(program.writeRef(), diagnostics.writeRef()); + diagnoseIfNeeded(diagnostics); + SLANG_RETURN_ON_FAIL(result); + + // Once the program has been compiled succcessfully, we can + // go ahead and grab reflection data from the program. + // + int targetIndex = 0; + slang::ProgramLayout* programLayout = + program->getLayout(targetIndex, diagnostics.writeRef()); + diagnoseIfNeeded(diagnostics); + if (!programLayout) + { + return SLANG_FAIL; + } + + // The compiled program can also have binary code (either + // for individual entry points, or the entire program) + // generated for it. + // + ComPtr<slang::IBlob> programBinary; + result = program->getEntryPointCode(0, 0, programBinary.writeRef(), diagnostics.writeRef()); + diagnoseIfNeeded(diagnostics); + if (SLANG_FAILED(result)) + return result; + + // Finally, once all of the initialization work is dealt with, + // we hand control over to the actual logic of the example. + // + PipelineLayoutReflectionContext_Vulkan context; + + context._gfxDevice = gfxDevice; + context._slangSession = slangSession; + context._slangProgramLayout = programLayout; + context._slangCompiledProgramBlob = programBinary; + + SLANG_RETURN_ON_FAIL(context.createAndValidatePipelineLayout()); + + return SLANG_OK; + } +}; + +int main(int argc, char* argv[]) +{ + ReflectionParameterBlocksExampleApp app; + if (SLANG_FAILED(app.execute(argc, argv))) + { + return -1; + } + return 0; +} diff --git a/examples/reflection-parameter-blocks/shader.slang b/examples/reflection-parameter-blocks/shader.slang new file mode 100644 index 000000000..6419bda3a --- /dev/null +++ b/examples/reflection-parameter-blocks/shader.slang @@ -0,0 +1,26 @@ +// shader.slang + +// This shader is part of the `reflection-parameter-blocks` +// example program. +// +// This file is an example program that *only* declares +// shader parameters in the global scope, and *only* uses +// explicit parameter blocks. + +import common; + +ParameterBlock<Environment> environment; +ParameterBlock<View> view; +ParameterBlock<Material> material; +ParameterBlock<Mesh> mesh; + +[shader("compute")] +void main() +{ + float4 r = 0; + + use(r, mesh); + use(r, material); + use(r, view); + use(r, environment); +} diff --git a/examples/reflection-parameter-blocks/vulkan-api.cpp b/examples/reflection-parameter-blocks/vulkan-api.cpp new file mode 100644 index 000000000..7fad578b0 --- /dev/null +++ b/examples/reflection-parameter-blocks/vulkan-api.cpp @@ -0,0 +1,285 @@ +#include "vulkan-api.h" + +#include "slang.h" + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <vector> + +#if SLANG_WINDOWS_FAMILY +#include <windows.h> +#else +#include <dlfcn.h> +#endif + +#if _DEBUG +#define ENABLE_VALIDATION_LAYER 1 +#endif + +VKAPI_ATTR VkBool32 VKAPI_CALL debugMessageCallback( + VkDebugReportFlagsEXT /*flags*/, + VkDebugReportObjectTypeEXT /*objType*/, + uint64_t /*srcObject*/, + size_t /*location*/, + int32_t /*msgCode*/, + const char* pLayerPrefix, + const char* pMsg, + void* /*pUserData*/ +) +{ + printf("[%s]: %s\n", pLayerPrefix, pMsg); + return 1; +} + +gfx::Result VulkanAPI::initGlobalProcs() +{ + // Load vulkan library. + const char* dynamicLibraryName = "Unknown"; + +#if SLANG_WINDOWS_FAMILY + dynamicLibraryName = "vulkan-1.dll"; + HMODULE module = ::LoadLibraryA(dynamicLibraryName); +#define VK_API_GET_GLOBAL_PROC(x) this->x = (PFN_##x)GetProcAddress(module, #x); +#elif SLANG_APPLE_FAMILY + dynamicLibraryName = "libvulkan.dylib"; + void* vulkanLibraryHandle = dlopen(dynamicLibraryName, RTLD_NOW); +#define VK_API_GET_GLOBAL_PROC(x) this->x = (PFN_##x)dlsym(vulkanLibraryHandle, #x); +#else + dynamicLibraryName = "libvulkan.so.1"; + void* vulkanLibraryHandle = dlopen(dynamicLibraryName, RTLD_NOW); +#define VK_API_GET_GLOBAL_PROC(x) this->x = (PFN_##x)dlsym(vulkanLibraryHandle, #x); +#endif + + // Initialize all the global functions. + VK_API_ALL_GLOBAL_PROCS(VK_API_GET_GLOBAL_PROC) + if (!this->vkCreateInstance) + return -1; + + return 0; +} + +gfx::Result initializeVulkanDevice(VulkanAPI& api) +{ + if (api.initGlobalProcs() != 0) + return -1; + + // Enable validation layer if available. + std::vector<const char*> layers; +#ifdef ENABLE_VALIDATION_LAYER + uint32_t propertyCount; + if (api.vkEnumerateInstanceLayerProperties(&propertyCount, nullptr) != 0) + return -1; + std::vector<VkLayerProperties> properties(propertyCount); + if (api.vkEnumerateInstanceLayerProperties(&propertyCount, properties.data()) != 0) + return -1; + for (const auto& p : properties) + { + if (strcmp(p.layerName, "VK_LAYER_KHRONOS_validation") == 0) + { + layers.push_back("VK_LAYER_KHRONOS_validation"); + } + } +#endif + + // Create Vulkan Instance. + VkApplicationInfo applicationInfo = {VK_STRUCTURE_TYPE_APPLICATION_INFO}; + applicationInfo.pApplicationName = "slang-hello-world"; + applicationInfo.pEngineName = "slang-hello-world"; + applicationInfo.apiVersion = VK_API_VERSION_1_2; + applicationInfo.engineVersion = 1; + applicationInfo.applicationVersion = 1; + const char* instanceExtensions[] = { +#if SLANG_APPLE_FAMILY + VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, +#endif + VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, + VK_EXT_DEBUG_REPORT_EXTENSION_NAME, + }; + VkInstanceCreateInfo instanceCreateInfo = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; +#if SLANG_APPLE_FAMILY + instanceCreateInfo.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +#endif + instanceCreateInfo.pApplicationInfo = &applicationInfo; + instanceCreateInfo.enabledExtensionCount = SLANG_COUNT_OF(instanceExtensions); + instanceCreateInfo.ppEnabledExtensionNames = &instanceExtensions[0]; + if (layers.size()) + { + instanceCreateInfo.ppEnabledLayerNames = &layers[0]; + instanceCreateInfo.enabledLayerCount = (uint32_t)layers.size(); + } + if (api.vkCreateInstance(&instanceCreateInfo, nullptr, &api.instance) != 0) + return -1; + + // Load instance functions. + api.initInstanceProcs(); + +#if 0 + // Create debug report callback. + if (api.vkCreateDebugReportCallbackEXT) + { + VkDebugReportFlagsEXT debugFlags = + VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT; + + VkDebugReportCallbackCreateInfoEXT debugCreateInfo = { + VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT}; + debugCreateInfo.pfnCallback = &debugMessageCallback; + debugCreateInfo.pUserData = nullptr; + debugCreateInfo.flags = debugFlags; + + RETURN_ON_FAIL(api.vkCreateDebugReportCallbackEXT( + api.instance, + &debugCreateInfo, + nullptr, + &api.debugReportCallback)); + } +#endif + + // Enumerate physical devices. + uint32_t numPhysicalDevices = 0; + RETURN_ON_FAIL(api.vkEnumeratePhysicalDevices(api.instance, &numPhysicalDevices, nullptr)); + std::vector<VkPhysicalDevice> physicalDevices; + physicalDevices.resize(numPhysicalDevices); + RETURN_ON_FAIL( + api.vkEnumeratePhysicalDevices(api.instance, &numPhysicalDevices, &physicalDevices[0])); + +#if 0 + // We will use device 0. + api.initPhysicalDevice(physicalDevices[0]); + + VkDeviceCreateInfo deviceCreateInfo = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + deviceCreateInfo.queueCreateInfoCount = 1; + deviceCreateInfo.pEnabledFeatures = &api.deviceFeatures; + + // Find proper queue family index. + uint32_t numQueueFamilies = 0; + api.vkGetPhysicalDeviceQueueFamilyProperties(api.physicalDevice, &numQueueFamilies, nullptr); + + std::vector<VkQueueFamilyProperties> queueFamilies; + queueFamilies.resize(numQueueFamilies); + api.vkGetPhysicalDeviceQueueFamilyProperties( + api.physicalDevice, + &numQueueFamilies, + &queueFamilies[0]); + + // Find a queue that can service our needs. + auto requiredQueueFlags = VK_QUEUE_COMPUTE_BIT; + for (int i = 0; i < int(numQueueFamilies); ++i) + { + if ((queueFamilies[i].queueFlags & requiredQueueFlags) == requiredQueueFlags) + { + api.queueFamilyIndex = i; + break; + } + } + if (api.queueFamilyIndex == -1) + return -1; + +#if SLANG_APPLE_FAMILY + const char* deviceExtensions[] = { + "VK_KHR_portability_subset", + }; +#endif + + VkDeviceQueueCreateInfo queueCreateInfo = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + float queuePriority = 0.0f; + queueCreateInfo.queueFamilyIndex = api.queueFamilyIndex; + queueCreateInfo.queueCount = 1; + queueCreateInfo.pQueuePriorities = &queuePriority; + deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo; +#if SLANG_APPLE_FAMILY + deviceCreateInfo.enabledExtensionCount = SLANG_COUNT_OF(deviceExtensions); + deviceCreateInfo.ppEnabledExtensionNames = &deviceExtensions[0]; +#endif + RETURN_ON_FAIL(api.vkCreateDevice(api.physicalDevice, &deviceCreateInfo, nullptr, &api.device)); + +#endif + + // Load device functions. + api.initDeviceProcs(); + + return 0; +} + +gfx::Result VulkanAPI::initInstanceProcs() +{ + assert(instance && vkGetInstanceProcAddr != nullptr); + +#define VK_API_GET_INSTANCE_PROC(x) x = (PFN_##x)vkGetInstanceProcAddr(instance, #x); + + VK_API_ALL_INSTANCE_PROCS(VK_API_GET_INSTANCE_PROC) + // Get optional + VK_API_INSTANCE_PROCS_OPT(VK_API_GET_INSTANCE_PROC) + +#undef VK_API_GET_INSTANCE_PROC + + return 0; +} + +#if 0 +int VulkanAPI::initPhysicalDevice(VkPhysicalDevice inPhysicalDevice) +{ + assert(physicalDevice == VK_NULL_HANDLE); + physicalDevice = inPhysicalDevice; + + vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties); + vkGetPhysicalDeviceFeatures(physicalDevice, &deviceFeatures); + vkGetPhysicalDeviceMemoryProperties(physicalDevice, &deviceMemoryProperties); + + return 0; +} +#endif + +gfx::Result VulkanAPI::initDeviceProcs() +{ + assert(instance && device && vkGetDeviceProcAddr != nullptr); + +#define VK_API_GET_DEVICE_PROC(x) x = (PFN_##x)vkGetDeviceProcAddr(device, #x); + VK_API_DEVICE_PROCS(VK_API_GET_DEVICE_PROC) +#undef VK_API_GET_DEVICE_PROC + + return 0; +} + +#if 0 +int VulkanAPI::findMemoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags properties) +{ + assert(typeBits); + + const int numMemoryTypes = int(deviceMemoryProperties.memoryTypeCount); + + // bit holds current test bit against typeBits. Ie bit == 1 << typeBits + + uint32_t bit = 1; + for (int i = 0; i < numMemoryTypes; ++i, bit += bit) + { + auto const& memoryType = deviceMemoryProperties.memoryTypes[i]; + if ((typeBits & bit) && (memoryType.propertyFlags & properties) == properties) + { + return i; + } + } + + // assert(!"failed to find a usable memory type"); + return -1; +} +#endif + +VulkanAPI::~VulkanAPI() +{ +#if 0 + if (vkDestroyDevice) + { + vkDestroyDevice(device, nullptr); + } + if (debugReportCallback && vkDestroyDebugReportCallbackEXT) + { + vkDestroyDebugReportCallbackEXT(instance, debugReportCallback, nullptr); + } + if (vkDestroyInstance) + { + vkDestroyInstance(instance, nullptr); + } +#endif +} diff --git a/examples/reflection-parameter-blocks/vulkan-api.h b/examples/reflection-parameter-blocks/vulkan-api.h new file mode 100644 index 000000000..05a84db5d --- /dev/null +++ b/examples/reflection-parameter-blocks/vulkan-api.h @@ -0,0 +1,128 @@ +#pragma once + +#include "slang-gfx.h" + +#define VK_NO_PROTOTYPES +#include "vulkan/vulkan.h" + +// This file provides basic loading and helper functions for using +// the Vulkan API. + +// The Vulkan function pointers we will use in this example. +// clang-format off +#define VK_API_GLOBAL_PROCS(x) \ + x(vkGetInstanceProcAddr) \ + x(vkCreateInstance) \ + x(vkEnumerateInstanceLayerProperties) \ + x(vkDestroyInstance) \ + /* */ + +#define VK_API_INSTANCE_PROCS_OPT(x) \ + x(vkGetPhysicalDeviceFeatures2) \ + x(vkGetPhysicalDeviceProperties2) \ + x(vkCreateDebugReportCallbackEXT) \ + x(vkDestroyDebugReportCallbackEXT) \ + x(vkDebugReportMessageEXT) \ + /* */ + +#define VK_API_INSTANCE_PROCS(x) \ + x(vkCreateDevice) \ + x(vkDestroyDevice) \ + x(vkEnumeratePhysicalDevices) \ + x(vkGetPhysicalDeviceProperties) \ + x(vkGetPhysicalDeviceFeatures) \ + x(vkGetPhysicalDeviceMemoryProperties) \ + x(vkGetPhysicalDeviceQueueFamilyProperties) \ + x(vkGetPhysicalDeviceFormatProperties) \ + x(vkGetDeviceProcAddr) \ + /* */ + +#define VK_API_DEVICE_PROCS(x) \ + x(vkCreateDescriptorPool) \ + x(vkDestroyDescriptorPool) \ + x(vkGetDeviceQueue) \ + x(vkQueueSubmit) \ + x(vkQueueWaitIdle) \ + x(vkCreateBuffer) \ + x(vkAllocateMemory) \ + x(vkMapMemory) \ + x(vkUnmapMemory) \ + x(vkCmdCopyBuffer) \ + x(vkDestroyBuffer) \ + x(vkFreeMemory) \ + x(vkCreateDescriptorSetLayout) \ + x(vkDestroyDescriptorSetLayout) \ + x(vkAllocateDescriptorSets) \ + x(vkUpdateDescriptorSets) \ + x(vkCreatePipelineLayout) \ + x(vkDestroyPipelineLayout) \ + x(vkCreateComputePipelines) \ + x(vkDestroyPipeline) \ + x(vkCreateShaderModule) \ + x(vkDestroyShaderModule) \ + x(vkCreateCommandPool) \ + x(vkDestroyCommandPool) \ + \ + x(vkGetBufferMemoryRequirements) \ + \ + x(vkCmdBindPipeline) \ + x(vkCmdBindDescriptorSets) \ + x(vkCmdDispatch) \ + \ + x(vkFreeCommandBuffers) \ + x(vkAllocateCommandBuffers) \ + x(vkBeginCommandBuffer) \ + x(vkEndCommandBuffer) \ + x(vkBindBufferMemory) \ + /* */ + +#define VK_API_ALL_GLOBAL_PROCS(x) \ + VK_API_GLOBAL_PROCS(x) + +#define VK_API_ALL_INSTANCE_PROCS(x) \ + VK_API_INSTANCE_PROCS(x) \ + +#define VK_API_ALL_PROCS(x) \ + VK_API_ALL_GLOBAL_PROCS(x) \ + VK_API_ALL_INSTANCE_PROCS(x) \ + VK_API_DEVICE_PROCS(x) \ + VK_API_INSTANCE_PROCS_OPT(x) \ + /* */ + +#define VK_API_DECLARE_PROC(NAME) PFN_##NAME NAME = nullptr; +// clang-format on + +struct VulkanAPI +{ + gfx::Result initFromGFX(gfx::IDevice* gfxDevice); + + VkInstance instance = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; + + VK_API_ALL_PROCS(VK_API_DECLARE_PROC) + + gfx::Result initGlobalProcs(); + + /// Initialize the instance functions + gfx::Result initInstanceProcs(); + + /// Initialize the device functions + gfx::Result initDeviceProcs(); + + /// Clean up + ~VulkanAPI(); +}; + +#define RETURN_ON_FAIL(x) \ + { \ + auto _res = x; \ + if (_res != 0) \ + { \ + return -1; \ + } \ + } + +// Loads Vulkan library and creates a VkDevice. +// Returns 0 if successful. +int initializeVulkanDevice(VulkanAPI& api); |
