diff --git a/example/test_thread/main.cpp b/example/test_thread/main.cpp index 4ee9aa3..7ea8f3f 100644 --- a/example/test_thread/main.cpp +++ b/example/test_thread/main.cpp @@ -66,32 +66,32 @@ int main(int argc, char* argv[]) { // new_widget()[ new_widget()->set_radius(40.0f) // ]->circle() | align(alignment::center_left) + ], + new_widget()[ + new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), + // 暗角效果 (使用新的 vignette_widget) + new_widget()->set_radius(0.5f).set_softness(0.5f) + ], + new_widget()[ + new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), + // 色差效果 (使用新的 chromatic_aberration_widget) + new_widget()->set_offset(vec2f_t{0.01f, 0.0f}) + ], + new_widget()[ + new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), + // 噪点效果 (使用新的 noise_widget) + new_widget()->set_amount(0.2f).set_grain_size(1.0f) + ], + new_widget()[ + new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), + // 颜色调整效果 (使用新的 color_adjust_widget) + new_widget()->set_brightness(0.1f).set_contrast(1.5f).set_saturation(1.5f).set_gamma(2.2f) + ], + new_widget()[ + new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), + // 色调效果 (使用新的 color_tint_widget) + new_widget()->set_tint_color(color{1.0f, 0.4f, 0.4f, 0.3f}).set_blend_mode(4) // Overlay 模式 ] - // new_widget()[ - // new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), - // // 暗角效果 (使用新的 vignette_widget) - // new_widget()->set_radius(0.5f).set_softness(0.5f) - // ], - // new_widget()[ - // new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), - // // 色差效果 (使用新的 chromatic_aberration_widget) - // new_widget()->set_offset(vec2f_t{0.01f, 0.0f}) - // ], - // new_widget()[ - // new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), - // // 噪点效果 (使用新的 noise_widget) - // new_widget()->set_amount(0.2f).set_grain_size(1.0f) - // ], - // new_widget()[ - // new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), - // // 颜色调整效果 (使用新的 color_adjust_widget) - // new_widget()->set_brightness(0.1f).set_contrast(1.5f).set_saturation(1.5f).set_gamma(2.2f) - // ], - // new_widget()[ - // new_widget()->texture_id(texture_id).source_size(tex_size).scale(scale_mode::contain), - // // 色调效果 (使用新的 color_tint_widget) - // new_widget()->set_tint_color(color{1.0f, 0.4f, 0.4f, 0.3f}).set_blend_mode(4) // Overlay 模式 - // ] ]; // 通过主窗口设置根控件 diff --git a/plans/dynamic_descriptor_layout_design.md b/plans/dynamic_descriptor_layout_design.md new file mode 100644 index 0000000..c4d9a6e --- /dev/null +++ b/plans/dynamic_descriptor_layout_design.md @@ -0,0 +1,953 @@ +# 动态描述符集布局创建机制设计文档 + +## 1. 问题背景 + +### 1.1 现有问题 + +当前 `custom_shader_widget_renderer` 使用硬编码的固定描述符集布局: + +```cpp +// custom_shader_widget_renderer.cpp:891-906 +std::array bindings = {{ + vk::DescriptorSetLayoutBinding{ + 0, // binding 0: 源纹理 + vk::DescriptorType::eCombinedImageSampler, + 1, + vk::ShaderStageFlagBits::eFragment, + nullptr + }, + vk::DescriptorSetLayoutBinding{ + 1, // binding 1: 遮罩纹理 + vk::DescriptorType::eCombinedImageSampler, + 1, + vk::ShaderStageFlagBits::eFragment, + nullptr + } +}}; +``` + +这导致 `blur_widget` 等需要 `UniformBuffer` 绑定的着色器无法正常工作: +- `blur.frag.glsl` 声明 `layout(binding = 1) uniform BlurParams { ... }` +- 但渲染器的描述符集布局在 binding 1 声明的是 `CombinedImageSampler` +- Vulkan 验证层会报错:描述符类型不匹配 + +### 1.2 已有基础设施 + +| 组件 | 位置 | 功能 | +|------|------|------| +| `shader_resource_manager::get_or_create_descriptor_set_layout()` | shader_resource_manager.h:258 | 描述符布局缓存(按 bindings 哈希) | +| `shader_binding_info` | vulkan_common.h:33 | 描述绑定点、类型、数量、阶段 | +| `custom_shader_widget_command::bindings` | render_command.h:273 | Widget 传递绑定信息 | +| `descriptor_update_context::bind_uniform_buffer` | custom_shader_widget_types.h:151 | UBO 绑定回调 | + +--- + +## 2. 架构设计 + +### 2.1 设计目标 + +1. **统一动态布局**:所有描述符集布局均通过 `shader_resource_manager` 动态创建 +2. **缓存复用**:利用 `shader_resource_manager` 的布局缓存避免重复创建 +3. **空绑定零开销**:当 `bindings` 为空时,Pipeline 不使用描述符集(`setLayoutCount = 0`) +4. **统一管理**:Pipeline 和描述符布局的生命周期由 `shader_resource_manager` 统一管理 + +### 2.2 核心类图 + +```mermaid +classDiagram + class custom_shader_widget_renderer { + -shader_resource_manager* shader_res_mgr_ + -unordered_map~custom_pipeline_key_t, pipeline_resources_t~ pipeline_cache_ + +render_impl() + +render_post_process() + -get_or_create_pipeline() + -create_pipeline() + -get_or_create_layout_for_command() + -allocate_and_update_descriptor_set_dynamic() + } + + class custom_pipeline_key_t { + +uint32_t shader_id + +custom_shader_render_mode render_mode + +VkRenderPass render_pass + +size_t layout_hash + +operator==() + } + + class shader_resource_manager { + +get_or_create_descriptor_set_layout() + +allocate_descriptor_set() + +allocate_uniform_buffer() + +update_uniform_buffer_descriptor() + } + + class shader_binding_info { + +uint32_t binding + +vk::DescriptorType type + +uint32_t count + +vk::ShaderStageFlags stage + } + + class custom_shader_widget_command { + +span~shader_binding_info~ bindings + +update_descriptors_fn update_descriptors + } + + custom_shader_widget_renderer --> shader_resource_manager : uses + custom_shader_widget_renderer --> custom_pipeline_key_t : cache key + custom_shader_widget_renderer ..> custom_shader_widget_command : processes + custom_shader_widget_command --> shader_binding_info : contains + shader_resource_manager --> shader_binding_info : uses +``` + +--- + +## 3. Pipeline 缓存键扩展设计 + +### 3.1 扩展 `custom_pipeline_key_t` + +**目标**:在缓存键中包含描述符布局哈希,确保不同布局的着色器使用不同的 Pipeline。 + +```cpp +// custom_shader_widget_renderer.h + +/// @brief Pipeline 缓存键 +struct custom_pipeline_key_t { + uint32_t shader_id; ///< 着色器唯一标识 + custom_shader_render_mode render_mode; ///< 渲染模式 + VkRenderPass render_pass; ///< 渲染通道 + size_t layout_hash; ///< 描述符布局哈希(新增) + + auto operator==(const custom_pipeline_key_t& other) const -> bool { + return shader_id == other.shader_id && + render_mode == other.render_mode && + render_pass == other.render_pass && + layout_hash == other.layout_hash; // 新增比较 + } +}; + +/// @brief Pipeline 键哈希函数 +struct custom_pipeline_key_hash { + auto operator()(const custom_pipeline_key_t& k) const noexcept -> std::size_t { + std::size_t h1 = std::hash{}(k.shader_id); + std::size_t h2 = std::hash{}(static_cast(k.render_mode)); + std::size_t h3 = std::hash{}(k.render_pass); + std::size_t h4 = k.layout_hash; // 新增 + return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3); // 新增 + } +}; +``` + +### 3.2 布局哈希计算 + +复用 `shader_resource_manager` 中已有的 `layout_cache_key_hash`: + +```cpp +// custom_shader_widget_renderer.cpp + +/// @brief 计算绑定信息的哈希值 +[[nodiscard]] static auto compute_bindings_hash( + std::span bindings +) -> size_t { + if (bindings.empty()) { + return 0; // 空绑定表示无描述符集,哈希为0 + } + + // 使用与 shader_resource_manager 相同的哈希算法 + size_t h = bindings.size(); + for (const auto& b : bindings) { + h ^= std::hash{}(static_cast(b.binding) + + 0x9e3779b9 + (h << 6) + (h >> 2)); + h ^= std::hash{}(static_cast(b.type) + + 0x9e3779b9 + (h << 6) + (h >> 2)); + h ^= std::hash{}(static_cast(b.count) + + 0x9e3779b9 + (h << 6) + (h >> 2)); + h ^= std::hash{}(static_cast(b.stage) + + 0x9e3779b9 + (h << 6) + (h >> 2)); + } + return h; +} +``` + +--- + +## 4. 描述符集布局动态创建流程 + +### 4.1 流程图 + +```mermaid +flowchart TD + A[render_post_process 调用] --> B{command.bindings 为空?} + B -->|是| C[无描述符集
setLayoutCount = 0] + B -->|否| D[计算 bindings 哈希] + + D --> E[调用 get_or_create_layout_for_command] + E --> F[shader_res_mgr_->get_or_create_descriptor_set_layout] + F --> G{布局已缓存?} + G -->|是| H[返回缓存的布局] + G -->|否| I[创建新布局并缓存] + I --> H + + C --> J[返回 VK_NULL_HANDLE, hash=0] + H --> K[构建 pipeline_key 包含 layout_hash] + J --> K + + K --> L[get_or_create_pipeline] + L --> M{Pipeline 已缓存?} + M -->|是| N[返回缓存的 Pipeline] + M -->|否| O[create_pipeline
根据布局是否为空决定 setLayoutCount] + O --> N + + N --> P{desc_layout 为空?} + P -->|是| Q[跳过描述符绑定] + P -->|否| R[allocate_and_update_descriptor_set_dynamic] + Q --> S[渲染] + R --> S +``` + +### 4.2 新增方法 `get_or_create_layout_for_command` + +```cpp +// custom_shader_widget_renderer.h (private section) + +/// @brief 根据命令获取或创建描述符集布局 +/// @param bindings 绑定信息 +/// @return <布局(可能为 VK_NULL_HANDLE), 哈希值> +[[nodiscard]] auto get_or_create_layout_for_command( + std::span bindings +) -> std::pair; +``` + +```cpp +// custom_shader_widget_renderer.cpp + +auto custom_shader_widget_renderer::get_or_create_layout_for_command( + std::span bindings +) -> std::pair { + + // 空绑定表示无描述符集 + if (bindings.empty()) { + return {VK_NULL_HANDLE, 0}; + } + + // 计算哈希 + size_t hash = compute_bindings_hash(bindings); + + // 委托给 shader_resource_manager 获取或创建布局 + auto layout = shader_res_mgr_->get_or_create_descriptor_set_layout(bindings); + + return {layout, hash}; +} +``` + +### 4.3 修改 `create_pipeline_layout` + +统一使用动态布局创建,支持空描述符集: + +```cpp +// custom_shader_widget_renderer.h (private section) + +/// @brief 使用指定描述符集布局创建 Pipeline 布局 +/// @param desc_layout 描述符集布局(可为 VK_NULL_HANDLE) +/// @return Pipeline 布局句柄 +[[nodiscard]] auto create_pipeline_layout_with( + vk::DescriptorSetLayout desc_layout +) -> device_handle_t; +``` + +```cpp +// custom_shader_widget_renderer.cpp + +auto custom_shader_widget_renderer::create_pipeline_layout_with( + vk::DescriptorSetLayout desc_layout +) -> device_handle_t { + auto vk_device = device_->get_handle(); + + vk::PushConstantRange pc_range{ + vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + 0, + sizeof(custom_shader_push_constants) + }; + + vk::PipelineLayoutCreateInfo pipeline_layout_info{}; + + // 根据描述符布局是否存在决定 setLayoutCount + if (desc_layout) { + pipeline_layout_info.setLayoutCount = 1; + pipeline_layout_info.pSetLayouts = &desc_layout; + } else { + // 无描述符集布局 + pipeline_layout_info.setLayoutCount = 0; + pipeline_layout_info.pSetLayouts = nullptr; + } + + pipeline_layout_info.pushConstantRangeCount = 1; + pipeline_layout_info.pPushConstantRanges = &pc_range; + + auto [result, pl] = vk_device.createPipelineLayout(pipeline_layout_info); + if (result != vk::Result::eSuccess) { + throw std::runtime_error("Failed to create pipeline layout"); + } + + return device_handle_t( + pl, device_deleter{vk_device} + ); +} +``` + +**注意**:初始化时不再创建默认布局,渲染器的 `initialize()` 方法简化为: + +```cpp +void custom_shader_widget_renderer::initialize(vk::RenderPass render_pass) { + if (initialized_) return; + + render_pass_ = render_pass; + initialized_ = true; + + // 不再创建默认布局,所有布局按需动态创建 +} +``` + +### 4.4 修改 `pipeline_resources_t` + +扩展 `pipeline_resources_t` 以存储动态创建的布局: + +```cpp +// render/vulkan/vk_handle_ex.h 或 custom_shader_widget_renderer.h + +/// @brief Pipeline 资源结构 +struct pipeline_resources_t { + device_handle_t pipeline; + device_handle_t layout; ///< 新增:关联的 Pipeline 布局 + vk::DescriptorSetLayout desc_layout; ///< 新增:关联的描述符集布局(可为 VK_NULL_HANDLE) +}; +``` + +### 4.5 修改 `create_pipeline` + +```cpp +// custom_shader_widget_renderer.h + +/// @brief 创建 Pipeline +/// @param command 渲染命令 +/// @param desc_layout 描述符集布局 +/// @param layout_hash 布局哈希 +/// @return Pipeline 资源 +[[nodiscard]] auto create_pipeline( + const custom_shader_widget_command& command, + vk::DescriptorSetLayout desc_layout, + size_t layout_hash +) -> pipeline_resources_t; +``` + +```cpp +// custom_shader_widget_renderer.cpp + +auto custom_shader_widget_renderer::create_pipeline( + const custom_shader_widget_command& command, + vk::DescriptorSetLayout desc_layout, + size_t layout_hash +) -> pipeline_resources_t { + pipeline_resources_t result; + + // ... 着色器模块创建代码不变 ... + + // 创建专用的 Pipeline 布局 + auto pl_handle = create_pipeline_layout_with(desc_layout); + + // ... Pipeline 创建配置不变 ... + + vk::GraphicsPipelineCreateInfo pipeline_info{}; + // ... + pipeline_info.layout = pl_handle.get(); // 使用新创建的布局 + // ... + + auto [pipe_result, pipeline] = vk_device.createGraphicsPipeline(nullptr, pipeline_info); + + // ... 错误处理 ... + + result.pipeline = device_handle_t(pipeline, ...); + result.layout = std::move(pl_handle); + result.desc_layout = desc_layout; + + return result; +} +``` + +### 4.6 修改 `get_or_create_pipeline` + +```cpp +auto custom_shader_widget_renderer::get_or_create_pipeline( + const custom_shader_widget_command& command +) -> vk::Pipeline { + + // 1. 获取或创建描述符集布局 + auto [desc_layout, layout_hash] = get_or_create_layout_for_command(command.bindings); + + // 2. 构建缓存键(包含布局哈希) + custom_pipeline_key_t key{ + command.shader_id, + command.render_mode, + static_cast(render_pass_), + layout_hash // 新增 + }; + + // 3. 查找缓存 + auto it = pipeline_cache_.find(key); + if (it != pipeline_cache_.end()) { + return it->second.pipeline.get(); + } + + // 4. 创建新 Pipeline(传递布局) + auto resources = create_pipeline(command, desc_layout, layout_hash); + if (resources.pipeline) { + pipeline_cache_[key] = std::move(resources); + return pipeline_cache_[key].pipeline.get(); + } + + return nullptr; +} +``` + +--- + +## 5. 描述符集分配与更新机制 + +### 5.1 新增方法 `allocate_and_update_descriptor_set_dynamic` + +```cpp +// custom_shader_widget_renderer.h (private section) + +/// @brief 动态分配并更新描述符集 +/// @param command 渲染命令 +/// @param source_view 源纹理视图 +/// @param source_sampler 源采样器 +/// @param desc_layout 描述符集布局 +/// @param descriptor_pool 描述符池 +/// @return 分配的描述符集 +[[nodiscard]] auto allocate_and_update_descriptor_set_dynamic( + const custom_shader_widget_command& command, + vk::ImageView source_view, + vk::Sampler source_sampler, + vk::DescriptorSetLayout desc_layout, + vk::DescriptorPool descriptor_pool +) -> vk::DescriptorSet; +``` + +### 5.2 实现 + +```cpp +auto custom_shader_widget_renderer::allocate_and_update_descriptor_set_dynamic( + const custom_shader_widget_command& command, + vk::ImageView source_view, + vk::Sampler source_sampler, + vk::DescriptorSetLayout desc_layout, + vk::DescriptorPool descriptor_pool +) -> vk::DescriptorSet { + + // 空布局表示不需要描述符集 + if (!desc_layout) { + return VK_NULL_HANDLE; + } + + if (!device_ || !descriptor_pool) { + return VK_NULL_HANDLE; + } + + // 1. 分配描述符集 + vk::DescriptorSetAllocateInfo alloc_info{}; + alloc_info.descriptorPool = descriptor_pool; + alloc_info.descriptorSetCount = 1; + alloc_info.pSetLayouts = &desc_layout; + + auto [result, sets] = device_->get_handle().allocateDescriptorSets(alloc_info); + if (result != vk::Result::eSuccess) { + std::cerr << "[CUSTOM_SHADER] 描述符集分配失败" << std::endl; + return VK_NULL_HANDLE; + } + + auto desc_set = sets[0]; + + // 2. 动态更新:遍历 bindings 并填充 + std::vector writes; + std::vector image_infos; + std::vector buffer_infos; + + // 预分配以避免迭代器失效 + image_infos.reserve(command.bindings.size()); + buffer_infos.reserve(command.bindings.size()); + writes.reserve(command.bindings.size()); + + for (const auto& binding : command.bindings) { + vk::WriteDescriptorSet write{}; + write.dstSet = desc_set; + write.dstBinding = binding.binding; + write.dstArrayElement = 0; + write.descriptorCount = binding.count; + write.descriptorType = binding.type; + + switch (binding.type) { + case vk::DescriptorType::eCombinedImageSampler: { + image_infos.push_back(vk::DescriptorImageInfo{ + source_sampler, + source_view, + vk::ImageLayout::eShaderReadOnlyOptimal + }); + write.pImageInfo = &image_infos.back(); + break; + } + + case vk::DescriptorType::eUniformBuffer: + case vk::DescriptorType::eUniformBufferDynamic: { + // UBO 需要通过回调由 widget 填充 + // 这里先分配占位符,由 update_descriptors 回调填充 + // 或者使用默认的全零数据 + auto alloc = shader_res_mgr_->allocate_uniform_buffer(256); + buffer_infos.push_back(vk::DescriptorBufferInfo{ + alloc.buffer, + alloc.offset, + alloc.size + }); + write.pBufferInfo = &buffer_infos.back(); + break; + } + + default: + std::cerr << "[CUSTOM_SHADER] 不支持的描述符类型: " + << static_cast(binding.type) << std::endl; + continue; + } + + writes.push_back(write); + } + + if (!writes.empty()) { + device_->get_handle().updateDescriptorSets(writes, {}); + } + + return desc_set; +} +``` + +### 5.3 修改 `render_post_process` + +```cpp +void custom_shader_widget_renderer::render_post_process( + vk::CommandBuffer cmd, + const custom_shader_widget_command& command, + vk::DescriptorPool descriptor_pool) { + + // 1. 获取源纹理(不变) + vk::ImageView source_view = command.injected_source_view; + vk::Sampler source_sampler = command.injected_source_sampler; + // ... 获取源纹理的逻辑不变 ... + + // 2. 获取描述符布局(可能为 VK_NULL_HANDLE) + auto [desc_layout, layout_hash] = get_or_create_layout_for_command(command.bindings); + + // 3. 获取或创建 Pipeline(布局已包含在缓存键中) + auto pipeline = get_or_create_pipeline(command); + if (!pipeline) { + std::cerr << "[CUSTOM_SHADER] Pipeline 创建失败" << std::endl; + return; + } + + // 4. 获取关联的 Pipeline 布局 + custom_pipeline_key_t key{ + command.shader_id, + command.render_mode, + static_cast(render_pass_), + layout_hash + }; + auto& resources = pipeline_cache_[key]; + + // 5. 绑定 Pipeline + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + + // 6. 分配并更新描述符集(仅当有描述符布局时) + vk::DescriptorSet descriptor_set = VK_NULL_HANDLE; + if (desc_layout) { + descriptor_set = allocate_and_update_descriptor_set_dynamic( + command, + source_view, + source_sampler, + desc_layout, + descriptor_pool + ); + + if (descriptor_set) { + cmd.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + resources.layout.get(), + 0, + {descriptor_set}, + {} + ); + } + } + // 若 desc_layout 为空,则跳过描述符绑定 + + // 7. 调用 widget 的描述符更新回调(仅当有描述符集时) + if (command.update_descriptors && descriptor_set) { + descriptor_update_context ctx{}; + ctx.desc_set = descriptor_set; + ctx.source_view = source_view; + ctx.source_sampler = source_sampler; + + ctx.bind_sampler = [this, descriptor_set](uint32_t binding, + vk::ImageView view, + vk::Sampler sampler) { + if (!device_) return; + vk::DescriptorImageInfo image_info{ + sampler, view, vk::ImageLayout::eShaderReadOnlyOptimal + }; + vk::WriteDescriptorSet write{ + descriptor_set, binding, 0, 1, + vk::DescriptorType::eCombinedImageSampler, + &image_info, nullptr, nullptr + }; + device_->get_handle().updateDescriptorSets({write}, {}); + }; + + ctx.bind_uniform_buffer = [this, descriptor_set](uint32_t binding, + const void* data, + size_t size) { + if (!shader_res_mgr_) return; + auto alloc = shader_res_mgr_->allocate_uniform_buffer(size); + if (alloc.mapped_ptr) { + std::memcpy(alloc.mapped_ptr, data, size); + } + shader_res_mgr_->update_uniform_buffer_descriptor( + descriptor_set, binding, alloc.buffer, alloc.offset, alloc.size + ); + }; + + ctx.get_sampler = [this](const sampler_config& config) -> vk::Sampler { + if (!res_mgr_) return nullptr; + return res_mgr_->get_sampler_cache().get(config); + }; + + command.update_descriptors(ctx); + } + + // 8. 推送 Push Constants 和绘制(不变) + // ... +} +``` + +--- + +## 6. 空绑定策略 + +### 6.1 空 bindings 处理 + +当 `command.bindings` 为空时,表示着色器不需要任何描述符绑定: + +```cpp +auto custom_shader_widget_renderer::get_or_create_layout_for_command( + std::span bindings +) -> std::pair { + + if (bindings.empty()) { + // 无描述符集,返回 VK_NULL_HANDLE + return {VK_NULL_HANDLE, 0}; + } + + // ... 动态创建逻辑 ... +} +``` + +**Pipeline 布局创建**:当 `desc_layout` 为 `VK_NULL_HANDLE` 时,`setLayoutCount = 0`。 + +### 6.2 Mask 模式处理 + +Mask 模式需要显式声明其绑定信息: + +```cpp +// mask_shader_spec.h + +template +struct mask_shader_spec { + static constexpr uint32_t shader_id = hash_compile_time("mask_shader"); + + // Mask 模式需要两个纹理绑定 + static constexpr std::array bindings = {{ + {0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, + {1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment} + }}; + + static auto get_bindings() -> std::span { + return bindings; + } + + // ... +}; +``` + +```cpp +void custom_shader_widget_renderer::render_mask_impl(...) { + // Mask 模式通过 command.bindings 获取布局 + auto [desc_layout, layout_hash] = get_or_create_layout_for_command(command.bindings); + + // 分配描述符集 + auto descriptor_set = allocate_and_update_descriptor_set_dynamic( + command, + mask_params.children_texture_view, + children_sampler, + desc_layout, + descriptor_pool + ); + + // 更新 mask 特定绑定 + if (descriptor_set && mask_params.mask_texture_view) { + // 更新 binding 1 为 mask 纹理 + vk::DescriptorImageInfo mask_info{ + mask_sampler, + mask_params.mask_texture_view, + vk::ImageLayout::eShaderReadOnlyOptimal + }; + vk::WriteDescriptorSet write{ + descriptor_set, 1, 0, 1, + vk::DescriptorType::eCombinedImageSampler, + &mask_info, nullptr, nullptr + }; + device_->get_handle().updateDescriptorSets({write}, {}); + } + + // ... +} +``` + +### 6.3 Procedural 模式 + +Procedural 模式通常不需要描述符绑定,其 `get_bindings()` 返回空 span: + +```cpp +// gradient_shader_spec.h (示例) + +struct gradient_shader_spec { + static constexpr uint32_t shader_id = hash_compile_time("gradient_shader"); + + // Procedural 模式无描述符绑定 + static auto get_bindings() -> std::span { + return {}; // 空绑定 + } + + // ... +}; +``` + +Pipeline 将使用 `setLayoutCount = 0` 创建。 + +--- + +## 7. 接口变更清单 + +### 7.1 `custom_shader_widget_renderer.h` 变更 + +| 类型 | 名称 | 变更 | +|------|------|------| +| 结构体 | `custom_pipeline_key_t` | 新增 `layout_hash` 字段 | +| 结构体 | `custom_pipeline_key_hash` | 修改哈希计算包含 `layout_hash` | +| 成员变量 | `descriptor_set_layout_` | **删除**:不再需要成员变量存储布局 | +| 成员变量 | `pipeline_layout_` | **删除**:布局存储在 `pipeline_resources_t` 中 | +| 方法 | `get_or_create_layout_for_command()` | 新增:动态获取布局(返回可能为 null) | +| 方法 | `create_pipeline_layout()` | **删除**:使用 `create_pipeline_layout_with()` 替代 | +| 方法 | `create_pipeline_layout_with()` | 新增:支持空布局的 Pipeline 布局创建 | +| 方法 | `allocate_and_update_descriptor_set_dynamic()` | 新增:动态描述符分配 | +| 方法 | `create_pipeline()` | 修改签名:增加布局参数 | + +### 7.2 `pipeline_resources_t` 变更 + +| 类型 | 名称 | 变更 | +|------|------|------| +| 字段 | `layout` | 新增:Pipeline 布局句柄 | +| 字段 | `desc_layout` | 新增:描述符集布局引用(可为 VK_NULL_HANDLE) | + +### 7.3 删除的方法/成员 + +| 名称 | 原因 | +|------|------| +| `default_descriptor_set_layout_` | 不再需要默认布局 | +| `allocate_mask_descriptor_set()` | 使用统一的 `allocate_and_update_descriptor_set_dynamic()` | +| `allocate_and_update_descriptor_set()` | 使用统一的动态版本替代 | + +### 7.4 不变的接口 + +| 接口 | 原因 | +|------|------| +| `render_impl()` | 分发逻辑不变 | +| `descriptor_update_context` | 回调接口不变 | + +--- + +## 8. 实现注意事项 + +### 8.1 线程安全 + +`shader_resource_manager` 的布局缓存已有互斥锁保护: + +```cpp +// shader_resource_manager.h:532 +std::mutex layout_mutex_; ///< Layout 缓存锁 +``` + +渲染器调用时无需额外加锁。 + +### 8.2 生命周期管理 + +| 资源 | 所有者 | 生命周期 | +|------|--------|----------| +| `DescriptorSetLayout` | `shader_resource_manager` | 应用生命周期(全局缓存) | +| `Pipeline` | `custom_shader_widget_renderer::pipeline_cache_` | 渲染器生命周期 | +| `PipelineLayout` | `pipeline_resources_t::layout` | 随 Pipeline 缓存条目销毁 | + +### 8.3 描述符池类型 + +确保描述符池创建时包含足够的 `UniformBuffer` 类型描述符: + +```cpp +// render_pipeline.cpp 或 descriptor_manager.cpp +std::array pool_sizes = {{ + {vk::DescriptorType::eCombinedImageSampler, max_sets * 4}, + {vk::DescriptorType::eUniformBuffer, max_sets * 2}, // 确保有 UBO 类型 + {vk::DescriptorType::eStorageBuffer, max_sets * 2} +}}; +``` + +### 8.4 内存对齐 + +UBO 分配时需遵守 `minUniformBufferOffsetAlignment`: + +```cpp +// shader_resource_manager 已处理对齐 +VkDeviceSize min_uniform_buffer_offset_alignment_ = 256; + +auto allocate_uniform_buffer(size_t size) -> uniform_buffer_allocation { + // 对齐到 minUniformBufferOffsetAlignment + size_t aligned_size = (size + alignment - 1) & ~(alignment - 1); + // ... +} +``` + +--- + +## 9. 测试方案 + +### 9.1 单元测试 + +```cpp +// test_custom_shader_renderer.cpp + +TEST_CASE("Pipeline 缓存键包含布局哈希") { + custom_pipeline_key_t key1{1, custom_shader_render_mode::post_process, render_pass, 0}; + custom_pipeline_key_t key2{1, custom_shader_render_mode::post_process, render_pass, 12345}; + + REQUIRE(key1 != key2); // 布局哈希不同应产生不同的键 +} + +TEST_CASE("空 bindings 返回 VK_NULL_HANDLE") { + auto [layout, hash] = renderer.get_or_create_layout_for_command({}); + + REQUIRE(layout == VK_NULL_HANDLE); + REQUIRE(hash == 0); +} + +TEST_CASE("空 bindings 创建无描述符集的 Pipeline 布局") { + std::span empty_bindings; + auto [desc_layout, hash] = renderer.get_or_create_layout_for_command(empty_bindings); + + // 创建 Pipeline 布局 + auto pl_layout = renderer.create_pipeline_layout_with(desc_layout); + + // 验证布局有效但无描述符集 + REQUIRE(pl_layout.get() != VK_NULL_HANDLE); + // Pipeline 布局的 setLayoutCount 应为 0(通过 Vulkan 验证层验证) +} + +TEST_CASE("相同 bindings 返回相同布局") { + std::vector bindings = { + {0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, + {1, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eFragment} + }; + + auto [layout1, hash1] = renderer.get_or_create_layout_for_command(bindings); + auto [layout2, hash2] = renderer.get_or_create_layout_for_command(bindings); + + REQUIRE(layout1 == layout2); + REQUIRE(layout1 != VK_NULL_HANDLE); + REQUIRE(hash1 == hash2); + REQUIRE(hash1 != 0); +} +``` + +### 9.2 集成测试 + +```cpp +TEST_CASE("blur_widget 渲染正确") { + // 1. 创建 blur_widget + auto blur = create_widget(); + blur->set_radius(5.0f); + blur->set_samples(16); + + // 2. 收集渲染命令 + render_collector collector; + blur->collect_render_commands(collector); + + // 3. 验证命令包含正确的 bindings + auto& cmd = collector.get_custom_shader_commands()[0]; + REQUIRE(cmd.bindings.size() == 2); + REQUIRE(cmd.bindings[0].type == vk::DescriptorType::eCombinedImageSampler); + REQUIRE(cmd.bindings[1].type == vk::DescriptorType::eUniformBuffer); + + // 4. 执行渲染(无 Vulkan 验证层错误) + render_tree_executor executor; + executor.execute(/* ... */); +} +``` + +### 9.3 验证层检查清单 + +- [ ] 无 `VUID-VkWriteDescriptorSet-descriptorType-00322` 错误(描述符类型匹配) +- [ ] 无 `VUID-vkCmdBindDescriptorSets-pDescriptorSets-00359` 错误(布局兼容) +- [ ] 无内存泄漏(Pipeline、布局正确销毁) + +--- + +## 10. 实现优先级 + +| 优先级 | 任务 | 依赖 | +|--------|------|------| +| P0 | 扩展 `custom_pipeline_key_t` | 无 | +| P0 | 实现 `get_or_create_layout_for_command()` | 无 | +| P0 | 修改 `create_pipeline()` 签名 | P0 | +| P1 | 实现 `allocate_and_update_descriptor_set_dynamic()` | P0 | +| P1 | 修改 `render_post_process()` | P0, P1 | +| P2 | 添加单元测试 | P0, P1 | +| P2 | 验证 `blur_widget` 工作正常 | 全部 | + +--- + +## 11. 风险与缓解 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| Pipeline 缓存膨胀 | 内存占用增加 | 复用 `shader_resource_manager` 的 LRU 淘汰策略 | +| 布局不兼容 | 渲染错误 | 强制 Pipeline 和描述符集使用相同布局 | +| 回调中 UBO 数据过期 | 渲染错误 | 使用帧内 ring buffer 确保数据有效 | +| 空绑定着色器访问描述符 | 验证层错误 | 着色器规格必须正确声明所有需要的绑定 | + +--- + +## 12. 总结 + +本设计通过以下机制解决 `custom_shader_widget_renderer` 的描述符布局限制: + +1. **Pipeline 缓存键扩展**:添加 `layout_hash` 字段,确保不同布局的着色器使用不同 Pipeline +2. **统一动态布局创建**:利用 `shader_resource_manager` 的缓存机制,按需创建描述符集布局 +3. **空绑定零开销**:当 `bindings` 为空时,Pipeline 使用 `setLayoutCount = 0`,无描述符集开销 +4. **统一更新机制**:`allocate_and_update_descriptor_set_dynamic()` 支持混合类型绑定 + +**核心原则**:所有描述符绑定信息必须由着色器规格(`ShaderSpec::get_bindings()`)显式声明,渲染器不再假设任何默认布局。 + +实现后,`blur_widget` 等需要 `UniformBuffer` 绑定的后处理效果将能够正常工作,同时纯程序化着色器(无需描述符)也能以最小开销运行。 \ No newline at end of file diff --git a/src/render/pipeline/render_tree_executor.cpp b/src/render/pipeline/render_tree_executor.cpp index 26b1dde..cc3b6a4 100644 --- a/src/render/pipeline/render_tree_executor.cpp +++ b/src/render/pipeline/render_tree_executor.cpp @@ -28,8 +28,6 @@ namespace mirage { return; } - std::cerr << "[BACKDROP_DEBUG] 进入 backdrop 模式处理" << std::endl; - auto* current_target = ctx.frame_ctx->current_target(); if (!current_target || !ctx.target_pool) { std::cerr << "[BACKDROP_DEBUG] 错误: current_target=" << current_target @@ -44,42 +42,25 @@ namespace mirage { auto cmd_buf = ctx.frame_ctx->cmd(); auto extent = current_target->extent(); - - std::cerr << "[BACKDROP_DEBUG] 当前目标尺寸: " << extent.width << "x" << extent.height << std::endl; - std::cerr << "[BACKDROP_DEBUG] 命令位置: (" << command.position.x() << ", " << command.position.y() - << "), 大小: (" << command.size.x() << ", " << command.size.y() << ")" << std::endl; - std::cerr << "[BACKDROP_DEBUG] 当前目标状态: " << static_cast(current_target->current_state()) << std::endl; - + if constexpr (std::is_same_v) { // 1. 结束当前 Pass (Offscreen -> Idle) - std::cerr << "[BACKDROP_DEBUG] 步骤1: 结束当前 Pass" << std::endl; auto idle_ctx = std::move(*ctx.frame_ctx).end_offscreen_pass(); // 2. 转换当前目标到 shader_read 状态 - std::cerr << "[BACKDROP_DEBUG] 步骤2: 转换到 shader_read 状态" << std::endl; current_target->transition_to(idle_ctx.cmd(), offscreen_target::state::shader_read); - std::cerr << "[BACKDROP_DEBUG] 当前目标状态(转换后): " << static_cast(current_target->current_state()) << std::endl; - + // 3. 获取临时目标 - std::cerr << "[BACKDROP_DEBUG] 步骤3: 获取临时目标" << std::endl; auto temp_handle = ctx.target_pool->acquire_for_post_effect(extent.width, extent.height); auto* temp_target = temp_handle.get(); if (temp_target) { - std::cerr << "[BACKDROP_DEBUG] 临时目标获取成功, 尺寸: " - << temp_target->extent().width << "x" << temp_target->extent().height << std::endl; - std::cerr << "[BACKDROP_DEBUG] 临时目标初始状态: " << static_cast(temp_target->current_state()) << std::endl; - // 4. 准备命令(注入源视图) custom_shader_widget_command modified_cmd = command; modified_cmd.injected_source_view = current_target->view(); modified_cmd.injected_source_sampler = ctx.linear_sampler; - std::cerr << "[BACKDROP_DEBUG] 步骤4: 注入源视图 view=" << (void*)current_target->view() - << ", sampler=" << (void*)ctx.linear_sampler << std::endl; - // 5. 渲染到临时目标 - std::cerr << "[BACKDROP_DEBUG] 步骤5: 渲染到临时目标" << std::endl; ctx.custom_shader->render_post_process_to_target( idle_ctx.cmd(), modified_cmd, @@ -87,22 +68,15 @@ namespace mirage { ctx.custom_shader_descriptor_pool ); - std::cerr << "[BACKDROP_DEBUG] 临时目标渲染后状态: " << static_cast(temp_target->current_state()) << std::endl; - // 6. Blit 回原目标 // 确保 temp_target 处于正确的状态(可能是 undefined 或 shader_read) // 先转换到 shader_read(如果需要),再转换到 transfer_src - std::cerr << "[BACKDROP_DEBUG] 步骤6: 准备 Blit" << std::endl; if (temp_target->current_state() == offscreen_target::state::undefined) { - std::cerr << "[BACKDROP_DEBUG] 临时目标状态为 undefined,转换到 shader_read" << std::endl; temp_target->transition_to(idle_ctx.cmd(), offscreen_target::state::shader_read); } temp_target->transition_to(idle_ctx.cmd(), offscreen_target::state::transfer_src); current_target->transition_to(idle_ctx.cmd(), offscreen_target::state::transfer_dst); - std::cerr << "[BACKDROP_DEBUG] Blit 前状态 - temp: " << static_cast(temp_target->current_state()) - << ", current: " << static_cast(current_target->current_state()) << std::endl; - // 计算 blit 区域 int32_t x = static_cast(std::max(0.0f, command.position.x())); int32_t y = static_cast(std::max(0.0f, command.position.y())); @@ -111,9 +85,6 @@ namespace mirage { int32_t h = static_cast(std::min(command.size.y(), static_cast(extent.height - y))); - std::cerr << "[BACKDROP_DEBUG] Blit 区域计算: x=" << x << ", y=" << y - << ", w=" << w << ", h=" << h << std::endl; - if (w > 0 && h > 0) { vk::ImageSubresourceLayers subresource{ vk::ImageAspectFlagBits::eColor, 0, 0, 1 @@ -131,13 +102,11 @@ namespace mirage { {offsets[0], offsets[1]} }; - std::cerr << "[BACKDROP_DEBUG] 执行 blitImage" << std::endl; idle_ctx.cmd().blitImage( temp_target->image(), vk::ImageLayout::eTransferSrcOptimal, current_target->image(), vk::ImageLayout::eTransferDstOptimal, 1, &blit_region, vk::Filter::eLinear ); - std::cerr << "[BACKDROP_DEBUG] blitImage 完成" << std::endl; } else { std::cerr << "[BACKDROP_DEBUG] 警告: Blit 区域无效 (w=" << w << ", h=" << h << "),跳过 blit" << std::endl; @@ -150,9 +119,7 @@ namespace mirage { } // 7. 恢复 Pass (Idle -> Offscreen) - std::cerr << "[BACKDROP_DEBUG] 步骤7: 恢复 Pass" << std::endl; *ctx.frame_ctx = std::move(idle_ctx).begin_offscreen_pass(*current_target, false); - std::cerr << "[BACKDROP_DEBUG] backdrop 模式处理完成" << std::endl; } else if constexpr (std::is_same_v) { // Mask Pass 内的 backdrop 处理 diff --git a/src/render/renderers/custom_shader_widget_renderer.cpp b/src/render/renderers/custom_shader_widget_renderer.cpp index d653aaa..11d6244 100644 --- a/src/render/renderers/custom_shader_widget_renderer.cpp +++ b/src/render/renderers/custom_shader_widget_renderer.cpp @@ -1160,20 +1160,16 @@ namespace mirage { vk::ImageLayout::eShaderReadOnlyOptimal }); write.pImageInfo = &image_infos.back(); + writes.push_back(write); break; } case vk::DescriptorType::eUniformBuffer: case vk::DescriptorType::eUniformBufferDynamic: { // UBO 需要通过回调由 widget 填充 - // 这里先分配占位符,由 update_descriptors 回调填充 - auto alloc = shader_res_mgr_->allocate_uniform_buffer(256); - buffer_infos.push_back(vk::DescriptorBufferInfo{ - alloc.buffer, - alloc.offset, - alloc.size - }); - write.pBufferInfo = &buffer_infos.back(); + // 注意:不要在这里创建占位符,否则会导致描述符被更新两次 + // 第一次更新时数据为空(0),可能导致渲染结果为黑色 + // widget 的 update_descriptors 回调会负责分配正确的 buffer 并填充数据 break; } @@ -1182,8 +1178,6 @@ namespace mirage { << static_cast(binding.type) << std::endl; continue; } - - writes.push_back(write); } if (!writes.empty()) { diff --git a/src/ui/widgets/custom_shader/custom_shader_widget_base.cpp b/src/ui/widgets/custom_shader/custom_shader_widget_base.cpp index ddca22f..5bf43f0 100644 --- a/src/ui/widgets/custom_shader/custom_shader_widget_base.cpp +++ b/src/ui/widgets/custom_shader/custom_shader_widget_base.cpp @@ -62,22 +62,10 @@ namespace mirage { if (render_mode == custom_shader_render_mode::post_process) { const vec2f_t pos = state.global_pos(); const vec2f_t size = state.size(); - - // 调试输出:检查 effect_rect 的当前值 - auto current_effect_rect = get_effect_rect(); - std::cerr << "[POST_PROCESS_DEBUG] 控件位置: (" << pos.x() << ", " << pos.y() - << "), 大小: (" << size.x() << ", " << size.y() << ")" << std::endl; - std::cerr << "[POST_PROCESS_DEBUG] 当前 effect_rect: (" - << current_effect_rect.x() << ", " << current_effect_rect.y() << ", " - << current_effect_rect.z() << ", " << current_effect_rect.w() << ")" << std::endl; // 设置 effect_rect(用于屏幕空间定位) // 修改:始终在每帧设置 effect_rect,因为布局可能已更改 set_effect_rect(vec4f_t{pos.x(), pos.y(), size.x(), size.y()}); - - std::cerr << "[POST_PROCESS_DEBUG] 设置后 effect_rect: (" - << pos.x() << ", " << pos.y() << ", " - << size.x() << ", " << size.y() << ")" << std::endl; } diff --git a/src/ui/widgets/custom_shader/post_process/post_process_shader_widget.h b/src/ui/widgets/custom_shader/post_process/post_process_shader_widget.h index 1e6bdc3..2968b5c 100644 --- a/src/ui/widgets/custom_shader/post_process/post_process_shader_widget.h +++ b/src/ui/widgets/custom_shader/post_process/post_process_shader_widget.h @@ -320,8 +320,7 @@ protected: const descriptor_update_context& ctx, std::span bindings) const override { for (const auto& binding : bindings) { - if (binding.type == vk::DescriptorType::eCombinedImageSampler && - binding.binding == 0) { + if (binding.type == vk::DescriptorType::eCombinedImageSampler && binding.binding == 0) { // 自动绑定源纹理到 binding 0 ctx.bind_sampler(0, ctx.source_view, ctx.source_sampler); } else if (binding.type == vk::DescriptorType::eUniformBuffer) {