From 8bbed51556ddef6f6056a0e3d3de9c0f9ca580ab Mon Sep 17 00:00:00 2001 From: Thomas Choquet Date: Wed, 6 May 2026 16:08:23 +0900 Subject: [PATCH 1/2] add suport for descriptor indexing --- CMakeLists.txt | 2 +- examples/CMakeLists.txt | 3 + examples/descriptor_indexing/CMakeLists.txt | 130 ++++++ .../descriptor_indexing.cpp | 416 ++++++++++++++++++ .../descriptor_indexing.def | 3 + examples/descriptor_indexing/shader.h | 49 +++ examples/descriptor_indexing/shader.slang | 65 +++ include/Graphics/ParameterBlock.hpp | 8 + include/Graphics/ParameterBlockLayout.hpp | 2 + include/Graphics/ParameterBlockPool.hpp | 1 + src/Metal/MetalCommandBuffer.mm | 26 +- src/Metal/MetalParameterBlock.hpp | 45 +- src/Metal/MetalParameterBlock.mm | 106 ++++- src/Metal/MetalParameterBlockPool.mm | 7 +- src/Vulkan/VulkanDevice.cpp | 9 +- src/Vulkan/VulkanParameterBlock.cpp | 93 +++- src/Vulkan/VulkanParameterBlock.hpp | 31 +- src/Vulkan/VulkanParameterBlockLayout.cpp | 32 +- src/Vulkan/VulkanParameterBlockPool.cpp | 3 + tests/test_descriptor_operator.cpp | 15 +- 20 files changed, 981 insertions(+), 65 deletions(-) create mode 100644 examples/descriptor_indexing/CMakeLists.txt create mode 100644 examples/descriptor_indexing/descriptor_indexing.cpp create mode 100644 examples/descriptor_indexing/descriptor_indexing.def create mode 100644 examples/descriptor_indexing/shader.h create mode 100644 examples/descriptor_indexing/shader.slang diff --git a/CMakeLists.txt b/CMakeLists.txt index 0280038f..55302caa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ option(GFX_BUILD_TESTS "Build the tests" OFF) option(GFX_INSTALL "Enable the install command" ON) if(GFX_BUILD_EXAMPLES) - set(GFX_EXAMPLES_TO_BUILD "triangle;multiBuffer;imgui_usage;mc_cube;scop" CACHE STRING "Semicolon separated list of example names to build") + set(GFX_EXAMPLES_TO_BUILD "triangle;multiBuffer;descriptor_indexing;imgui_usage;mc_cube;scop" CACHE STRING "Semicolon separated list of example names to build") endif() enable_language(CXX) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 8876e3af..18804850 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -12,6 +12,9 @@ if (GFX_ENABLE_GLFW) if ("multiBuffer" IN_LIST GFX_EXAMPLES_TO_BUILD) add_subdirectory(multiBuffer) endif() + if ("descriptor_indexing" IN_LIST GFX_EXAMPLES_TO_BUILD) + add_subdirectory(descriptor_indexing) + endif() if (GFX_ENABLE_IMGUI) if ("imgui_usage" IN_LIST GFX_EXAMPLES_TO_BUILD) add_subdirectory(imgui_usage) diff --git a/examples/descriptor_indexing/CMakeLists.txt b/examples/descriptor_indexing/CMakeLists.txt new file mode 100644 index 00000000..6be04b9b --- /dev/null +++ b/examples/descriptor_indexing/CMakeLists.txt @@ -0,0 +1,130 @@ +# --------------------------------------------------- +# CMakeLists.txt +# +# Author: Thomas Choquet +# --------------------------------------------------- + +include(FetchContent) + +## GLFW ########################################################## + +FetchContent_Declare(glfw3 + GIT_REPOSITORY https://github.com/glfw/glfw.git + GIT_TAG 3.4 + GIT_SHALLOW 1 + GIT_PROGRESS TRUE + FIND_PACKAGE_ARGS +) +set(GLFW_BUILD_TESTS OFF) +set(GLFW_BUILD_DOCS OFF) +set(GLFW_INSTALL OFF) +set(GLFW_BUILD_EXAMPLES OFF) +if(NOT GLFW_BUILD_WAYLAND) + set(GLFW_BUILD_WAYLAND OFF) +endif() +if (APPLE) + enable_language(OBJC) +endif() +FetchContent_MakeAvailable(glfw3) +if (glfw3_SOURCE_DIR) + if (TARGET update_mappings) + set_target_properties(glfw PROPERTIES FOLDER "dependencies/GLFW3") + set_target_properties(update_mappings PROPERTIES FOLDER "dependencies/GLFW3") + else() + set_target_properties(glfw PROPERTIES FOLDER "dependencies") + endif() +endif() + +## GLM ############################################################### + +FetchContent_Declare(gml + GIT_REPOSITORY https://github.com/g-truc/glm.git + GIT_TAG 1.0.1 + GIT_SHALLOW 1 + GIT_PROGRESS TRUE + FIND_PACKAGE_ARGS NAMES glm +) +FetchContent_MakeAvailable(gml) +if (gml_SOURCE_DIR) + set_target_properties(glm PROPERTIES FOLDER "dependencies") +endif() + +################################################################## + +set(SHADER_TARGET_LIST) +if (GFX_BUILD_METAL) + list(APPEND SHADER_TARGET_LIST metal) +endif() +if (GFX_BUILD_VULKAN) + list(APPEND SHADER_TARGET_LIST spirv) +endif() +list(JOIN SHADER_TARGET_LIST "," SHADER_TARGETS) + +set(SHADER_SLIB "${CMAKE_CURRENT_BINARY_DIR}/shader.slib") +file(GLOB SHADER_SRCS "*.slang") + +add_custom_command( + OUTPUT ${SHADER_SLIB} + COMMAND $ -t ${SHADER_TARGETS} -o ${SHADER_SLIB} ${SHADER_SRCS} + DEPENDS gfxsc ${SHADER_SRCS} + COMMENT "Building descriptor_indexing shader" + VERBATIM +) +add_custom_target(descriptor_indexing_shader ALL DEPENDS ${SHADER_SLIB}) +set_target_properties(descriptor_indexing_shader PROPERTIES FOLDER "examples/descriptor_indexing") + +add_executable(descriptor_indexing) + +if (APPLE) + enable_language(OBJC) +endif() + +set_target_properties(descriptor_indexing PROPERTIES + FOLDER "examples/descriptor_indexing" + ENABLE_EXPORTS ON +) + +target_compile_features(descriptor_indexing PRIVATE cxx_std_23) + +if(MSVC) + target_compile_options(descriptor_indexing PRIVATE /W4) +else() + target_compile_options(descriptor_indexing PRIVATE -Wall -Wextra -Wpedantic) + target_compile_options(descriptor_indexing PRIVATE $<$:-Wno-missing-field-initializers>) +endif() + +file(GLOB EXE_SRCS "*.cpp" "*.hpp") +target_sources(descriptor_indexing PRIVATE ${EXE_SRCS}) +if (WIN32) + target_sources(descriptor_indexing PRIVATE descriptor_indexing.def) +endif() + +target_include_directories(descriptor_indexing PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") + +target_compile_definitions(descriptor_indexing PRIVATE "SHADER_SLIB=\"${SHADER_SLIB}\"") +if(CMAKE_GENERATOR STREQUAL "Xcode") + target_compile_definitions(descriptor_indexing PRIVATE "__XCODE__") +endif() +target_compile_definitions(descriptor_indexing PRIVATE "GLFW_INCLUDE_NONE") + +target_link_libraries(descriptor_indexing PRIVATE Graphics glfw glm::glm) +add_dependencies(descriptor_indexing descriptor_indexing_shader) + +if(APPLE AND NOT CMAKE_GENERATOR STREQUAL "Xcode") + set(CODESIGN_IDENTITY "-" CACHE STRING "Codesigning identity for descriptor_indexing") + + set(DESCRIPTOR_INDEXING_ENTITLEMENTS_FILE "${CMAKE_CURRENT_SOURCE_DIR}/descriptor_indexing.entitlements") + if(EXISTS "${DESCRIPTOR_INDEXING_ENTITLEMENTS_FILE}") + add_custom_command(TARGET descriptor_indexing POST_BUILD + COMMAND /bin/sh -c "codesign --sign \"${CODESIGN_IDENTITY}\" --force --entitlements \"${DESCRIPTOR_INDEXING_ENTITLEMENTS_FILE}\" \"$\" >/dev/null 2>&1" + COMMENT "Codesigning descriptor_indexing (with entitlements) [identity: ${CODESIGN_IDENTITY}]" + VERBATIM + ) + else() + add_custom_command(TARGET descriptor_indexing POST_BUILD + COMMAND /bin/sh -c "codesign --sign \"${CODESIGN_IDENTITY}\" --force \"$\" >/dev/null 2>&1" + COMMENT "Codesigning descriptor_indexing [identity: ${CODESIGN_IDENTITY}]" + VERBATIM + ) + endif() +endif() diff --git a/examples/descriptor_indexing/descriptor_indexing.cpp b/examples/descriptor_indexing/descriptor_indexing.cpp new file mode 100644 index 00000000..bf2d7ea3 --- /dev/null +++ b/examples/descriptor_indexing/descriptor_indexing.cpp @@ -0,0 +1,416 @@ +/* + * --------------------------------------------------- + * descriptor_indexing.cpp + * + * Author: Thomas Choquet + * --------------------------------------------------- + */ + +#include "Graphics/Buffer.hpp" +#include "Graphics/CommandBuffer.hpp" +#include "Graphics/Device.hpp" +#include "Graphics/Drawable.hpp" +#include "Graphics/Enums.hpp" +#include "Graphics/Framebuffer.hpp" +#include "Graphics/GraphicsPipeline.hpp" +#include "Graphics/Instance.hpp" +#include "Graphics/ParameterBlock.hpp" +#include "Graphics/ParameterBlockLayout.hpp" +#include "Graphics/ParameterBlockPool.hpp" +#include "Graphics/Sampler.hpp" +#include "Graphics/ShaderLib.hpp" +#include "Graphics/Surface.hpp" +#include "Graphics/Swapchain.hpp" +#include "Graphics/Texture.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __XCODE__ + #include +#endif + +constexpr uint32_t WINDOW_WIDTH = 800; +constexpr uint32_t WINDOW_HEIGHT = 600; +constexpr uint8_t maxFrameInFlight = 3; +constexpr uint32_t maxTextures = 4096; +constexpr float initialSpriteSize = 0.25f; +constexpr float spriteShrinkPerFrame = 0.0025f; + +using namespace std::chrono_literals; + +struct Vertex +{ + glm::vec2 pos; +}; + +struct PushConstants +{ + glm::vec2 pos; + float rotation; + float size; + uint32_t textureIdx; + uint32_t _padding; +}; + +struct Sprite +{ + glm::vec2 pos; + float rotation; + float size = initialSpriteSize; + uint32_t textureIdx = 0; +}; + +constexpr auto sprite_vertices = std::to_array({ + Vertex{ .pos = { -0.5f, 0.5f } }, + Vertex{ .pos = { 0.5f, 0.5f } }, + Vertex{ .pos = { 0.5f, -0.5f } }, + Vertex{ .pos = { -0.5f, 0.5f } }, + Vertex{ .pos = { 0.5f, -0.5f } }, + Vertex{ .pos = { -0.5f, -0.5f } }, +}); + +class Application +{ +public: + void init() + { + #if __XCODE__ + sleep(1); // XCODE BUG https://github.com/glfw/glfw/issues/2634 + #endif + + auto res = glfwInit(); + assert(res == GLFW_TRUE); + (void)res; + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + m_window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Descriptor indexing", nullptr, nullptr); + assert(m_window); + + glfwSetWindowUserPointer(m_window, this); + glfwSetWindowSizeCallback(m_window, [](GLFWwindow* window, int, int){ + static_cast(glfwGetWindowUserPointer(window))->m_swapchain = nullptr; + }); + + m_instance = gfx::Instance::newInstance(gfx::Instance::Descriptor{}); + assert(m_instance); + + m_surface = m_instance->createSurface(m_window); + assert(m_surface); + + gfx::Device::Descriptor deviceDescriptor = { + .queueCaps = { + .graphics = true, + .compute = true, + .transfer = true, + .present = { m_surface.get() } + } + }; + m_device = m_instance->newDevice(deviceDescriptor); + assert(m_device); + + assert(m_surface->supportedPixelFormats(*m_device).contains(gfx::PixelFormat::BGRA8Unorm)); + assert(m_surface->supportedPresentModes(*m_device).contains(gfx::PresentMode::fifo)); + + std::unique_ptr shaderLib = m_device->newShaderLib(SHADER_SLIB); + assert(shaderLib); + + m_textureArrayPBLayout = m_device->newParameterBlockLayout(gfx::ParameterBlockLayout::Descriptor{ + .bindings = { + gfx::ParameterBlockBinding{ .type = gfx::BindingType::sampler, .usages = gfx::BindingUsage::fragmentRead }, + gfx::ParameterBlockBinding{ .type = gfx::BindingType::sampledTexture, .usages = gfx::BindingUsage::fragmentRead, .count = maxTextures }, + } + }); + + gfx::GraphicsPipeline::Descriptor pipelineDesc = { + .vertexLayout = gfx::VertexLayout{ + .stride = sizeof(Vertex), + .attributes = { + gfx::VertexAttribute{ .format = gfx::VertexAttributeFormat::float2, .offset = offsetof(Vertex, pos) }, + } + }, + .vertexShader = &shaderLib->getFunction("vertexMain"), + .fragmentShader = &shaderLib->getFunction("fragmentMain"), + .colorAttachmentPxFormats = { gfx::PixelFormat::BGRA8Unorm }, + .parameterBlockLayouts = { m_textureArrayPBLayout }, + }; + m_graphicsPipeline = m_device->newGraphicsPipeline(pipelineDesc); + assert(m_graphicsPipeline); + + m_vertexBuffer = m_device->newBuffer(gfx::Buffer::Descriptor{ + .size = sizeof(Vertex) * sprite_vertices.size(), + .usages = gfx::BufferUsage::vertexBuffer | gfx::BufferUsage::copyDestination, + .storageMode = gfx::ResourceStorageMode::deviceLocal + }); + assert(m_vertexBuffer); + + std::shared_ptr stagingBuffer = m_device->newBuffer(gfx::Buffer::Descriptor{ + .size = m_vertexBuffer->size(), + .usages = gfx::BufferUsage::copySource, + .storageMode = gfx::ResourceStorageMode::hostVisible + }); + assert(stagingBuffer); + + std::ranges::copy(sprite_vertices, stagingBuffer->content()); + + std::shared_ptr commandBuffer = m_device->newCommandBufferPool()->get(); + commandBuffer->beginBlitPass(); + commandBuffer->copyBufferToBuffer(stagingBuffer, m_vertexBuffer, m_vertexBuffer->size()); + commandBuffer->endBlitPass(); + m_device->submitCommandBuffers(commandBuffer); + m_device->waitCommandBuffer(*commandBuffer); + + for (uint8_t i = 0; i < maxFrameInFlight; ++i) + m_commandBufferPools.at(i) = m_device->newCommandBufferPool(); + + m_textureStreamerThread = std::thread([this]() + { + std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution channelDistribution(32, 255); + + std::shared_ptr parameterBlock = m_device->newParameterBlockPool(gfx::ParameterBlockPool::Descriptor{ + .maxBindingCount = { + { gfx::BindingType::sampler, 1 }, + { gfx::BindingType::sampledTexture, maxTextures }, + }, + .updateAfterBind = true + })->get(m_textureArrayPBLayout); + parameterBlock->setBinding(0, std::shared_ptr(m_device->newSampler(gfx::Sampler::Descriptor{}))); + + std::atomic_store(&m_textureArrayPBlock, parameterBlock); + + std::unique_ptr cmdBufferPool = m_device->newCommandBufferPool(); + uint32_t textureIndex = 0; + + while (m_textureStreamerThreadContinue.load()) + { + if (textureIndex >= maxTextures) + break; + + std::shared_ptr texture = m_device->newTexture(gfx::Texture::Descriptor{ + .type = gfx::TextureType::texture2d, + .width = 2, + .height = 2, + .pixelFormat = gfx::PixelFormat::RGBA8Unorm, + .usages = gfx::TextureUsage::copyDestination | gfx::TextureUsage::shaderRead, + .storageMode = gfx::ResourceStorageMode::deviceLocal + }); + assert(texture); + + std::shared_ptr stagingBuffer = m_device->newBuffer(gfx::Buffer::Descriptor{ + .size = static_cast(4 * 4), + .usages = gfx::BufferUsage::copySource, + .storageMode = gfx::ResourceStorageMode::hostVisible + }); + assert(stagingBuffer); + + const std::array rgba = { + static_cast(channelDistribution(rng)), + static_cast(channelDistribution(rng)), + static_cast(channelDistribution(rng)), + 255u + }; + auto* bytes = stagingBuffer->content(); + for (uint32_t texel = 0; texel < 4; ++texel) + std::memcpy(bytes + texel * rgba.size(), rgba.data(), rgba.size()); + + std::shared_ptr uploadCommandBuffer = cmdBufferPool->get(); + uploadCommandBuffer->beginBlitPass(); + uploadCommandBuffer->copyBufferToTexture(stagingBuffer, texture); + uploadCommandBuffer->endBlitPass(); + m_device->submitCommandBuffers(uploadCommandBuffer); + m_device->waitCommandBuffer(*uploadCommandBuffer); + cmdBufferPool->reset(); + + parameterBlock->setBinding(1, textureIndex, texture); + { + std::lock_guard guard(m_availableTextureIndicesMtx); + m_availableTextureIndices.insert(textureIndex); + } + + ++textureIndex; + + std::this_thread::sleep_for(100ms); + } + }); + } + + void loop() + { + while (true) + { + glfwPollEvents(); + if (glfwWindowShouldClose(m_window)) + break; + + if (m_swapchain == nullptr) { + int width = 0; + int height = 0; + glfwGetFramebufferSize(m_window, &width, &height); + m_swapchain = m_device->newSwapchain(gfx::Swapchain::Descriptor{ + .surface = m_surface.get(), + .width = static_cast(width), + .height = static_cast(height), + .imageCount = 3, + .pixelFormat = gfx::PixelFormat::BGRA8Unorm, + .presentMode = gfx::PresentMode::fifo, + }); + assert(m_swapchain); + m_device->waitIdle(); + } + + if (m_lastCommandBuffers.at(m_frameIdx) != nullptr) { + m_device->waitCommandBuffer(*m_lastCommandBuffers.at(m_frameIdx)); + m_lastCommandBuffers.at(m_frameIdx) = nullptr; + m_commandBufferPools.at(m_frameIdx)->reset(); + } + + for (auto& sprite : m_sprites) + sprite.size = std::max(0.0f, sprite.size - spriteShrinkPerFrame); + std::erase_if(m_sprites, [](const auto& sprite) { return sprite.size <= 0.0f; }); + + { + std::lock_guard guard(m_availableTextureIndicesMtx); + if (m_availableTextureIndices.empty() == false) { + int width = 0; + int height = 0; + glfwGetFramebufferSize(m_window, &width, &height); + if (width > 0 && height > 0) { + double mouseX = 0.0; + double mouseY = 0.0; + glfwGetCursorPos(m_window, &mouseX, &mouseY); + + std::uniform_int_distribution textureIndexDistribution(0, m_availableTextureIndices.size() - 1); + auto textureIndexIt = m_availableTextureIndices.begin(); + std::advance(textureIndexIt, static_cast(textureIndexDistribution(m_rng))); + const glm::vec2 clipPos = { + static_cast((mouseX / static_cast(width)) * 2.0 - 1.0), + static_cast(1.0 - (mouseY / static_cast(height)) * 2.0) + }; + std::uniform_real_distribution rotationDistribution(0.0f, 2.0f * std::numbers::pi_v); + m_sprites.push_back(Sprite{ + .pos = clipPos, + .rotation = rotationDistribution(m_rng), + .size = initialSpriteSize, + .textureIdx = *textureIndexIt + }); + } + } + } + + std::shared_ptr drawable = m_swapchain->nextDrawable(); + if (drawable == nullptr) { + m_swapchain = nullptr; + continue; + } + + std::shared_ptr commandBuffer = m_commandBufferPools.at(m_frameIdx)->get(); + + gfx::Framebuffer framebuffer = { + .colorAttachments = { + gfx::Framebuffer::Attachment{ + .loadAction = gfx::LoadAction::clear, + .clearColor = { 0.08f, 0.08f, 0.10f, 1.0f }, + .texture = drawable->texture() + } + } + }; + + commandBuffer->beginRenderPass(framebuffer); + { + commandBuffer->usePipeline(m_graphicsPipeline); + commandBuffer->useVertexBuffer(m_vertexBuffer); + const auto textureArrayPBlock = std::atomic_load(&m_textureArrayPBlock); + if (textureArrayPBlock != nullptr) { + commandBuffer->setParameterBlock(textureArrayPBlock, 0); + + for (const auto& sprite : m_sprites) { + const PushConstants pushConstants{ + .pos = sprite.pos, + .rotation = sprite.rotation, + .size = sprite.size, + .textureIdx = sprite.textureIdx + }; + commandBuffer->setPushConstants(&pushConstants); + commandBuffer->drawVertices(0, static_cast(sprite_vertices.size())); + } + } + } + commandBuffer->endRenderPass(); + + commandBuffer->presentDrawable(drawable); + + m_device->submitCommandBuffers(commandBuffer); + + m_lastCommandBuffers.at(m_frameIdx) = commandBuffer.get(); + m_frameIdx = (m_frameIdx + 1) % maxFrameInFlight; + } + + m_textureStreamerThreadContinue.store(false); + } + + void clean() + { + if (m_textureStreamerThread.joinable()) + m_textureStreamerThread.join(); + glfwDestroyWindow(m_window); + glfwTerminate(); + } + +private: + GLFWwindow* m_window = nullptr; + + std::unique_ptr m_instance; + std::unique_ptr m_surface; + std::unique_ptr m_device; + std::unique_ptr m_swapchain; + + std::shared_ptr m_textureArrayPBLayout; + std::shared_ptr m_graphicsPipeline; + std::shared_ptr m_textureArrayPBlock; + + std::shared_ptr m_vertexBuffer; + + std::thread m_textureStreamerThread; + std::atomic m_textureStreamerThreadContinue = true; + std::set m_availableTextureIndices; + std::mutex m_availableTextureIndicesMtx; + + std::vector m_sprites; + std::mt19937 m_rng = std::mt19937(std::random_device{}()); + + uint8_t m_frameIdx = 0; + std::array, maxFrameInFlight> m_commandBufferPools; + std::array m_lastCommandBuffers{}; +}; + +int main() +{ + try + { + Application app; + app.init(); + app.loop(); + app.clean(); + } + catch (...) + { + return -1; + } + return 0; +} diff --git a/examples/descriptor_indexing/descriptor_indexing.def b/examples/descriptor_indexing/descriptor_indexing.def new file mode 100644 index 00000000..d3db5b00 --- /dev/null +++ b/examples/descriptor_indexing/descriptor_indexing.def @@ -0,0 +1,3 @@ +EXPORTS + glfwGetRequiredInstanceExtensions + glfwCreateWindowSurface diff --git a/examples/descriptor_indexing/shader.h b/examples/descriptor_indexing/shader.h new file mode 100644 index 00000000..e13b5cb3 --- /dev/null +++ b/examples/descriptor_indexing/shader.h @@ -0,0 +1,49 @@ +/* + * --------------------------------------------------- + * shader.h + * + * Author: Thomas Choquet + * --------------------------------------------------- + * + * file included by all slang files + * + */ + +#ifndef SHADER_H +#define SHADER_H + +#ifdef __cplusplus + #include + using float2 = glm::vec2; + using float3 = glm::vec3; + using float4 = glm::vec4; + using float4x4 = glm::mat4; + #define SLANG_PUBLIC + #define SLANG_MODULE_DEF(name) + #define SLANG_MODULE_IMP(name) + #define CBUFFER_BEGIN(name) + #define CBUFFER_END + #define FLOAT3_PADDING(n) float _padding3##n + #define FLOAT1_PADDING(n) float _padding1a##n, _padding1b##n, _padding1c##n +#else + #define SLANG_PUBLIC public + #define SLANG_MODULE_DEF(name) module name + #define SLANG_MODULE_IMP(name) import name + #define alignas(n) + #define CBUFFER_BEGIN(name) cbuffer name { + #define CBUFFER_END }; + #ifdef __SPIRV__ + #define FLOAT3_PADDING(n) float _padding3##n + #define FLOAT1_PADDING(n) float _padding1a##n, _padding1b##n, _padding1c##n + #else + #define FLOAT3_PADDING(n) + #define FLOAT1_PADDING(n) + #endif + #ifdef __METAL__ + #define PUSH_CONSTANT [[vk::push_constant]] cbuffer PushConstant : register(b6) + #else + #define PUSH_CONSTANT [[vk::push_constant]] cbuffer PushConstant + #endif +#endif + +#endif diff --git a/examples/descriptor_indexing/shader.slang b/examples/descriptor_indexing/shader.slang new file mode 100644 index 00000000..f58bab77 --- /dev/null +++ b/examples/descriptor_indexing/shader.slang @@ -0,0 +1,65 @@ +/* + * --------------------------------------------------- + * shader.h + * + * Author: Thomas Choquet + * --------------------------------------------------- + */ + +#ifndef SHADER_SLANG +#define SHADER_SLANG + +#include "shader.h" + +#define MAX_TEXTURES 4096 + +struct TextureArray +{ + SamplerState sampler; + Texture2D textures[MAX_TEXTURES]; +}; +ParameterBlock textureArray; + +PUSH_CONSTANT +{ + float2 pos; + float rotation; + float size; + uint32_t textureIdx; +} + +struct Vertex +{ + float2 pos; +}; + +struct VSOutput +{ + float4 clipPos : SV_Position; + float2 uv; +}; + +[shader("vertex")] +VSOutput vertexMain(Vertex input) +{ + VSOutput output; + const float c = cos(rotation); + const float s = sin(rotation); + const float2 scaledPos = input.pos * size; + const float2 rotatedPos = float2( + scaledPos.x * c - scaledPos.y * s, + scaledPos.x * s + scaledPos.y * c + ); + + output.clipPos = float4(rotatedPos + pos, 0.0, 1.0); + output.uv = input.pos + 0.5; + return output; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_TARGET +{ + return textureArray.textures[textureIdx].Sample(textureArray.sampler, input.uv); +} + +#endif diff --git a/include/Graphics/ParameterBlock.hpp b/include/Graphics/ParameterBlock.hpp index e8215dcc..8a48cf57 100644 --- a/include/Graphics/ParameterBlock.hpp +++ b/include/Graphics/ParameterBlock.hpp @@ -17,6 +17,7 @@ #include #include +#include namespace gfx { @@ -29,9 +30,16 @@ class ParameterBlock virtual std::shared_ptr layout() const = 0; virtual void setBinding(uint32_t idx, const std::shared_ptr&) = 0; + virtual void setBinding(uint32_t idx, const std::shared_ptr&) = 0; + virtual void setBinding(uint32_t idx, uint32_t arrayIndex, const std::shared_ptr&) = 0; + virtual void setBinding(uint32_t idx, uint32_t firstArrayIndex, std::span>) = 0; + virtual void setBinding(uint32_t idx, const std::shared_ptr&) = 0; + virtual void clearBinding(uint32_t idx, uint32_t arrayIndex) = 0; + virtual void clearBinding(uint32_t idx, uint32_t firstArrayIndex, uint32_t count) = 0; + virtual ~ParameterBlock() = default; protected: diff --git a/include/Graphics/ParameterBlockLayout.hpp b/include/Graphics/ParameterBlockLayout.hpp index 1bdc608c..78d11689 100644 --- a/include/Graphics/ParameterBlockLayout.hpp +++ b/include/Graphics/ParameterBlockLayout.hpp @@ -12,6 +12,7 @@ #include "Graphics/Enums.hpp" +#include #include namespace gfx @@ -21,6 +22,7 @@ struct ParameterBlockBinding { BindingType type = BindingType::constantBuffer; BindingUsages usages = BindingUsage::vertexRead | BindingUsage::fragmentRead; + uint32_t count = 1; auto operator<=>(const ParameterBlockBinding&) const = default; }; diff --git a/include/Graphics/ParameterBlockPool.hpp b/include/Graphics/ParameterBlockPool.hpp index d00e0036..7b52caaf 100644 --- a/include/Graphics/ParameterBlockPool.hpp +++ b/include/Graphics/ParameterBlockPool.hpp @@ -27,6 +27,7 @@ class ParameterBlockPool struct Descriptor { std::map maxBindingCount; + bool updateAfterBind = false; auto operator<=>(const Descriptor&) const = default; }; diff --git a/src/Metal/MetalCommandBuffer.mm b/src/Metal/MetalCommandBuffer.mm index 4a8729d1..70fb1657 100644 --- a/src/Metal/MetalCommandBuffer.mm +++ b/src/Metal/MetalCommandBuffer.mm @@ -119,29 +119,29 @@ assert([m_commandEncoder conformsToProtocol:@protocol(MTLRenderCommandEncoder)]); auto renderCommandEncoder = (id)m_commandEncoder; - for (const auto& [buffer, binding] : pBlock->encodedBuffers()) - [renderCommandEncoder useResource:buffer->mtlBuffer() usage:toMTLResourceUsage(binding.usages) stages:toMTLRenderStages(binding.usages)]; + for (const auto& encodedBuffer : pBlock->encodedBuffers()) + [renderCommandEncoder useResource:encodedBuffer.resource->mtlBuffer() usage:toMTLResourceUsage(encodedBuffer.binding.usages) stages:toMTLRenderStages(encodedBuffer.binding.usages)]; - for (const auto& [texture, binding] : pBlock->encodedTextures()) - [renderCommandEncoder useResource:texture->mtltexture() usage:toMTLResourceUsage(binding.usages) stages:toMTLRenderStages(binding.usages)]; + for (const auto& encodedTexture : pBlock->encodedTextures()) + [renderCommandEncoder useResource:encodedTexture.resource->mtltexture() usage:toMTLResourceUsage(encodedTexture.binding.usages) stages:toMTLRenderStages(encodedTexture.binding.usages)]; - if (std::ranges::any_of(pBlock->encodedBuffers(), [](auto& pair) { return pair.second.usages & BindingUsage::vertexRead || pair.second.usages & BindingUsage::vertexWrite; }) || - std::ranges::any_of(pBlock->encodedTextures(), [](auto& pair) { return pair.second.usages & BindingUsage::vertexRead || pair.second.usages & BindingUsage::vertexWrite; }) || - std::ranges::any_of(pBlock->encodedSamplers(), [](auto& pair) { return pair.second.usages & BindingUsage::vertexRead || pair.second.usages & BindingUsage::vertexWrite; })) + if (std::ranges::any_of(pBlock->encodedBuffers(), [](const auto& encodedBuffer) { return encodedBuffer.binding.usages & BindingUsage::vertexRead || encodedBuffer.binding.usages & BindingUsage::vertexWrite; }) || + std::ranges::any_of(pBlock->encodedTextures(), [](const auto& encodedTexture) { return encodedTexture.binding.usages & BindingUsage::vertexRead || encodedTexture.binding.usages & BindingUsage::vertexWrite; }) || + std::ranges::any_of(pBlock->encodedSamplers(), [](const auto& encodedSampler) { return encodedSampler.binding.usages & BindingUsage::vertexRead || encodedSampler.binding.usages & BindingUsage::vertexWrite; })) { [renderCommandEncoder setVertexBuffer:pBlock->argumentBuffer().mtlBuffer() offset:pBlock->offset() atIndex:index]; } - if (std::ranges::any_of(pBlock->encodedBuffers(), [](auto& pair) { return pair.second.usages & BindingUsage::fragmentRead || pair.second.usages & BindingUsage::fragmentWrite; }) || - std::ranges::any_of(pBlock->encodedTextures(), [](auto& pair) { return pair.second.usages & BindingUsage::fragmentRead || pair.second.usages & BindingUsage::fragmentWrite; }) || - std::ranges::any_of(pBlock->encodedSamplers(), [](auto& pair) { return pair.second.usages & BindingUsage::fragmentRead || pair.second.usages & BindingUsage::fragmentWrite; })) + if (std::ranges::any_of(pBlock->encodedBuffers(), [](const auto& encodedBuffer) { return encodedBuffer.binding.usages & BindingUsage::fragmentRead || encodedBuffer.binding.usages & BindingUsage::fragmentWrite; }) || + std::ranges::any_of(pBlock->encodedTextures(), [](const auto& encodedTexture) { return encodedTexture.binding.usages & BindingUsage::fragmentRead || encodedTexture.binding.usages & BindingUsage::fragmentWrite; }) || + std::ranges::any_of(pBlock->encodedSamplers(), [](const auto& encodedSampler) { return encodedSampler.binding.usages & BindingUsage::fragmentRead || encodedSampler.binding.usages & BindingUsage::fragmentWrite; })) { [renderCommandEncoder setFragmentBuffer:pBlock->argumentBuffer().mtlBuffer() offset:pBlock->offset() atIndex:index]; } - m_usedBuffers.insert_range(pBlock->encodedBuffers() | std::views::transform([](auto& pair) -> std::shared_ptr { return pair.first; })); - m_usedTextures.insert_range(pBlock->encodedTextures() | std::views::transform([](auto& pair) -> std::shared_ptr { return pair.first; })); - m_usedSamplers.insert_range(pBlock->encodedSamplers() | std::views::transform([](auto& pair) -> std::shared_ptr { return pair.first; })); + m_usedBuffers.insert_range(pBlock->encodedBuffers() | std::views::transform([](const auto& encodedBuffer) -> std::shared_ptr { return encodedBuffer.resource; })); + m_usedTextures.insert_range(pBlock->encodedTextures() | std::views::transform([](const auto& encodedTexture) -> std::shared_ptr { return encodedTexture.resource; })); + m_usedSamplers.insert_range(pBlock->encodedSamplers() | std::views::transform([](const auto& encodedSampler) -> std::shared_ptr { return encodedSampler.resource; })); m_usedPBlock.insert(pBlock); }} diff --git a/src/Metal/MetalParameterBlock.hpp b/src/Metal/MetalParameterBlock.hpp index 579fd2aa..2f7443ef 100644 --- a/src/Metal/MetalParameterBlock.hpp +++ b/src/Metal/MetalParameterBlock.hpp @@ -18,11 +18,23 @@ #include "Metal/MetalSampler.hpp" #include "MetalParameterBlockLayout.hpp" +#include +#include +#include + namespace gfx { class MetalParameterBlock : public ParameterBlock { +public: + template + struct EncodedResource + { + std::shared_ptr resource; + ParameterBlockBinding binding; + }; + public: MetalParameterBlock() = delete; MetalParameterBlock(const MetalParameterBlock&) = delete; @@ -33,17 +45,32 @@ class MetalParameterBlock : public ParameterBlock inline std::shared_ptr layout() const override { return m_layout; } void setBinding(uint32_t idx, const std::shared_ptr&) override; + void setBinding(uint32_t idx, const std::shared_ptr&) override; + void setBinding(uint32_t idx, uint32_t arrayIndex, const std::shared_ptr&) override; + void setBinding(uint32_t idx, uint32_t firstArrayIndex, std::span>) override; + void setBinding(uint32_t idx, const std::shared_ptr&) override; + void clearBinding(uint32_t idx, uint32_t arrayIndex) override; + void clearBinding(uint32_t idx, uint32_t firstArrayIndex, uint32_t count) override; + inline const MetalBuffer& argumentBuffer() const { return *m_argumentBuffer; } inline size_t offset() const { return m_offset; } - inline const std::map, ParameterBlockBinding>& encodedBuffers() const { return m_nonReusedRessources.encodedBuffers; } - inline const std::map, ParameterBlockBinding>& encodedTextures() const { return m_nonReusedRessources.encodedTextures; }; - inline const std::map, ParameterBlockBinding>& encodedSamplers() const { return m_nonReusedRessources.encodedSamplers; }; + inline auto encodedBuffers() const { return m_encodedBuffers | std::views::transform([](const auto& resources) { return resources | std::views::values; }) | std::views::join; } + inline auto encodedTextures() const { return m_encodedTextures | std::views::transform([](const auto& resources) { return resources | std::views::values; }) | std::views::join; } + inline auto encodedSamplers() const { return m_encodedSamplers | std::views::transform([](const auto& resources) { return resources | std::views::values; }) | std::views::join; } - inline void reuse() { m_nonReusedRessources = NonReusedRessources(); } + inline void reuse() + { + for (auto& resources : m_encodedBuffers) + resources.clear(); + for (auto& resources : m_encodedTextures) + resources.clear(); + for (auto& resources : m_encodedSamplers) + resources.clear(); + } ~MetalParameterBlock() override = default; @@ -52,13 +79,9 @@ class MetalParameterBlock : public ParameterBlock std::shared_ptr m_argumentBuffer; size_t m_offset = 0; - struct NonReusedRessources - { - std::map, ParameterBlockBinding> encodedBuffers; - std::map, ParameterBlockBinding> encodedTextures; - std::map, ParameterBlockBinding> encodedSamplers; - } - m_nonReusedRessources; + std::vector>> m_encodedBuffers; + std::vector>> m_encodedTextures; + std::vector>> m_encodedSamplers; public: MetalParameterBlock& operator=(const MetalParameterBlock&) = delete; diff --git a/src/Metal/MetalParameterBlock.mm b/src/Metal/MetalParameterBlock.mm index 1602b94a..5a0935ab 100644 --- a/src/Metal/MetalParameterBlock.mm +++ b/src/Metal/MetalParameterBlock.mm @@ -13,12 +13,30 @@ #include "Metal/MetalParameterBlock.hpp" #include "Metal/MetalBuffer.hpp" -#define m_encodedBuffers m_nonReusedRessources.encodedBuffers -#define m_encodedTextures m_nonReusedRessources.encodedTextures -#define m_encodedSamplers m_nonReusedRessources.encodedSamplers +#include +#include +#include +#include static_assert(sizeof(MTLResourceID) == sizeof(uint64_t), "MTLResourceID is not 64 bits"); +namespace +{ + +uint32_t bindingOffset(const gfx::ParameterBlockLayout& layout, uint32_t idx) +{ + const auto bindingCounts = layout.bindings() | std::views::transform([](const auto& binding) { return binding.count; }); + return std::accumulate(bindingCounts.begin(), std::next(bindingCounts.begin(), idx), 0u); +} + +uint32_t totalDescriptorCount(const gfx::ParameterBlockLayout& layout) +{ + const auto bindingCounts = layout.bindings() | std::views::transform([](const auto& binding) { return binding.count; }); + return std::accumulate(bindingCounts.begin(), bindingCounts.end(), 0u); +} + +} + namespace gfx { @@ -27,7 +45,10 @@ m_argumentBuffer(argumentBuffer), m_offset(offset) { - assert((argumentBuffer->size() - m_offset) >= (m_layout->bindings().size() * sizeof(uint64_t))); + assert((argumentBuffer->size() - m_offset) >= (totalDescriptorCount(*m_layout) * sizeof(uint64_t))); + m_encodedBuffers.resize(m_layout->bindings().size()); + m_encodedTextures.resize(m_layout->bindings().size()); + m_encodedSamplers.resize(m_layout->bindings().size()); } void MetalParameterBlock::setBinding(uint32_t idx, const std::shared_ptr& aBuffer) { @autoreleasepool @@ -36,20 +57,41 @@ assert(buffer); auto* content = std::bit_cast(m_argumentBuffer->content() + m_offset); - content[idx] = buffer->mtlBuffer().gpuAddress; - - m_encodedBuffers.insert(std::make_pair(buffer, m_layout->bindings()[idx])); + content[bindingOffset(*m_layout, idx)] = buffer->mtlBuffer().gpuAddress; + auto& encodedBuffers = m_encodedBuffers.at(idx); + encodedBuffers.insert_or_assign(0, EncodedResource{ + .resource = buffer, + .binding = m_layout->bindings().at(idx) + }); }} void MetalParameterBlock::setBinding(uint32_t idx, const std::shared_ptr& aTexture) { @autoreleasepool { - auto texture = std::dynamic_pointer_cast(aTexture); - assert(texture); + setBinding(idx, 0, aTexture); +}} - auto* content = std::bit_cast(m_argumentBuffer->content() + m_offset); - content[idx] = texture->mtltexture().gpuResourceID; +void MetalParameterBlock::setBinding(uint32_t idx, uint32_t arrayIndex, const std::shared_ptr& aTexture) { @autoreleasepool +{ + setBinding(idx, arrayIndex, std::span(&aTexture, 1)); +}} - m_encodedTextures.insert(std::make_pair(texture, m_layout->bindings()[idx])); +void MetalParameterBlock::setBinding(uint32_t idx, uint32_t firstArrayIndex, std::span> textures) { @autoreleasepool +{ + assert(m_layout->bindings().at(idx).type == BindingType::sampledTexture); + assert(firstArrayIndex + textures.size() <= m_layout->bindings().at(idx).count); + + auto* content = std::bit_cast(m_argumentBuffer->content() + m_offset); + const uint32_t offset = bindingOffset(*m_layout, idx); + for (uint32_t i = firstArrayIndex; const auto& texturePtr : textures) { + auto texture = std::dynamic_pointer_cast(texturePtr); + assert(texture); + content[offset + i] = texture->mtltexture().gpuResourceID; + m_encodedTextures.at(idx).insert_or_assign(i, EncodedResource{ + .resource = texture, + .binding = m_layout->bindings().at(idx) + }); + ++i; + } }} void MetalParameterBlock::setBinding(uint32_t idx, const std::shared_ptr& aSampler) { @autoreleasepool @@ -58,9 +100,45 @@ assert(sampler); auto* content = std::bit_cast(m_argumentBuffer->content() + m_offset); - content[idx] = sampler->mtlSamplerState().gpuResourceID; + content[bindingOffset(*m_layout, idx)] = sampler->mtlSamplerState().gpuResourceID; + auto& encodedSamplers = m_encodedSamplers.at(idx); + encodedSamplers.insert_or_assign(0, EncodedResource{ + .resource = sampler, + .binding = m_layout->bindings().at(idx) + }); +}} - m_encodedSamplers.insert(std::make_pair(sampler, m_layout->bindings()[idx])); +void MetalParameterBlock::clearBinding(uint32_t idx, uint32_t arrayIndex) { @autoreleasepool +{ + clearBinding(idx, arrayIndex, 1); +}} + +void MetalParameterBlock::clearBinding(uint32_t idx, uint32_t firstArrayIndex, uint32_t count) { @autoreleasepool +{ + assert(count > 0); + assert(firstArrayIndex + count <= m_layout->bindings().at(idx).count); + + auto eraseBindingRange = [firstArrayIndex, count](std::unordered_map>& resources) { + std::erase_if(resources, [firstArrayIndex, count](const auto& entry) { + return entry.first >= firstArrayIndex && entry.first < firstArrayIndex + count; + }); + }; + + switch (m_layout->bindings().at(idx).type) + { + case BindingType::constantBuffer: + case BindingType::structuredBuffer: + eraseBindingRange(m_encodedBuffers.at(idx)); + break; + case BindingType::sampledTexture: + eraseBindingRange(m_encodedTextures.at(idx)); + break; + case BindingType::sampler: + eraseBindingRange(m_encodedSamplers.at(idx)); + break; + default: + std::unreachable(); + } }} } // namespace gfx diff --git a/src/Metal/MetalParameterBlockPool.mm b/src/Metal/MetalParameterBlockPool.mm index 3e8db707..e988fff2 100644 --- a/src/Metal/MetalParameterBlockPool.mm +++ b/src/Metal/MetalParameterBlockPool.mm @@ -14,6 +14,10 @@ #include "Metal/MetalDevice.hpp" #include "MetalParameterBlockLayout.hpp" +#include +#include +#include + namespace gfx { @@ -38,7 +42,8 @@ } else { pBlock = std::make_shared(pbLayout, m_argumentBuffer, m_nextOffset); - size_t usedSize = sizeof(uint64_t) * pbLayout->bindings().size(); + const auto bindingCounts = pbLayout->bindings() | std::views::transform([](const auto& binding) { return binding.count; }); + size_t usedSize = sizeof(uint64_t) * std::accumulate(bindingCounts.begin(), bindingCounts.end(), 0u); m_nextOffset += (usedSize + 31uz) & ~31uz; } m_usedPBlocks.push_back(pBlock); diff --git a/src/Vulkan/VulkanDevice.cpp b/src/Vulkan/VulkanDevice.cpp index b57f9b8d..79032c09 100644 --- a/src/Vulkan/VulkanDevice.cpp +++ b/src/Vulkan/VulkanDevice.cpp @@ -53,6 +53,13 @@ VulkanDevice::VulkanDevice(const VulkanInstance* instance, const VulkanPhysicalD .setTimelineSemaphore(vk::True) .setPNext(dynamicRenderingFeature); + auto descriptorIndexingFeatures = vk::PhysicalDeviceDescriptorIndexingFeatures{} + .setPNext(&timelineSemaphoreFeature) + .setShaderSampledImageArrayNonUniformIndexing(vk::True) + .setDescriptorBindingSampledImageUpdateAfterBind(vk::True) + .setDescriptorBindingUpdateUnusedWhilePending(vk::True) + .setDescriptorBindingPartiallyBound(vk::True); + m_queueFamily = (m_physicalDevice->getQueueFamilies() | std::views::filter([&desc](auto f){ return f.hasCapabilities(desc.deviceDescriptor->queueCaps); })).front(); float queuePriority = 1.0f; auto queueCreateInfo = vk::DeviceQueueCreateInfo{} @@ -71,7 +78,7 @@ VulkanDevice::VulkanDevice(const VulkanInstance* instance, const VulkanPhysicalD vk::PhysicalDeviceFeatures deviceFeatures{}; auto deviceCreateInfo = vk::DeviceCreateInfo{} - .setPNext(&timelineSemaphoreFeature) + .setPNext(&descriptorIndexingFeatures) .setQueueCreateInfos(queueCreateInfo) .setEnabledExtensionCount(static_cast(enabledExtensions.size())) .setPpEnabledExtensionNames(enabledExtensions.data()) diff --git a/src/Vulkan/VulkanParameterBlock.cpp b/src/Vulkan/VulkanParameterBlock.cpp index 27215c1c..9e3e1af2 100644 --- a/src/Vulkan/VulkanParameterBlock.cpp +++ b/src/Vulkan/VulkanParameterBlock.cpp @@ -28,6 +28,10 @@ VulkanParameterBlock::VulkanParameterBlock(const VulkanDevice* device, const std assert(m_layout); assert(m_descriptorPool); + m_usedBuffers.resize(m_layout->bindings().size()); + m_usedTextures.resize(m_layout->bindings().size()); + m_usedSamplers.resize(m_layout->bindings().size()); + auto descriptorSetAllocateInfo = vk::DescriptorSetAllocateInfo{} .setDescriptorPool(*m_descriptorPool) .setDescriptorSetCount(1) @@ -36,7 +40,7 @@ VulkanParameterBlock::VulkanParameterBlock(const VulkanDevice* device, const std try { std::vector descriptorSets = m_device->vkDevice().allocateDescriptorSets(descriptorSetAllocateInfo); m_descriptorSet = std::move(descriptorSets.front()); - } catch (...){ + } catch (...) { throw std::runtime_error("failed to allocate descriptorSet"); } } @@ -59,29 +63,55 @@ void VulkanParameterBlock::setBinding(uint32_t idx, const std::shared_ptrvkDevice().updateDescriptorSets(writeDescriptorSet, {}); - - m_usedBuffers.insert(std::make_pair(buffer, m_layout->bindings().at(idx))); + auto& usedBuffers = m_usedBuffers.at(idx); + usedBuffers.insert_or_assign(0, UsedResource{ + .resource = buffer, + .binding = m_layout->bindings().at(idx) + }); } void VulkanParameterBlock::setBinding(uint32_t idx, const std::shared_ptr& aTexture) { - auto texture = std::dynamic_pointer_cast(aTexture); + setBinding(idx, 0, aTexture); +} - auto descriptorImageInfo = vk::DescriptorImageInfo{} - .setImageView(texture->vkImageView()) - .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); +void VulkanParameterBlock::setBinding(uint32_t idx, uint32_t arrayIndex, const std::shared_ptr& aTexture) +{ + setBinding(idx, arrayIndex, std::span(&aTexture, 1)); +} + +void VulkanParameterBlock::setBinding(uint32_t idx, uint32_t firstArrayIndex, std::span> textures) +{ + assert(m_layout->bindings().at(idx).type == BindingType::sampledTexture); + assert(firstArrayIndex + textures.size() <= m_layout->bindings().at(idx).count); + + std::vector descriptorImageInfos; + descriptorImageInfos.reserve(textures.size()); + auto& usedTextures = m_usedTextures.at(idx); + + for (uint32_t i = 0; const auto& texturePtr : textures) { + auto texture = std::dynamic_pointer_cast(texturePtr); + assert(texture); + + descriptorImageInfos.push_back(vk::DescriptorImageInfo{} + .setImageView(texture->vkImageView()) + .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal)); + usedTextures.insert_or_assign(firstArrayIndex + i, UsedResource{ + .resource = texture, + .binding = m_layout->bindings().at(idx) + }); + ++i; + } auto writeDescriptorSet = vk::WriteDescriptorSet{} .setDstSet(m_descriptorSet) .setDstBinding(idx) - .setDstArrayElement(0) - .setDescriptorCount(1) + .setDstArrayElement(firstArrayIndex) + .setDescriptorCount(static_cast(descriptorImageInfos.size())) .setDescriptorType(vk::DescriptorType::eSampledImage) - .setImageInfo(descriptorImageInfo); + .setImageInfo(descriptorImageInfos); m_device->vkDevice().updateDescriptorSets(writeDescriptorSet, {}); - - m_usedTextures.insert(std::make_pair(texture, m_layout->bindings().at(idx))); } void VulkanParameterBlock::setBinding(uint32_t idx, const std::shared_ptr& aSampler) @@ -100,8 +130,45 @@ void VulkanParameterBlock::setBinding(uint32_t idx, const std::shared_ptrvkDevice().updateDescriptorSets(writeDescriptorSet, {}); + auto& usedSamplers = m_usedSamplers.at(idx); + usedSamplers.insert_or_assign(0, UsedResource{ + .resource = sampler, + .binding = m_layout->bindings().at(idx) + }); +} - m_usedSampler.insert(std::make_pair(sampler, m_layout->bindings().at(idx))); +void VulkanParameterBlock::clearBinding(uint32_t idx, uint32_t arrayIndex) +{ + clearBinding(idx, arrayIndex, 1); } +void VulkanParameterBlock::clearBinding(uint32_t idx, uint32_t firstArrayIndex, uint32_t count) +{ + assert(count > 0); + assert(firstArrayIndex + count <= m_layout->bindings().at(idx).count); + + auto eraseBindingRange = [firstArrayIndex, count](std::unordered_map>& resources) { + std::erase_if(resources, [firstArrayIndex, count](const auto& entry) { + return entry.first >= firstArrayIndex && + entry.first < firstArrayIndex + count; + }); + }; + + switch (m_layout->bindings().at(idx).type) + { + case BindingType::constantBuffer: + case BindingType::structuredBuffer: + eraseBindingRange(m_usedBuffers.at(idx)); + break; + case BindingType::sampledTexture: + eraseBindingRange(m_usedTextures.at(idx)); + break; + case BindingType::sampler: + eraseBindingRange(m_usedSamplers.at(idx)); + break; + default: + std::unreachable(); + } } + +} // namespace gfx diff --git a/src/Vulkan/VulkanParameterBlock.hpp b/src/Vulkan/VulkanParameterBlock.hpp index 071cd008..f2f0e876 100644 --- a/src/Vulkan/VulkanParameterBlock.hpp +++ b/src/Vulkan/VulkanParameterBlock.hpp @@ -18,6 +18,10 @@ #include "Vulkan/VulkanSampler.hpp" #include "Vulkan/VulkanParameterBlockLayout.hpp" +#include +#include +#include + namespace gfx { @@ -25,6 +29,14 @@ class VulkanDevice; class VulkanParameterBlock : public ParameterBlock { +public: + template + struct UsedResource + { + std::shared_ptr resource; + ParameterBlockBinding binding; + }; + public: VulkanParameterBlock() = default; VulkanParameterBlock(const VulkanParameterBlock&) = delete; @@ -35,14 +47,21 @@ class VulkanParameterBlock : public ParameterBlock inline std::shared_ptr layout() const override { return m_layout; } void setBinding(uint32_t idx, const std::shared_ptr&) override; + void setBinding(uint32_t idx, const std::shared_ptr&) override; + void setBinding(uint32_t idx, uint32_t arrayIndex, const std::shared_ptr&) override; + void setBinding(uint32_t idx, uint32_t firstArrayIndex, std::span>) override; + void setBinding(uint32_t idx, const std::shared_ptr&) override; + void clearBinding(uint32_t idx, uint32_t arrayIndex) override; + void clearBinding(uint32_t idx, uint32_t firstArrayIndex, uint32_t count) override; + inline const vk::DescriptorSet& descriptorSet() const { return m_descriptorSet; } - inline const std::map, ParameterBlockBinding>& usedBuffers() const { return m_usedBuffers; } - inline const std::map, ParameterBlockBinding>& usedTextures() const { return m_usedTextures; } - inline const std::map, ParameterBlockBinding>& usedSamplers() const { return m_usedSampler; } + inline auto usedBuffers() const { return m_usedBuffers | std::views::transform([](const auto& resources) { return resources | std::views::values; }) | std::views::join; } + inline auto usedTextures() const { return m_usedTextures | std::views::transform([](const auto& resources) { return resources | std::views::values; }) | std::views::join; } + inline auto usedSamplers() const { return m_usedSamplers | std::views::transform([](const auto& resources) { return resources | std::views::values; }) | std::views::join; } ~VulkanParameterBlock() override = default; @@ -53,9 +72,9 @@ class VulkanParameterBlock : public ParameterBlock vk::DescriptorSet m_descriptorSet; - std::map, ParameterBlockBinding> m_usedBuffers; - std::map, ParameterBlockBinding> m_usedTextures; - std::map, ParameterBlockBinding> m_usedSampler; + std::vector>> m_usedBuffers; + std::vector>> m_usedTextures; + std::vector>> m_usedSamplers; public: VulkanParameterBlock& operator=(const VulkanParameterBlock&) = delete; diff --git a/src/Vulkan/VulkanParameterBlockLayout.cpp b/src/Vulkan/VulkanParameterBlockLayout.cpp index e2a09638..d3d06bb4 100644 --- a/src/Vulkan/VulkanParameterBlockLayout.cpp +++ b/src/Vulkan/VulkanParameterBlockLayout.cpp @@ -8,9 +8,13 @@ */ #include "Vulkan/VulkanParameterBlockLayout.hpp" +#include "Graphics/Enums.hpp" #include "Vulkan/VulkanEnums.hpp" #include "Vulkan/VulkanDevice.hpp" +#include +#include + namespace gfx { @@ -20,16 +24,36 @@ VulkanParameterBlockLayout::VulkanParameterBlockLayout(const VulkanDevice* devic assert(m_device); std::vector vkBindings; + std::vector bindingFlags; + vkBindings.reserve(desc.bindings.size()); + bindingFlags.reserve(desc.bindings.size()); + for (uint32_t i = 0; const auto& binding : desc.bindings) { + assert(binding.count > 0); + assert(binding.type == BindingType::sampledTexture || binding.count == 1); + vkBindings.push_back(vk::DescriptorSetLayoutBinding{} - .setBinding(i) + .setBinding(i++) .setDescriptorType(toVkDescriptorType(binding.type)) - .setDescriptorCount(1) + .setDescriptorCount(binding.count) .setStageFlags(toVkShaderStageFlags(binding.usages))); - i++; + + if (binding.count > 1) + bindingFlags.push_back(vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending | vk::DescriptorBindingFlagBits::ePartiallyBound); + else + bindingFlags.emplace_back(); } + + auto bindingFlagsCreateInfo = vk::DescriptorSetLayoutBindingFlagsCreateInfo{} + .setBindingFlags(bindingFlags); + auto descriptorSetLayoutCreateInfo = vk::DescriptorSetLayoutCreateInfo{} + .setPNext(&bindingFlagsCreateInfo) .setBindings(vkBindings); + + if (std::ranges::any_of(bindingFlags, [](const vk::DescriptorBindingFlags& e) -> bool { return static_cast(e);})) + descriptorSetLayoutCreateInfo.setFlags(vk::DescriptorSetLayoutCreateFlagBits::eUpdateAfterBindPool); + m_vkDescriptorSetLayout = m_device->vkDevice().createDescriptorSetLayout(descriptorSetLayoutCreateInfo); } @@ -38,4 +62,4 @@ VulkanParameterBlockLayout::~VulkanParameterBlockLayout() m_device->vkDevice().destroyDescriptorSetLayout(m_vkDescriptorSetLayout); } -} +} // namespace gfx diff --git a/src/Vulkan/VulkanParameterBlockPool.cpp b/src/Vulkan/VulkanParameterBlockPool.cpp index 54b2ffcc..3827d09c 100644 --- a/src/Vulkan/VulkanParameterBlockPool.cpp +++ b/src/Vulkan/VulkanParameterBlockPool.cpp @@ -34,6 +34,9 @@ VulkanParameterBlockPool::VulkanParameterBlockPool(const VulkanDevice* device, c .setMaxSets(std::accumulate(descriptor.maxBindingCount.begin(), descriptor.maxBindingCount.end(), 0, [](auto acc, const auto& kv) { return acc + kv.second; })) .setPoolSizes(poolSizes); + if (descriptor.updateAfterBind) + descriptorPoolCreateInfo.setFlags(vk::DescriptorPoolCreateFlagBits::eUpdateAfterBind); + m_descriptorPool = std::shared_ptr( new vk::DescriptorPool(m_device->vkDevice().createDescriptorPool(descriptorPoolCreateInfo)), [device=m_device](vk::DescriptorPool* pool){ diff --git a/tests/test_descriptor_operator.cpp b/tests/test_descriptor_operator.cpp index d0c87fd3..32af4b2e 100644 --- a/tests/test_descriptor_operator.cpp +++ b/tests/test_descriptor_operator.cpp @@ -90,11 +90,24 @@ TEST(descriptor_operator, parameter_block_layout_descriptor) expectDescriptorComparableInMap(lhs, rhs); } +TEST(descriptor_operator, parameter_block_binding) +{ + gfx::ParameterBlockBinding lhs { + .type = gfx::BindingType::sampledTexture, + .usages = gfx::BindingUsage::fragmentRead, + .count = 1 + }; + gfx::ParameterBlockBinding rhs = lhs; + rhs.count = 8; + + expectDescriptorComparableInMap(lhs, rhs); +} + TEST(descriptor_operator, parameter_block_pool_descriptor) { gfx::ParameterBlockPool::Descriptor lhs {}; gfx::ParameterBlockPool::Descriptor rhs = lhs; - rhs.maxBindingCount[gfx::BindingType::constantBuffer] = lhs.maxBindingCount[gfx::BindingType::constantBuffer] + 1; + rhs.updateAfterBind = true; expectDescriptorComparableInMap(lhs, rhs); } From 7d75af230070b475c6dedb8906d40d62c85d6928 Mon Sep 17 00:00:00 2001 From: thomas Date: Wed, 6 May 2026 01:03:24 -0700 Subject: [PATCH 2/2] enable host query reset feature in VulkanDevice descriptor indexing --- src/Vulkan/VulkanDevice.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Vulkan/VulkanDevice.cpp b/src/Vulkan/VulkanDevice.cpp index 79032c09..94293456 100644 --- a/src/Vulkan/VulkanDevice.cpp +++ b/src/Vulkan/VulkanDevice.cpp @@ -53,8 +53,12 @@ VulkanDevice::VulkanDevice(const VulkanInstance* instance, const VulkanPhysicalD .setTimelineSemaphore(vk::True) .setPNext(dynamicRenderingFeature); + auto hostQueryResetFeature = vk::PhysicalDeviceHostQueryResetFeatures{} + .setHostQueryReset(vk::True) + .setPNext(&timelineSemaphoreFeature); + auto descriptorIndexingFeatures = vk::PhysicalDeviceDescriptorIndexingFeatures{} - .setPNext(&timelineSemaphoreFeature) + .setPNext(&hostQueryResetFeature) .setShaderSampledImageArrayNonUniformIndexing(vk::True) .setDescriptorBindingSampledImageUpdateAfterBind(vk::True) .setDescriptorBindingUpdateUnusedWhilePending(vk::True)