From 539d35d01da54ae7c16fd43c8db3a72acbc5bd01 Mon Sep 17 00:00:00 2001 From: daiqingshuang Date: Wed, 3 Dec 2025 02:14:21 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B8=B2=E6=9F=93=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=8C=E4=BD=BF=E7=94=A8render=5Ftree=E6=9D=A5?= =?UTF-8?q?=E4=BF=9D=E8=AF=81=E5=B5=8C=E5=A5=97=E5=90=8E=E6=95=88=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakePresets.json | 395 ++++++++ docs/REFACTOR_NESTED_MASK_POST_EFFECT.md | 946 ++++++++++++++++++ example/new_pipeline/new_pipeline_example.cpp | 17 +- src/render/pipeline/command_processor.cpp | 333 ------ src/render/pipeline/command_processor.h | 63 -- src/render/pipeline/frame_context_v2.h | 8 + src/render/pipeline/mask_renderer.cpp | 203 +++- src/render/pipeline/mask_renderer.h | 53 +- src/render/pipeline/nested_pass_context.h | 395 -------- src/render/pipeline/offscreen_target.cpp | 35 +- src/render/pipeline/offscreen_target.h | 14 +- src/render/pipeline/pass_state_tags.h | 69 ++ src/render/pipeline/render_pipeline.cpp | 306 +++--- src/render/pipeline/render_pipeline.h | 42 +- src/render/pipeline/render_target_pool.cpp | 162 +++ src/render/pipeline/render_target_pool.h | 86 ++ src/render/pipeline/render_tree.h | 638 ++++++++++++ src/render/pipeline/render_tree_builder.cpp | 400 ++++++++ src/render/pipeline/render_tree_builder.h | 68 ++ src/render/pipeline/render_tree_executor.cpp | 229 +++++ src/render/pipeline/render_tree_executor.h | 36 + src/render/pipeline/stencil_mask_renderer.cpp | 656 ------------ src/render/pipeline/stencil_mask_renderer.h | 141 --- src/render/pipeline/types.h | 30 - tests/CMakeLists.txt | 143 +-- tests/generated/runtime_array_test_comp.h | 490 --------- tests/render_tree/CMakeLists.txt | 10 + tests/render_tree/test_render_tree.cpp | 172 ++++ tests/render_tree_builder/CMakeLists.txt | 10 + .../test_render_tree_builder.cpp | 113 +++ tests/render_tree_integration/CMakeLists.txt | 11 + .../test_render_tree_integration.cpp | 854 ++++++++++++++++ tests/shaders/minimal/minimal.comp | 8 - tests/shaders/minimal/minimal.frag | 8 - tests/shaders/minimal/minimal.vert | 6 - .../minimal/runtime_array_test.comp.glsl | 33 - tests/test_refactor_syntax/CMakeLists.txt | 11 + .../test_refactor_syntax.cpp | 60 ++ tests/unit/core/test_context.cpp | 221 ---- tests/unit/core/test_device_manager.cpp | 247 ----- tests/unit/core/test_logical_device.cpp | 308 ------ tests/unit/pipeline/test_compute_pipeline.cpp | 333 ------ .../unit/pipeline/test_graphics_pipeline.cpp | 360 ------- tests/unit/pipeline/test_pass_state.cpp | 388 ------- tests/unit/pipeline/test_pipeline_builder.cpp | 351 ------- tests/unit/resource/test_command_buffer.cpp | 237 ----- tests/unit/resource/test_resource_manager.cpp | 263 ----- tests/unit/resource/test_typed_buffer.cpp | 238 ----- .../unit/scheduler/test_compute_scheduler.cpp | 385 ------- tests/utils/mock_window.cpp | 69 -- tests/utils/mock_window.h | 61 -- tests/utils/shader_loader.cpp | 161 --- tests/utils/shader_loader.h | 47 - tests/utils/test_helpers.cpp | 61 -- tests/utils/test_helpers.h | 50 - tests/utils/vulkan_test_base.cpp | 125 --- tests/utils/vulkan_test_base.h | 55 - 57 files changed, 4714 insertions(+), 6500 deletions(-) create mode 100644 CMakePresets.json create mode 100644 docs/REFACTOR_NESTED_MASK_POST_EFFECT.md delete mode 100644 src/render/pipeline/command_processor.cpp delete mode 100644 src/render/pipeline/command_processor.h delete mode 100644 src/render/pipeline/nested_pass_context.h create mode 100644 src/render/pipeline/render_target_pool.cpp create mode 100644 src/render/pipeline/render_target_pool.h create mode 100644 src/render/pipeline/render_tree.h create mode 100644 src/render/pipeline/render_tree_builder.cpp create mode 100644 src/render/pipeline/render_tree_builder.h create mode 100644 src/render/pipeline/render_tree_executor.cpp create mode 100644 src/render/pipeline/render_tree_executor.h delete mode 100644 src/render/pipeline/stencil_mask_renderer.cpp delete mode 100644 src/render/pipeline/stencil_mask_renderer.h delete mode 100644 tests/generated/runtime_array_test_comp.h create mode 100644 tests/render_tree/CMakeLists.txt create mode 100644 tests/render_tree/test_render_tree.cpp create mode 100644 tests/render_tree_builder/CMakeLists.txt create mode 100644 tests/render_tree_builder/test_render_tree_builder.cpp create mode 100644 tests/render_tree_integration/CMakeLists.txt create mode 100644 tests/render_tree_integration/test_render_tree_integration.cpp delete mode 100644 tests/shaders/minimal/minimal.comp delete mode 100644 tests/shaders/minimal/minimal.frag delete mode 100644 tests/shaders/minimal/minimal.vert delete mode 100644 tests/shaders/minimal/runtime_array_test.comp.glsl create mode 100644 tests/test_refactor_syntax/CMakeLists.txt create mode 100644 tests/test_refactor_syntax/test_refactor_syntax.cpp delete mode 100644 tests/unit/core/test_context.cpp delete mode 100644 tests/unit/core/test_device_manager.cpp delete mode 100644 tests/unit/core/test_logical_device.cpp delete mode 100644 tests/unit/pipeline/test_compute_pipeline.cpp delete mode 100644 tests/unit/pipeline/test_graphics_pipeline.cpp delete mode 100644 tests/unit/pipeline/test_pass_state.cpp delete mode 100644 tests/unit/pipeline/test_pipeline_builder.cpp delete mode 100644 tests/unit/resource/test_command_buffer.cpp delete mode 100644 tests/unit/resource/test_resource_manager.cpp delete mode 100644 tests/unit/resource/test_typed_buffer.cpp delete mode 100644 tests/unit/scheduler/test_compute_scheduler.cpp delete mode 100644 tests/utils/mock_window.cpp delete mode 100644 tests/utils/mock_window.h delete mode 100644 tests/utils/shader_loader.cpp delete mode 100644 tests/utils/shader_loader.h delete mode 100644 tests/utils/test_helpers.cpp delete mode 100644 tests/utils/test_helpers.h delete mode 100644 tests/utils/vulkan_test_base.cpp delete mode 100644 tests/utils/vulkan_test_base.h diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..2e2ca28 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,395 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 21, + "patch": 0 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "${sourceDir}/build/${presetName}", + "installDir": "${sourceDir}/install/${presetName}", + "cacheVariables": { + "BUILD_TESTS": "ON" + } + }, + { + "name": "vcpkg-base", + "hidden": true, + "inherits": "base", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": { + "type": "FILEPATH", + "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + } + }, + { + "name": "windows-base", + "hidden": true, + "inherits": "vcpkg-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "architecture": { + "value": "x64", + "strategy": "set" + }, + "cacheVariables": { + "MIRAGE_ENABLE_DX11": "ON", + "VCPKG_TARGET_TRIPLET": "x64-windows" + } + }, + { + "name": "windows-vs2026-base", + "hidden": true, + "inherits": "windows-base", + "generator": "Visual Studio 17 2022", + "toolset": { + "value": "host=x64", + "strategy": "set" + } + }, + { + "name": "windows-ninja-base", + "hidden": true, + "inherits": "windows-base", + "generator": "Ninja", + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "hostOS": ["Windows"] + } + } + }, + { + "name": "linux-base", + "hidden": true, + "inherits": "vcpkg-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "cacheVariables": { + "MIRAGE_ENABLE_DX11": "OFF" + } + }, + { + "name": "macos-base", + "hidden": true, + "inherits": "vcpkg-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "cacheVariables": { + "MIRAGE_ENABLE_DX11": "OFF" + } + }, + { + "name": "windows-msvc-debug", + "displayName": "Windows MSVC Debug (VS2026 Preview)", + "description": "Windows平台使用Visual Studio 2026 Preview MSVC编译器的Debug配置", + "inherits": "windows-vs2026-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-msvc-release", + "displayName": "Windows MSVC Release (VS2026 Preview)", + "description": "Windows平台使用Visual Studio 2026 Preview MSVC编译器的Release配置", + "inherits": "windows-vs2026-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "windows-msvc-relwithdebinfo", + "displayName": "Windows MSVC RelWithDebInfo (VS2026 Preview)", + "description": "Windows平台使用Visual Studio 2026 Preview MSVC编译器的RelWithDebInfo配置", + "inherits": "windows-vs2026-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "windows-ninja-debug", + "displayName": "Windows Ninja Debug", + "description": "Windows平台使用Ninja和MSVC编译器的Debug配置", + "inherits": "windows-ninja-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "cl", + "CMAKE_CXX_COMPILER": "cl" + } + }, + { + "name": "windows-ninja-release", + "displayName": "Windows Ninja Release", + "description": "Windows平台使用Ninja和MSVC编译器的Release配置", + "inherits": "windows-ninja-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "cl", + "CMAKE_CXX_COMPILER": "cl" + } + }, + { + "name": "windows-clang-debug", + "displayName": "Windows Clang Debug", + "description": "Windows平台使用Clang编译器的Debug配置", + "inherits": "windows-ninja-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "windows-clang-release", + "displayName": "Windows Clang Release", + "description": "Windows平台使用Clang编译器的Release配置", + "inherits": "windows-ninja-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "linux-gcc-debug", + "displayName": "Linux GCC Debug", + "description": "Linux平台使用GCC编译器的Debug配置", + "inherits": "linux-base", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" + } + }, + { + "name": "linux-gcc-release", + "displayName": "Linux GCC Release", + "description": "Linux平台使用GCC编译器的Release配置", + "inherits": "linux-base", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" + } + }, + { + "name": "linux-clang-debug", + "displayName": "Linux Clang Debug", + "description": "Linux平台使用Clang编译器的Debug配置", + "inherits": "linux-base", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "linux-clang-release", + "displayName": "Linux Clang Release", + "description": "Linux平台使用Clang编译器的Release配置", + "inherits": "linux-base", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "macos-clang-debug", + "displayName": "macOS Clang Debug", + "description": "macOS平台使用Clang编译器的Debug配置", + "inherits": "macos-base", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "macos-clang-release", + "displayName": "macOS Clang Release", + "description": "macOS平台使用Clang编译器的Release配置", + "inherits": "macos-base", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { + "name": "windows-msvc-debug", + "displayName": "Build Windows MSVC Debug", + "configurePreset": "windows-msvc-debug", + "configuration": "Debug" + }, + { + "name": "windows-msvc-release", + "displayName": "Build Windows MSVC Release", + "configurePreset": "windows-msvc-release", + "configuration": "Release" + }, + { + "name": "windows-msvc-relwithdebinfo", + "displayName": "Build Windows MSVC RelWithDebInfo", + "configurePreset": "windows-msvc-relwithdebinfo", + "configuration": "RelWithDebInfo" + }, + { + "name": "windows-ninja-debug", + "displayName": "Build Windows Ninja Debug", + "configurePreset": "windows-ninja-debug" + }, + { + "name": "windows-ninja-release", + "displayName": "Build Windows Ninja Release", + "configurePreset": "windows-ninja-release" + }, + { + "name": "windows-clang-debug", + "displayName": "Build Windows Clang Debug", + "configurePreset": "windows-clang-debug" + }, + { + "name": "windows-clang-release", + "displayName": "Build Windows Clang Release", + "configurePreset": "windows-clang-release" + }, + { + "name": "linux-gcc-debug", + "displayName": "Build Linux GCC Debug", + "configurePreset": "linux-gcc-debug" + }, + { + "name": "linux-gcc-release", + "displayName": "Build Linux GCC Release", + "configurePreset": "linux-gcc-release" + }, + { + "name": "linux-clang-debug", + "displayName": "Build Linux Clang Debug", + "configurePreset": "linux-clang-debug" + }, + { + "name": "linux-clang-release", + "displayName": "Build Linux Clang Release", + "configurePreset": "linux-clang-release" + }, + { + "name": "macos-clang-debug", + "displayName": "Build macOS Clang Debug", + "configurePreset": "macos-clang-debug" + }, + { + "name": "macos-clang-release", + "displayName": "Build macOS Clang Release", + "configurePreset": "macos-clang-release" + } + ], + "testPresets": [ + { + "name": "test-base", + "hidden": true, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error", + "stopOnFailure": false + } + }, + { + "name": "windows-msvc-debug", + "displayName": "Test Windows MSVC Debug", + "inherits": "test-base", + "configurePreset": "windows-msvc-debug", + "configuration": "Debug" + }, + { + "name": "windows-msvc-release", + "displayName": "Test Windows MSVC Release", + "inherits": "test-base", + "configurePreset": "windows-msvc-release", + "configuration": "Release" + }, + { + "name": "windows-ninja-debug", + "displayName": "Test Windows Ninja Debug", + "inherits": "test-base", + "configurePreset": "windows-ninja-debug" + }, + { + "name": "windows-ninja-release", + "displayName": "Test Windows Ninja Release", + "inherits": "test-base", + "configurePreset": "windows-ninja-release" + }, + { + "name": "windows-clang-debug", + "displayName": "Test Windows Clang Debug", + "inherits": "test-base", + "configurePreset": "windows-clang-debug" + }, + { + "name": "windows-clang-release", + "displayName": "Test Windows Clang Release", + "inherits": "test-base", + "configurePreset": "windows-clang-release" + }, + { + "name": "linux-gcc-debug", + "displayName": "Test Linux GCC Debug", + "inherits": "test-base", + "configurePreset": "linux-gcc-debug" + }, + { + "name": "linux-gcc-release", + "displayName": "Test Linux GCC Release", + "inherits": "test-base", + "configurePreset": "linux-gcc-release" + }, + { + "name": "linux-clang-debug", + "displayName": "Test Linux Clang Debug", + "inherits": "test-base", + "configurePreset": "linux-clang-debug" + }, + { + "name": "linux-clang-release", + "displayName": "Test Linux Clang Release", + "inherits": "test-base", + "configurePreset": "linux-clang-release" + }, + { + "name": "macos-clang-debug", + "displayName": "Test macOS Clang Debug", + "inherits": "test-base", + "configurePreset": "macos-clang-debug" + }, + { + "name": "macos-clang-release", + "displayName": "Test macOS Clang Release", + "inherits": "test-base", + "configurePreset": "macos-clang-release" + } + ] +} \ No newline at end of file diff --git a/docs/REFACTOR_NESTED_MASK_POST_EFFECT.md b/docs/REFACTOR_NESTED_MASK_POST_EFFECT.md new file mode 100644 index 0000000..6745ff4 --- /dev/null +++ b/docs/REFACTOR_NESTED_MASK_POST_EFFECT.md @@ -0,0 +1,946 @@ +# Post Effect 与 Mask 嵌套渲染重构方案 + +## 1. 问题分析 + +### 1.1 当前架构 + +当前的渲染管线采用**线性段(Segment)处理模式**: + +``` +render_command[] → command_processor → render_segment[] → render_pipeline +``` + +**关键组件**: +- `command_processor`: 将渲染命令按 z_order 排序,分割为线性的 `render_segment` 列表 +- `render_segment`: 包含 `segment_type`(geometry/post_effect/mask_begin/mask_end) +- `render_pipeline::render_segments()`: 线性遍历 segments,处理每种类型 + +### 1.2 问题根源 + +**线性处理无法正确表达嵌套关系**: + +```cpp +// 当前的 segment 处理逻辑(简化) +for (const auto& segment : segments) { + if (segment.type == segment_type::geometry) { + // 渲染几何 + } + else if (segment.type == segment_type::post_effect) { + // 应用后效到当前目标 + } + else if (segment.type == segment_type::mask_begin) { + // 开始 Stencil 遮罩 + } + else if (segment.type == segment_type::mask_end) { + // 结束 Stencil 遮罩 + } +} +``` + +**问题场景**: + +``` +Widget 树: +overlay { + imager | mask(rounded_rect) // 第一个 overlay 的内容 + post_effect(blur) +} | stretch() + +overlay { + imager // 第二个 overlay 的内容 + post_effect(blur) +} | stretch() | mask(circle) // 外层 mask +``` + +**生成的命令序列**(按 z_order 排序后): +``` +[0] mask_begin (rounded_rect) +[1] image_command +[2] mask_end +[3] post_effect (blur) +[4] mask_begin (circle) ← 问题:此时 Stencil 状态已被前面的 mask 污染 +[5] image_command +[6] post_effect (blur) +[7] mask_end +``` + +**核心问题**: +1. **Stencil 状态污染**:第一个 mask 的 Stencil 操作影响了后续渲染 +2. **后效作用域不明确**:post_effect 应该只作用于其父容器内的内容 +3. **嵌套边界丢失**:线性结构无法表达 "mask 内部有 post_effect" 或 "post_effect 内部有 mask" + +### 1.3 期望支持的场景 + +| 场景 | 描述 | 示例 | +|------|------|------| +| 1 | mask 内部有 post_effect | 圆形遮罩内的内容需要模糊 | +| 2 | post_effect 作用于 mask 内容 | 对整个遮罩区域应用暗角 | +| 3 | mask 内部嵌套 mask | 多层遮罩嵌套 | +| 4 | post_effect 内部有 mask | 模糊区域内只显示特定形状 | +| 5 | 复杂多层嵌套 | mask 和 post_effect 任意组合 | + +## 2. 重构方案 + +### 2.1 核心思想:渲染树(Render Tree) + +将线性的 `render_segment[]` 替换为**树形结构**,准确表达嵌套关系: + +``` +RenderNode (树节点) +├── GeometryNode: 几何渲染(叶子节点) +├── MaskNode: 遮罩节点(容器节点) +│ └── children: RenderNode[] +├── PostEffectNode: 后效节点(容器节点) +│ └── children: RenderNode[] +└── GroupNode: 分组节点(容器节点) + └── children: RenderNode[] +``` + +### 2.2 新的数据结构 + +```cpp +// src/render/pipeline/render_tree.h + +namespace mirage { + +/// 渲染节点类型 +enum class render_node_type { + geometry, ///< 几何渲染(叶子) + mask, ///< 遮罩容器 + post_effect, ///< 后效容器 + group ///< 分组容器 +}; + +/// 渲染节点基类 +struct render_node { + render_node_type type; + aabb2d_t bounds; ///< 节点边界 + int32_t z_order = 0; ///< 排序用 + + virtual ~render_node() = default; +}; + +/// 几何节点 - 包含一批可合并的几何命令 +struct geometry_node : render_node { + std::vector batches; + + geometry_node() { type = render_node_type::geometry; } +}; + +/// 遮罩节点 - 包含遮罩参数和子节点 +struct mask_node : render_node { + mask_params params; + aabb2d_t content_bounds; + std::vector> children; + + mask_node() { type = render_node_type::mask; } +}; + +/// 后效节点 - 包含后效参数和子节点 +struct post_effect_node : render_node { + post_effect_command effect; + std::vector> children; + + post_effect_node() { type = render_node_type::post_effect; } +}; + +/// 分组节点 - 纯粹的容器,用于组织子节点 +struct group_node : render_node { + std::vector> children; + + group_node() { type = render_node_type::group; } +}; + +/// 渲染树 - 根节点 +struct render_tree { + std::unique_ptr root; + + /// 从命令列表构建渲染树 + static auto build(const std::vector& commands) -> render_tree; +}; + +} // namespace mirage +``` + +### 2.3 渲染树构建器 + +```cpp +// src/render/pipeline/render_tree_builder.h + +namespace mirage { + +/// 渲染树构建器 +/// 将线性的 render_command 列表转换为树形结构 +class render_tree_builder { +public: + /// 从命令列表构建渲染树 + [[nodiscard]] auto build(const std::vector& commands) -> render_tree; + +private: + /// 构建上下文 - 跟踪当前嵌套状态 + struct build_context { + std::stack node_stack; ///< 当前节点栈 + render_node* current = nullptr; + }; + + /// 处理单个命令 + void process_command(build_context& ctx, const render_command& cmd); + + /// 处理几何命令 + void process_geometry(build_context& ctx, const render_command& cmd); + + /// 处理 mask_begin + void process_mask_begin(build_context& ctx, const mask_begin_command& cmd); + + /// 处理 mask_end + void process_mask_end(build_context& ctx); + + /// 处理 post_effect + void process_post_effect(build_context& ctx, const post_effect_command& cmd); +}; + +} // namespace mirage +``` + +**构建算法**: + +```cpp +auto render_tree_builder::build(const std::vector& commands) -> render_tree { + render_tree tree; + tree.root = std::make_unique(); + + build_context ctx; + ctx.current = tree.root.get(); + ctx.node_stack.push(tree.root.get()); + + // 按 z_order 排序 + auto sorted = commands; + std::ranges::stable_sort(sorted, [](const auto& a, const auto& b) { + return get_z_order(a) < get_z_order(b); + }); + + for (const auto& cmd : sorted) { + process_command(ctx, cmd); + } + + return tree; +} + +void render_tree_builder::process_command(build_context& ctx, const render_command& cmd) { + std::visit([&](const auto& c) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + process_mask_begin(ctx, c); + } + else if constexpr (std::is_same_v) { + process_mask_end(ctx); + } + else if constexpr (std::is_same_v) { + process_post_effect(ctx, c); + } + else { + // 几何命令 + process_geometry(ctx, cmd); + } + }, cmd); +} + +void render_tree_builder::process_mask_begin(build_context& ctx, const mask_begin_command& cmd) { + // 创建新的 mask 节点 + auto mask = std::make_unique(); + mask->params = cmd.params; + mask->content_bounds = cmd.content_bounds; + + // 获取当前容器 + auto* container = get_container(ctx.current); + auto* mask_ptr = mask.get(); + container->children.push_back(std::move(mask)); + + // 压栈,进入 mask 内部 + ctx.node_stack.push(ctx.current); + ctx.current = mask_ptr; +} + +void render_tree_builder::process_mask_end(build_context& ctx) { + // 弹栈,退出 mask + if (!ctx.node_stack.empty()) { + ctx.current = ctx.node_stack.top(); + ctx.node_stack.pop(); + } +} + +void render_tree_builder::process_post_effect(build_context& ctx, const post_effect_command& cmd) { + // 后效节点作为当前容器的子节点 + // 注意:后效应用于其之前的同级内容 + auto effect = std::make_unique(); + effect->effect = cmd; + + auto* container = get_container(ctx.current); + container->children.push_back(std::move(effect)); +} +``` + +### 2.4 渲染树执行器 + +```cpp +// src/render/pipeline/render_tree_executor.h + +namespace mirage { + +/// 渲染树执行器 +/// 递归遍历渲染树,执行实际的渲染操作 +class render_tree_executor { +public: + struct context { + geometry_renderer* geometry; + image_renderer* imager; + mask_renderer* mask; + stencil_mask_renderer* stencil_mask; + post_effect_applicator* effects; + offscreen_target* main_target; + float time; + }; + + render_tree_executor(context ctx); + + /// 执行渲染树 + /// @param tree 渲染树 + /// @param frame_ctx 帧上下文(offscreen_pass 状态) + /// @return 完成后的帧上下文(idle 状态) + [[nodiscard]] auto execute( + const render_tree& tree, + pass_state::frame_context&& frame_ctx + ) -> pass_state::frame_context; + +private: + context ctx_; + + /// 递归执行节点 + void execute_node( + const render_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target + ); + + /// 执行几何节点 + void execute_geometry( + const geometry_node* node, + vk::CommandBuffer cmd + ); + + /// 执行遮罩节点 + void execute_mask( + const mask_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target + ); + + /// 执行后效节点 + void execute_post_effect( + const post_effect_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target + ); + + /// 执行分组节点 + void execute_group( + const group_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target + ); +}; + +} // namespace mirage +``` + +**执行算法**: + +```cpp +void render_tree_executor::execute_node( + const render_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target +) { + switch (node->type) { + case render_node_type::geometry: + execute_geometry(static_cast(node), frame_ctx.cmd()); + break; + case render_node_type::mask: + execute_mask(static_cast(node), frame_ctx, current_target); + break; + case render_node_type::post_effect: + execute_post_effect(static_cast(node), frame_ctx, current_target); + break; + case render_node_type::group: + execute_group(static_cast(node), frame_ctx, current_target); + break; + } +} + +void render_tree_executor::execute_mask( + const mask_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target +) { + // 方案 A: 使用离屏目标(支持后效) + // 1. 获取临时渲染目标 + auto* mask_target = ctx_.mask->acquire_render_target(); + + // 2. 结束当前 pass,切换到 mask 目标 + auto idle_ctx = std::move(frame_ctx).end_offscreen_pass(); + + // 3. 开始 mask 目标渲染 + auto mask_ctx = std::move(idle_ctx).begin_offscreen_pass(*mask_target, true); + + // 4. 递归渲染子节点到 mask 目标 + for (const auto& child : node->children) { + execute_node(child.get(), mask_ctx, mask_target); + } + + // 5. 结束 mask 目标渲染 + idle_ctx = std::move(mask_ctx).end_offscreen_pass(); + + // 6. 恢复到原目标 + frame_ctx = std::move(idle_ctx).begin_offscreen_pass(*current_target, false); + + // 7. 应用遮罩合成 + ctx_.mask->composite_mask( + frame_ctx, + mask_target, + node->params, + node->content_bounds + ); + + // 8. 释放临时目标 + ctx_.mask->release_render_target(mask_target); +} + +void render_tree_executor::execute_post_effect( + const post_effect_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target +) { + // 后效应用于当前目标 + // 1. 结束当前 pass + auto idle_ctx = std::move(frame_ctx).end_offscreen_pass(); + + // 2. 应用后效 + ctx_.effects->apply(idle_ctx.cmd(), *current_target, node->effect, ctx_.time); + + // 3. 恢复 pass + frame_ctx = std::move(idle_ctx).begin_offscreen_pass(*current_target, false); +} +``` + +### 2.5 架构对比 + +**重构前**: +``` +render_command[] + → command_processor::process() + → render_segment[] (线性) + → render_pipeline::render_segments() (线性遍历) +``` + +**重构后**: +``` +render_command[] + → render_tree_builder::build() + → render_tree (树形) + → render_tree_executor::execute() (递归遍历) +``` + +## 3. 实现计划 + +### 3.1 阶段一:数据结构(1-2天) + +1. 创建 `src/render/pipeline/render_tree.h` + - 定义 `render_node` 基类和派生类 + - 定义 `render_tree` 结构 + +2. 创建 `src/render/pipeline/render_tree_builder.h/cpp` + - 实现命令到树的转换逻辑 + - 处理嵌套边界(mask_begin/end) + +### 3.2 阶段二:执行器(2-3天) + +1. 创建 `src/render/pipeline/render_tree_executor.h/cpp` + - 实现递归遍历逻辑 + - 实现各节点类型的渲染逻辑 + +2. 重构 `mask_renderer` + - 支持离屏目标池管理 + - 支持递归遮罩渲染 + +### 3.3 阶段三:集成和清理(1-2天) + +1. 修改 `render_pipeline` + - 删除 `command_processor` 引用 + - 使用 `render_tree_builder` 替代 + - 使用 `render_tree_executor::execute()` 替代 `render_segments()` + +2. 删除旧代码 + - 删除 `command_processor.h` 和 `command_processor.cpp` + - 删除 `stencil_mask_renderer.h` 和 `stencil_mask_renderer.cpp`(不再需要 Stencil 方案) + - 删除 `nested_pass_context.h`(新架构不需要) + - 清理 `types.h` 中的 `render_segment` 和 `segment_type` + +### 3.4 阶段四:测试和优化(1-2天) + +1. 编写测试用例 + - 单层 mask + - 单层 post_effect + - mask 内 post_effect + - post_effect 内 mask + - 多层嵌套 + +2. 性能优化 + - 渲染目标池复用 + - 减少不必要的 pass 切换 + +## 4. 关键设计决策 + +### 4.1 遮罩实现方式 + +| 方案 | 优点 | 缺点 | 适用场景 | +|------|------|------|----------| +| Stencil Buffer | 无需额外目标,性能好 | 不支持内部后效 | 简单遮罩 | +| 离屏目标 | 支持任意嵌套和后效 | 需要额外内存和 pass 切换 | 复杂嵌套 | +| 混合方案 | 根据需要选择 | 实现复杂 | 通用 | + +**推荐**:采用**离屏目标方案**,因为: +1. 支持所有嵌套场景 +2. 实现逻辑清晰 +3. 可以通过目标池复用优化性能 + +### 4.2 后效作用域 + +后效节点的语义:**应用于同一容器内、在其之前渲染的所有内容** + +``` +group { + geometry_a // 被后效影响 + geometry_b // 被后效影响 + post_effect // 应用于 a 和 b + geometry_c // 不被后效影响 +} +``` + +### 4.3 渲染目标管理 + +```cpp +class render_target_pool { +public: + /// 获取指定大小的渲染目标 + auto acquire(uint32_t width, uint32_t height) -> offscreen_target*; + + /// 释放渲染目标回池 + void release(offscreen_target* target); + + /// 帧结束时重置 + void reset_frame(); + +private: + std::vector> pool_; + std::vector available_; +}; +``` + +## 5. 风险和缓解 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| 性能下降 | 多层嵌套导致大量 pass 切换 | 渲染目标池复用;合并相邻几何节点 | +| 内存增加 | 每层嵌套需要临时目标 | 动态调整池大小;延迟创建 | +| 复杂度增加 | 树形结构比线性复杂 | 完善文档;单元测试覆盖 | + +## 6. 文件变更清单 + +### 新增文件 +- `src/render/pipeline/render_tree.h` - 渲染树数据结构 +- `src/render/pipeline/render_tree_builder.h` - 树构建器声明 +- `src/render/pipeline/render_tree_builder.cpp` - 树构建器实现 +- `src/render/pipeline/render_tree_executor.h` - 树执行器声明 +- `src/render/pipeline/render_tree_executor.cpp` - 树执行器实现 +- `src/render/pipeline/render_target_pool.h` - 渲染目标池 +- `src/render/pipeline/render_target_pool.cpp` - 渲染目标池实现 + +### 修改文件 +- `src/render/pipeline/render_pipeline.h` - 删除旧组件,添加新组件引用 +- `src/render/pipeline/render_pipeline.cpp` - 使用新的渲染流程 +- `src/render/pipeline/mask_renderer.h` - 调整接口支持递归 +- `src/render/pipeline/mask_renderer.cpp` - 实现调整 +- `src/render/pipeline/types.h` - 删除 render_segment 和 segment_type +- `src/render/pipeline/CMakeLists.txt` - 添加新文件,删除旧文件 + +### 删除文件 +- `src/render/pipeline/command_processor.h` - 旧的线性处理器 +- `src/render/pipeline/command_processor.cpp` - 旧的线性处理器实现 +- `src/render/pipeline/stencil_mask_renderer.h` - 不再需要的 Stencil 方案 +- `src/render/pipeline/stencil_mask_renderer.cpp` - 不再需要的 Stencil 方案实现 +- `src/render/pipeline/nested_pass_context.h` - 旧的嵌套上下文管理 + +## 7. Mermaid 架构图 + +### 7.1 重构前架构 + +```mermaid +flowchart LR + subgraph Input + RC[render_command 列表] + end + + subgraph Processing + CP[command_processor] + RS[render_segment 列表] + end + + subgraph Rendering + RP[render_pipeline] + GR[geometry_renderer] + MR[mask_renderer] + PE[post_effect_applicator] + end + + RC --> CP + CP --> RS + RS --> RP + RP --> GR + RP --> MR + RP --> PE +``` + +### 7.2 重构后架构 + +```mermaid +flowchart LR + subgraph Input + RC[render_command 列表] + end + + subgraph TreeBuilding + RTB[render_tree_builder] + RT[render_tree] + end + + subgraph TreeExecution + RTE[render_tree_executor] + end + + subgraph Renderers + GR[geometry_renderer] + MR[mask_renderer] + PE[post_effect_applicator] + RTP[render_target_pool] + end + + RC --> RTB + RTB --> RT + RT --> RTE + RTE --> GR + RTE --> MR + RTE --> PE + MR --> RTP +``` + +### 7.3 渲染树结构示例 + +```mermaid +graph TD + subgraph RenderTree + ROOT[group_node root] + + ROOT --> G1[geometry_node] + ROOT --> M1[mask_node rounded_rect] + ROOT --> PE1[post_effect_node blur] + ROOT --> M2[mask_node circle] + + M1 --> G2[geometry_node image] + + M2 --> G3[geometry_node image] + M2 --> PE2[post_effect_node blur] + end +``` + +### 7.4 执行流程 + +```mermaid +sequenceDiagram + participant E as Executor + participant M as MaskRenderer + participant P as PostEffectApplicator + participant T as TargetPool + + E->>E: execute_group root + E->>E: execute_geometry G1 + + Note over E,T: 进入 mask M1 + E->>T: acquire target + T-->>E: mask_target_1 + E->>E: execute_geometry G2 到 mask_target_1 + E->>M: composite_mask + E->>T: release target + + Note over E,P: 应用后效 PE1 + E->>P: apply blur + + Note over E,T: 进入 mask M2 + E->>T: acquire target + T-->>E: mask_target_2 + E->>E: execute_geometry G3 到 mask_target_2 + E->>P: apply blur PE2 到 mask_target_2 + E->>M: composite_mask + E->>T: release target +``` + +## 8. 移动端优化策略 + +移动设备(iOS/Android)相比桌面平台有以下特点和限制: +- **带宽受限**:内存带宽较低,频繁的渲染目标切换代价高 +- **Tile-Based GPU**:大多数移动 GPU 使用 Tile-Based Deferred Rendering (TBDR) +- **内存受限**:可用显存较少,需要谨慎管理渲染目标 +- **功耗敏感**:过多的 GPU 操作会导致发热和耗电 + +### 8.1 渲染目标优化 + +#### 8.1.1 延迟分配策略 + +```cpp +class render_target_pool { +public: + struct mobile_config { + uint32_t max_pool_size = 4; ///< 移动端限制池大小 + bool use_transient_memory = true; ///< 使用 transient 内存 + bool prefer_smaller_targets = true; ///< 优先使用较小的目标 + }; + + /// 获取渲染目标(移动端优化版本) + auto acquire_mobile(const aabb2d_t& bounds) -> offscreen_target* { + // 1. 计算实际需要的大小(可能小于全屏) + uint32_t width = static_cast(bounds.sizes().x()); + uint32_t height = static_cast(bounds.sizes().y()); + + // 2. 对齐到 tile 大小(通常 32x32 或 16x16) + width = align_to_tile(width); + height = align_to_tile(height); + + // 3. 查找合适大小的可用目标 + return find_or_create(width, height); + } + +private: + static constexpr uint32_t TILE_SIZE = 32; + + uint32_t align_to_tile(uint32_t size) { + return (size + TILE_SIZE - 1) & ~(TILE_SIZE - 1); + } +}; +``` + +#### 8.1.2 Transient 附件 + +对于移动端,使用 Vulkan 的 transient 附件可以显著减少内存带宽: + +```cpp +// 创建 transient 渲染目标 +vk::ImageCreateInfo image_info{}; +image_info.usage = vk::ImageUsageFlagBits::eColorAttachment | + vk::ImageUsageFlagBits::eTransientAttachment; + +// 使用 lazily allocated 内存 +VmaAllocationCreateInfo alloc_info{}; +alloc_info.usage = VMA_MEMORY_USAGE_GPU_LAZILY_ALLOCATED; +alloc_info.preferredFlags = VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT; +``` + +### 8.2 Subpass 优化 + +移动端 GPU 的 TBDR 架构可以通过 subpass 实现高效的渲染目标内读取: + +```cpp +/// 移动端优化的遮罩渲染 +/// 使用 subpass 而非独立的渲染目标切换 +class mobile_mask_renderer { +public: + /// 创建多 subpass 的 RenderPass + void create_render_pass_with_subpasses() { + // Subpass 0: 渲染遮罩内容 + // Subpass 1: 应用遮罩效果(input attachment 读取 subpass 0 的结果) + + vk::SubpassDescription subpasses[2]; + + // Subpass 0: 渲染到临时附件 + subpasses[0].colorAttachmentCount = 1; + subpasses[0].pColorAttachments = &temp_attachment_ref; + + // Subpass 1: 读取临时附件,输出到最终目标 + subpasses[1].inputAttachmentCount = 1; + subpasses[1].pInputAttachments = &temp_input_ref; + subpasses[1].colorAttachmentCount = 1; + subpasses[1].pColorAttachments = &final_attachment_ref; + + // Subpass 依赖 + vk::SubpassDependency dependency{}; + dependency.srcSubpass = 0; + dependency.dstSubpass = 1; + dependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + dependency.dstStageMask = vk::PipelineStageFlagBits::eFragmentShader; + dependency.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; + dependency.dstAccessMask = vk::AccessFlagBits::eInputAttachmentRead; + dependency.dependencyFlags = vk::DependencyFlagBits::eByRegion; // 关键:按区域依赖 + } +}; +``` + +### 8.3 嵌套深度限制 + +移动端应限制嵌套深度以避免过多的渲染目标: + +```cpp +struct mobile_render_config { + uint32_t max_mask_depth = 3; ///< 最大遮罩嵌套深度 + uint32_t max_effect_depth = 2; ///< 最大后效嵌套深度 + uint32_t max_total_depth = 4; ///< 最大总嵌套深度 + bool fallback_to_stencil = true; ///< 超过深度时回退到 Stencil +}; + +void render_tree_executor::execute_mask_mobile( + const mask_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target, + uint32_t current_depth +) { + if (current_depth >= config_.max_mask_depth) { + // 回退到 Stencil 遮罩(不支持内部后效,但性能更好) + execute_mask_stencil_fallback(node, frame_ctx, current_target); + return; + } + + // 正常的离屏目标遮罩渲染 + execute_mask_offscreen(node, frame_ctx, current_target, current_depth); +} +``` + +### 8.4 分辨率缩放 + +对于后效处理,可以使用较低分辨率以提高性能: + +```cpp +struct effect_quality_config { + float blur_resolution_scale = 0.5f; ///< 模糊效果使用半分辨率 + float vignette_resolution_scale = 1.0f; ///< 暗角效果使用全分辨率 + float chromatic_resolution_scale = 0.75f; ///< 色差效果使用 3/4 分辨率 +}; + +void render_tree_executor::execute_post_effect_mobile( + const post_effect_node* node, + pass_state::frame_context& frame_ctx, + offscreen_target* current_target +) { + float scale = get_resolution_scale(node->effect); + + if (scale < 1.0f) { + // 1. 降采样到临时目标 + auto* low_res_target = acquire_scaled_target(current_target, scale); + downsample(current_target, low_res_target); + + // 2. 在低分辨率目标上应用效果 + apply_effect(low_res_target, node->effect); + + // 3. 升采样回原目标 + upsample(low_res_target, current_target); + + release_target(low_res_target); + } else { + // 全分辨率处理 + apply_effect(current_target, node->effect); + } +} +``` + +### 8.5 批处理优化 + +减少 draw call 和状态切换: + +```cpp +/// 几何节点批处理优化 +void render_tree_executor::execute_geometry_batched( + const std::vector& nodes, + vk::CommandBuffer cmd +) { + // 1. 合并所有几何节点的顶点数据 + std::vector merged_vertices; + std::vector merged_indices; + + for (const auto* node : nodes) { + for (const auto& batch : node->batches) { + // 合并顶点和索引 + merge_batch(merged_vertices, merged_indices, batch); + } + } + + // 2. 单次 draw call 渲染所有几何 + if (!merged_vertices.empty()) { + upload_and_draw(cmd, merged_vertices, merged_indices); + } +} +``` + +### 8.6 平台检测和自适应 + +```cpp +/// 渲染配置工厂 +class render_config_factory { +public: + static auto create_for_platform() -> render_config { + render_config config; + + #if defined(__ANDROID__) || defined(__APPLE__) && TARGET_OS_IOS + // 移动端配置 + config.use_transient_attachments = true; + config.max_render_targets = 4; + config.prefer_subpass_optimization = true; + config.effect_resolution_scale = 0.5f; + config.max_nested_depth = 4; + #else + // 桌面端配置 + config.use_transient_attachments = false; + config.max_render_targets = 16; + config.prefer_subpass_optimization = false; + config.effect_resolution_scale = 1.0f; + config.max_nested_depth = 8; + #endif + + return config; + } +}; +``` + +### 8.7 移动端优化总结 + +| 优化策略 | 桌面端 | 移动端 | 说明 | +|----------|--------|--------|------| +| 渲染目标池大小 | 16 | 4 | 减少内存占用 | +| Transient 附件 | 否 | 是 | 利用 TBDR 架构 | +| Subpass 优化 | 否 | 是 | 减少带宽 | +| 最大嵌套深度 | 8 | 4 | 限制复杂度 | +| 后效分辨率 | 100% | 50-75% | 降低计算量 | +| Stencil 回退 | 否 | 是 | 超深度时使用 | + +## 9. 总结 + +本重构方案通过引入**渲染树**数据结构,将线性的命令处理转换为树形的递归处理,从根本上解决了 mask 和 post_effect 嵌套渲染的问题。 + +**核心改进**: +1. **准确表达嵌套关系**:树形结构天然支持任意深度的嵌套 +2. **清晰的作用域**:每个节点的子节点明确属于该节点的作用域 +3. **灵活的渲染策略**:可以根据节点类型选择最优的渲染方式 +4. **可扩展性**:易于添加新的节点类型和渲染效果 +5. **移动端友好**:通过多种优化策略适配移动设备 + +**预期效果**: +- 修复当前 "仅第一个 overlay 能够生效" 的问题 +- 支持任意深度的 mask 和 post_effect 嵌套 +- 在桌面端保持良好的渲染性能 +- 在移动端通过自适应策略保持流畅体验 \ No newline at end of file diff --git a/example/new_pipeline/new_pipeline_example.cpp b/example/new_pipeline/new_pipeline_example.cpp index 1705b13..a19ab66 100644 --- a/example/new_pipeline/new_pipeline_example.cpp +++ b/example/new_pipeline/new_pipeline_example.cpp @@ -6,6 +6,8 @@ #include "vulkan/swapchain.h" #include "vulkan/resource_manager.h" #include "pipeline/render_pipeline.h" +#include "pipeline/render_tree.h" +#include "pipeline/render_tree_builder.h" #include "render_command.h" #include @@ -404,7 +406,7 @@ private: } /// 创建widget测试渲染命令 - auto create_widget_test_commands(render_command_builder& builder) -> std::vector { + auto create_widget_test_commands(render_command_builder& builder) -> render::render_tree { static uint32_t widget_build_count = 0; widget_build_count++; @@ -466,7 +468,10 @@ private: v.build_render_commands(builder); auto commands = builder.get_sorted_commands(); - return commands; + render::render_tree_builder tree_builder; + auto tree = tree_builder.build(commands); + // render::print_render_tree(tree); + return tree; } // ======================================================================== @@ -512,17 +517,17 @@ private: build_commands_call_count++; // auto commands = create_test_commands(); - auto commands = create_widget_test_commands(builder); + auto tree = create_widget_test_commands(builder); // 调试:打印每帧的命令数量 if (frame_count % 60 == 0) { std::cout << "[DEBUG] 帧 " << frame_count << ": build_render_commands 调用次数=" << build_commands_call_count - << ", 生成命令数=" << commands.size() << std::endl; + << std::endl; } - // 使用新管线渲染帧 - if (!pipeline_->render_frame(commands)) { + // 使用新管线渲染帧(直接传递渲染树) + if (!pipeline_->render_frame(tree)) { // 渲染失败,通常是 swapchain 过期 // on_resize 已经在事件回调中处理 std::cerr << "渲染帧失败,跳过..." << std::endl; diff --git a/src/render/pipeline/command_processor.cpp b/src/render/pipeline/command_processor.cpp deleted file mode 100644 index 4237728..0000000 --- a/src/render/pipeline/command_processor.cpp +++ /dev/null @@ -1,333 +0,0 @@ -#include "command_processor.h" -#include "renderer/vertex_types.h" -#include -#include -#include - -namespace mirage { - namespace { - auto get_command_texture_id(const render_command& cmd) -> std::optional { - return std::visit([](const T0& c) -> std::optional { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return c.texture_id; - } - return std::nullopt; - }, cmd); - } - - /// 四边形顶点参数 - struct quad_params { - vec2f_t position; ///< 左上角位置 - vec2f_t size; ///< 尺寸 - std::array color; ///< RGBA 颜色 - std::array data_a; ///< 自定义数据 A (corner_data) - std::array data_b; ///< 自定义数据 B (flags) - std::array uv_rect = {0.0f, 0.0f, 1.0f, 1.0f}; ///< UV 矩形 [u_min, v_min, u_max, v_max] - }; - - /// 向批次添加四边形 - /// @param batch 目标批次 - /// @param params 四边形参数 - void append_quad(draw_batch& batch, const quad_params& params) { - const auto base_index = static_cast(batch.vertices.size()); - - // 创建四个顶点 - ui_vertex vertices[4]; - - const float u_min = params.uv_rect[0]; - const float v_min = params.uv_rect[1]; - const float u_max = params.uv_rect[2]; - const float v_max = params.uv_rect[3]; - - // 顶点 0: 左上角 - vertices[0].position[0] = params.position.x(); - vertices[0].position[1] = params.position.y(); - std::copy_n(params.color.data(), 4, vertices[0].color); - vertices[0].uv[0] = u_min; - vertices[0].uv[1] = v_min; - std::copy_n(params.data_a.data(), 4, vertices[0].data_a); - std::copy_n(params.data_b.data(), 4, vertices[0].data_b); - - // 顶点 1: 右上角 - vertices[1].position[0] = params.position.x() + params.size.x(); - vertices[1].position[1] = params.position.y(); - std::copy_n(params.color.data(), 4, vertices[1].color); - vertices[1].uv[0] = u_max; - vertices[1].uv[1] = v_min; - std::copy_n(params.data_a.data(), 4, vertices[1].data_a); - std::copy_n(params.data_b.data(), 4, vertices[1].data_b); - - // 顶点 2: 右下角 - vertices[2].position[0] = params.position.x() + params.size.x(); - vertices[2].position[1] = params.position.y() + params.size.y(); - std::copy_n(params.color.data(), 4, vertices[2].color); - vertices[2].uv[0] = u_max; - vertices[2].uv[1] = v_max; - std::copy_n(params.data_a.data(), 4, vertices[2].data_a); - std::copy_n(params.data_b.data(), 4, vertices[2].data_b); - - // 顶点 3: 左下角 - vertices[3].position[0] = params.position.x(); - vertices[3].position[1] = params.position.y() + params.size.y(); - std::copy_n(params.color.data(), 4, vertices[3].color); - vertices[3].uv[0] = u_min; - vertices[3].uv[1] = v_max; - std::copy_n(params.data_a.data(), 4, vertices[3].data_a); - std::copy_n(params.data_b.data(), 4, vertices[3].data_b); - - // 添加顶点 - batch.vertices.insert(batch.vertices.end(), vertices, vertices + 4); - - // 添加索引(两个三角形) - batch.indices.push_back(base_index + 0); - batch.indices.push_back(base_index + 1); - batch.indices.push_back(base_index + 2); - batch.indices.push_back(base_index + 0); - batch.indices.push_back(base_index + 2); - batch.indices.push_back(base_index + 3); - } - } - - void command_processor::set_viewport_size(const vec2f_t& size) { - viewport_size_ = size; - } - - auto command_processor::get_command_z_order(const render_command& cmd) -> int32_t { - return std::visit([](const CMD& c) -> int32_t { - using T = std::decay_t; - if constexpr (std::is_same_v) { - // post_effect_command 是 variant,需要再次 visit - return std::visit([](const auto& effect) { - return effect.order.z_order; - }, c); - } - else { - return c.order.z_order; - } - }, cmd); - } - - void command_processor::sort_commands(std::vector& cmds) { - // 使用稳定排序保持相同 z_order 的命令顺序 - std::ranges::stable_sort(cmds, - [](const render_command& a, const render_command& b) { - return get_command_z_order(a) < get_command_z_order(b); - }); - } - - auto command_processor::split_into_segments(std::span cmds) - -> std::vector { - std::vector segments; - - if (cmds.empty()) { - return segments; - } - - // 当前几何段 - render_segment current_geo_segment; - current_geo_segment.type = segment_type::geometry; - - // 当前批次命令 - std::vector current_batch_cmds; - std::optional current_texture_id = std::nullopt; - - auto flush_batch = [&]() { - if (current_batch_cmds.empty()) { - return; - } - - draw_batch batch = build_draw_batch(current_batch_cmds); - batch.texture_id = current_texture_id; // 设置批次纹理ID - - if (!batch.empty()) { - current_geo_segment.batches.push_back(std::move(batch)); - } - current_batch_cmds.clear(); - }; - - auto flush_geometry_segment = [&]() { - flush_batch(); - if (!current_geo_segment.batches.empty()) { - segments.push_back(std::move(current_geo_segment)); - current_geo_segment = render_segment(); - current_geo_segment.type = segment_type::geometry; - } - }; - - for (const auto& cmd : cmds) { - // 检查是否是后效命令 - bool is_post_effect = std::holds_alternative(cmd); - // 检查是否是遮罩开始命令 - bool is_mask_begin = std::holds_alternative(cmd); - // 检查是否是遮罩结束命令 - bool is_mask_end = std::holds_alternative(cmd); - - if (is_post_effect) { - // 遇到后效命令: - // 1. 提交当前批次 - flush_geometry_segment(); - - // 2. 创建后效段 - render_segment effect_segment; - effect_segment.type = segment_type::post_effect; - effect_segment.effect = std::get(cmd); - segments.push_back(std::move(effect_segment)); - - // 重置当前纹理状态 - current_texture_id = std::nullopt; - } - else if (is_mask_begin) { - // 遇到遮罩开始命令: - // 1. 提交当前几何段 - flush_geometry_segment(); - - // 2. 创建遮罩开始段 - render_segment mask_segment; - mask_segment.type = segment_type::mask_begin; - mask_segment.mask_begin = std::get(cmd); - segments.push_back(std::move(mask_segment)); - - // 重置当前纹理状态 - current_texture_id = std::nullopt; - } - else if (is_mask_end) { - // 遇到遮罩结束命令: - // 1. 提交当前几何段 - flush_geometry_segment(); - - // 2. 创建遮罩结束段 - render_segment mask_segment; - mask_segment.type = segment_type::mask_end; - segments.push_back(std::move(mask_segment)); - - // 重置当前纹理状态 - current_texture_id = std::nullopt; - } - else { - // 几何命令 - auto cmd_tex_id = get_command_texture_id(cmd); - - // 检查是否需要切分批次(纹理变化) - if (!current_batch_cmds.empty() && cmd_tex_id != current_texture_id) { - flush_batch(); - } - - current_texture_id = cmd_tex_id; - current_batch_cmds.push_back(cmd); - } - } - - // 处理剩余的几何命令 - flush_geometry_segment(); - - return segments; - } - - auto command_processor::build_draw_batch(std::span geometry_cmds) -> draw_batch { - draw_batch batch; - - // 预估容量(每个命令平均 4 个顶点和 6 个索引) - batch.reserve(geometry_cmds.size() * 4, geometry_cmds.size() * 6); - - auto process_cmd = [&batch](const C& concrete_cmd) { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - // 生成矩形的四边形顶点 - quad_params params; - params.position = concrete_cmd.position; - params.size = concrete_cmd.size; - params.color = { - concrete_cmd.fill_color.r, - concrete_cmd.fill_color.g, - concrete_cmd.fill_color.b, - concrete_cmd.fill_color.a - }; - // rect_data: [size.x, size.y, border_width, border_only] - const auto border_width = concrete_cmd.border_width; - const auto border_only = concrete_cmd.border_only ? 1.0f : 0.0f; - params.data_a = { - concrete_cmd.size.x(), - concrete_cmd.size.y(), - border_width, // border_width - border_only // border_only - }; - // corner_radii: [top_left, top_right, bottom_right, bottom_left] - const auto tl = concrete_cmd.corner_radius.top_left; - const auto tr = concrete_cmd.corner_radius.top_right; - const auto rb = concrete_cmd.corner_radius.bottom_right; - const auto lb = concrete_cmd.corner_radius.bottom_left; - params.data_b = {tl, tr, rb, lb}; - - append_quad(batch, params); - } - else if constexpr (std::is_same_v) { - // TODO: 实现文本渲染 - // 需要字体纹理 atlas 和字形信息 - } - else if constexpr (std::is_same_v) { - // 生成图片的四边形顶点 - quad_params params; - params.position = concrete_cmd.position; - params.size = concrete_cmd.size; - // 颜色 (默认为白色,未来可以从 image_command 支持 tint) - params.color = {1.0f, 1.0f, 1.0f, 1.0f}; - params.data_a = {}; - params.data_b = {}; - - append_quad(batch, params); - } - else if constexpr (std::is_same_v) { - // 调试框:只绘制边框,不填充 - quad_params params; - params.position = concrete_cmd.position; - params.size = concrete_cmd.size; - // 边框颜色 - params.color = { - concrete_cmd.border_color.r, - concrete_cmd.border_color.g, - concrete_cmd.border_color.b, - concrete_cmd.border_color.a - }; - // rect_data: [size.x, size.y, border_width, border_only] - params.data_a = { - concrete_cmd.size.x(), - concrete_cmd.size.y(), - concrete_cmd.border_width, - 1.0f // border_only = true - }; - // corner_radii: [top_left, top_right, bottom_right, bottom_left] - const auto tl = concrete_cmd.corner_radius.top_left; - const auto tr = concrete_cmd.corner_radius.top_right; - const auto rb = concrete_cmd.corner_radius.bottom_right; - const auto lb = concrete_cmd.corner_radius.bottom_left; - params.data_b = {tl, tr, rb, lb}; - - append_quad(batch, params); - } - // 忽略其他命令类型(post_effect, push_layer, pop_layer) - }; - - for (const auto& cmd : geometry_cmds) { - std::visit(process_cmd, cmd); - } - - return batch; - } - - auto command_processor::process(const std::vector& commands) - -> std::vector { - // 如果命令列表为空,直接返回 - if (commands.empty()) { - return {}; - } - - // 1. 复制并排序命令 - std::vector sorted_commands = commands; - sort_commands(sorted_commands); - - // 2. 分割为渲染段 - return split_into_segments(sorted_commands); - } -} // namespace mirage diff --git a/src/render/pipeline/command_processor.h b/src/render/pipeline/command_processor.h deleted file mode 100644 index a0be0e2..0000000 --- a/src/render/pipeline/command_processor.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include "types.h" -#include "render_command.h" -#include "types.h" -#include -#include - -namespace mirage { - /// 命令处理器 - 将渲染命令转换为渲染段 - /// - /// 此类负责: - /// 1. 按 z_order 排序渲染命令 - /// 2. 识别几何命令和后效命令,分割成不同的渲染段 - /// 3. 将连续的几何命令合并为绘制批次 - class command_processor { - public: - command_processor() = default; - ~command_processor() = default; - - // 禁止拷贝 - command_processor(const command_processor&) = delete; - auto operator=(const command_processor&) -> command_processor& = delete; - - // 允许移动 - command_processor(command_processor&&) noexcept = default; - auto operator=(command_processor&&) noexcept -> command_processor& = default; - - /// 处理命令列表,生成渲染段 - /// @param commands 输入的渲染命令列表 - /// @return 渲染段列表 - [[nodiscard]] auto process(const std::vector& commands) - -> std::vector; - - /// 设置视口大小(用于剔除) - /// @param size 视口大小 - void set_viewport_size(const vec2f_t& size); - - private: - vec2f_t viewport_size_{0.0f, 0.0f}; - - /// 按 z_order 排序命令 - /// @param cmds 要排序的命令列表(原地排序) - void sort_commands(std::vector& cmds); - - /// 将命令分割为渲染段 - /// @param cmds 已排序的命令列表 - /// @return 渲染段列表 - [[nodiscard]] auto split_into_segments(std::span cmds) - -> std::vector; - - /// 从几何命令构建绘制批次 - /// @param geometry_cmds 几何命令列表 - /// @return 绘制批次 - [[nodiscard]] auto build_draw_batch(std::span geometry_cmds) - -> draw_batch; - - /// 获取命令的 z_order - /// @param cmd 渲染命令 - /// @return z_order 值 - [[nodiscard]] static auto get_command_z_order(const render_command& cmd) -> int32_t; - }; -} // namespace mirage diff --git a/src/render/pipeline/frame_context_v2.h b/src/render/pipeline/frame_context_v2.h index b3ff0a5..75dbd54 100644 --- a/src/render/pipeline/frame_context_v2.h +++ b/src/render/pipeline/frame_context_v2.h @@ -13,6 +13,11 @@ #include #include +// 前向声明 +namespace mirage { + class mask_renderer; +} + namespace mirage::pass_state { // ============================================================================ // 帧级资源结构体 @@ -354,6 +359,9 @@ namespace mirage::pass_state { /// 工厂函数需要访问私有构造函数 friend auto make_frame_context(frame_resources&& res) -> frame_context; + + /// 允许 mask_renderer 访问私有构造函数(用于遮罩渲染) + friend class mirage::mask_renderer; }; // ============================================================================ diff --git a/src/render/pipeline/mask_renderer.cpp b/src/render/pipeline/mask_renderer.cpp index 9237ff1..09fbae6 100644 --- a/src/render/pipeline/mask_renderer.cpp +++ b/src/render/pipeline/mask_renderer.cpp @@ -44,7 +44,12 @@ namespace mirage { // 预创建一些渲染目标到池中 for (uint32_t i = 0; i < config_.initial_target_pool_size; ++i) { - auto target = std::make_unique(device_, res_mgr_); + auto target = std::make_unique(device_, res_mgr_, + offscreen_target::config{ + .enable_depth_stencil = true, + .depth_stencil_format = vk::Format::eD24UnormS8Uint + } + ); target->initialize(viewport_extent_.width, viewport_extent_.height); available_targets_.push_back(target.get()); target_pool_.push_back(std::move(target)); @@ -293,7 +298,8 @@ namespace mirage { } void mask_renderer::end_frame() { - // 新版 API 使用类型安全的上下文管理,无需清理栈 + // 树形渲染架构中,状态由 frame_context 和 nested_pass_context 管理 + // 无需手动清理遮罩栈 } // ============================================================================ @@ -313,7 +319,12 @@ namespace mirage { return target; } - auto target = std::make_unique(device_, res_mgr_); + auto target = std::make_unique(device_, res_mgr_, + offscreen_target::config{ + .enable_depth_stencil = true, + .depth_stencil_format = vk::Format::eD24UnormS8Uint + } + ); target->initialize(viewport_extent_.width, viewport_extent_.height); auto* ptr = target.get(); target_pool_.push_back(std::move(target)); @@ -327,69 +338,191 @@ namespace mirage { } // ============================================================================ - // 类型安全的遮罩渲染接口实现 (新 API) + // 类型安全的遮罩渲染接口实现 (树形渲染架构) // ============================================================================ auto mask_renderer::begin_mask(pass_state::frame_context&& parent_ctx, const mask_begin_command& cmd_data) - -> std::pair> { - // 获取遮罩渲染目标 - auto* mask_target = acquire_render_target(); + -> std::pair> { + // 从池中获取遮罩渲染目标 + auto* mask_target = acquire_render_target(); - // 使用 nested_pass_context 进入遮罩 Pass - // begin_mask_pass 会: - // 1. 结束父目标的 RenderPass - // 2. 开始遮罩目标的 RenderPass - // 3. 返回嵌套上下文和遮罩帧上下文 - return pass_state::begin_mask_pass(std::move(parent_ctx), *mask_target, true); + // 状态转换(Offscreen -> Mask): + // 1. 结束父目标的 RenderPass + // 2. 开始遮罩目标的 RenderPass(清空) + // 3. 返回嵌套上下文(暂存父状态)和遮罩帧上下文 + + // 在移动之前保存父目标引用 + auto* parent_target = parent_ctx.current_target(); + auto cmd = parent_ctx.cmd(); + auto frame_index = parent_ctx.frame_index(); + + // 结束父 RenderPass(不转换状态,保持在 render_target) + if (parent_target != nullptr) { + parent_target->end_render(cmd, false); } + + // 开始遮罩目标渲染(使用 begin_render_no_transition 以正确更新状态) + // render_pass_clear_ 会处理 Undefined -> ColorAttachment 的转换 + mask_target->begin_render_no_transition(cmd, true); // clear = true + + // 创建嵌套上下文和遮罩帧上下文 + // 注意:将 parent_target 作为 main_target 传递,以便在 end_mask 时恢复 + pass_state::mask_context nested_ctx{}; + pass_state::frame_context mask_frame_ctx( + pass_state::frame_resources{cmd, frame_index, parent_target}, + mask_target + ); + + return {std::move(nested_ctx), std::move(mask_frame_ctx)}; +} auto mask_renderer::begin_nested_mask(pass_state::frame_context&& parent_ctx, const mask_begin_command& cmd_data) - -> std::pair> { - // 获取内层遮罩渲染目标 - auto* inner_mask_target = acquire_render_target(); + -> std::pair> { + // 从池中获取内层遮罩渲染目标 + auto* inner_mask_target = acquire_render_target(); - // 使用 nested_pass_context 进入嵌套遮罩 Pass - return pass_state::begin_nested_mask_pass(std::move(parent_ctx), *inner_mask_target, true); + // 状态转换(Mask -> Mask,深度+1) + // 在移动之前保存外层遮罩目标引用 + auto* outer_mask_target = parent_ctx.current_target(); + auto cmd = parent_ctx.cmd(); + auto frame_index = parent_ctx.frame_index(); + + // 结束外层遮罩 RenderPass(不转换状态) + if (outer_mask_target != nullptr) { + outer_mask_target->end_render(cmd, false); } + + // 开始内层遮罩目标渲染 + inner_mask_target->begin_render_no_transition(cmd, true); // clear = true + + // 创建嵌套上下文和内层遮罩帧上下文 + // 注意:将 outer_mask_target 作为 main_target 传递,以便在 end_nested_mask 时恢复 + pass_state::nested_mask_context nested_ctx{}; + pass_state::frame_context inner_mask_frame_ctx( + pass_state::frame_resources{cmd, frame_index, outer_mask_target}, + inner_mask_target + ); + + return {std::move(nested_ctx), std::move(inner_mask_frame_ctx)}; +} auto mask_renderer::end_mask(pass_state::mask_context&& nested_ctx, pass_state::frame_context&& mask_ctx) - -> std::pair, pass_state::mask_pass_result> { - // 获取遮罩边界(从当前遮罩目标) - aabb2d_t bounds{}; + -> std::pair, pass_state::mask_pass_result> { +// 获取遮罩边界(可用于优化合成区域) + aabb2d_t bounds{}; - // 使用 end_mask_pass 结束遮罩 Pass - // 这会: - // 1. 结束遮罩目标的 RenderPass,转换为 shader read - // 2. 重新开始父目标的 RenderPass - // 3. 返回父上下文和遮罩结果 - return pass_state::end_mask_pass(std::move(nested_ctx), std::move(mask_ctx), bounds); + // 状态转换(Mask -> Offscreen): + // 1. 结束遮罩目标的 RenderPass,转换图像布局为 shader read + // 2. 恢复父目标的 RenderPass(LoadOp::Load,保留之前的内容) + // 3. 返回父上下文和遮罩结果(包含遮罩纹理视图) + + // 获取遮罩目标和父目标 + offscreen_target* mask_target = mask_ctx.current_target(); + offscreen_target* parent_target = mask_ctx.main_target(); + + // 获取遮罩纹理视图(在结束渲染前获取) + vk::ImageView mask_view = (mask_target != nullptr) ? mask_target->view() : vk::ImageView{}; + + // 提取资源 + auto cmd = mask_ctx.cmd(); + auto frame_index = mask_ctx.frame_index(); + + // 结束遮罩渲染 + if (mask_target != nullptr) { + // 结束 RenderPass 并转换到 ShaderRead 状态 + mask_target->end_render(cmd, true); } + + // 恢复父 Pass 渲染(使用 begin_render_no_transition,因为图像已经在正确的布局) + if (parent_target != nullptr) { + parent_target->begin_render_no_transition(cmd, false); // clear = false (Load) + } + + // 构造父状态的上下文 + pass_state::frame_context parent_ctx( + pass_state::frame_resources{cmd, frame_index, parent_target}, + parent_target + ); + + // 构造遮罩结果 + pass_state::mask_pass_result result{ + parent_target, + mask_view, + bounds, + mask_target + }; + + return {std::move(parent_ctx), std::move(result)}; +} auto mask_renderer::end_nested_mask(pass_state::nested_mask_context&& nested_ctx, pass_state::frame_context&& inner_mask_ctx) - -> std::pair, pass_state::mask_pass_result> { - // 获取遮罩边界 - aabb2d_t bounds{}; + -> std::pair, pass_state::mask_pass_result> { +// 获取遮罩边界 + aabb2d_t bounds{}; - // 使用 end_nested_mask_pass 结束嵌套遮罩 Pass - return pass_state::end_nested_mask_pass(std::move(nested_ctx), std::move(inner_mask_ctx), bounds); + // 状态转换(Mask -> Mask,深度-1) + // 结束内层遮罩,返回外层遮罩上下文 + + // 获取内层遮罩目标和外层遮罩目标 + offscreen_target* inner_mask_target = inner_mask_ctx.current_target(); + offscreen_target* outer_mask_target = inner_mask_ctx.main_target(); + + // 获取遮罩纹理视图 + vk::ImageView mask_view = (inner_mask_target != nullptr) + ? inner_mask_target->view() + : vk::ImageView{}; + + // 提取资源 + auto cmd = inner_mask_ctx.cmd(); + auto frame_index = inner_mask_ctx.frame_index(); + + // 结束内层遮罩渲染 + if (inner_mask_target != nullptr) { + // 结束 RenderPass 并转换到 ShaderRead 状态 + inner_mask_target->end_render(cmd, true); } + + // 恢复外层遮罩 Pass(使用 begin_render_no_transition) + if (outer_mask_target != nullptr) { + outer_mask_target->begin_render_no_transition(cmd, false); // clear = false (Load) + } + + // 构造外层遮罩上下文 + pass_state::frame_context outer_ctx( + pass_state::frame_resources{cmd, frame_index, outer_mask_target}, + outer_mask_target + ); + + // 构造遮罩结果 + pass_state::mask_pass_result result{ + outer_mask_target, + mask_view, + bounds, + inner_mask_target + }; + + return {std::move(outer_ctx), std::move(result)}; +} auto mask_renderer::get_current_target() const -> offscreen_target* { - // 新版 API 通过 frame_context 管理当前目标 + // 树形渲染架构中,当前目标由 frame_context 管理 + // 此方法保留用于兼容性,但在新架构中不再使用 return nullptr; } auto mask_renderer::is_in_mask() const -> bool { - // 新版 API 通过类型状态管理遮罩状态 + // 树形渲染架构中,遮罩状态由类型系统(frame_context 的状态标签)管理 + // 此方法保留用于兼容性,但在新架构中不再使用 return false; } auto mask_renderer::get_mask_depth() const -> size_t { - // 新版 API 通过 nested_pass_context 管理嵌套深度 + // 树形渲染架构中,嵌套深度由 render_tree_executor 的递归调用栈管理 + // 此方法保留用于兼容性,但在新架构中不再使用 return 0; } diff --git a/src/render/pipeline/mask_renderer.h b/src/render/pipeline/mask_renderer.h index 5b60508..5edf1fc 100644 --- a/src/render/pipeline/mask_renderer.h +++ b/src/render/pipeline/mask_renderer.h @@ -2,13 +2,14 @@ /// @file mask_renderer.h /// @brief 遮罩渲染器 - 使用类型安全帧上下文管理遮罩渲染 -/// @details 支持嵌套遮罩渲染,通过 nested_pass_context 正确管理父子 Pass 关系 +/// @details 在树形渲染架构中提供遮罩渲染功能,通过 nested_pass_context 管理状态转换。 +/// 递归渲染逻辑由 render_tree_executor 管理,mask_renderer 负责状态转换和遮罩合成。 /// @see docs/DESIGN_RENDER_PIPELINE_REFACTOR_2025.md +/// @see render_tree_executor.h 渲染树执行器 #include "types.h" #include "offscreen_target.h" #include "frame_context_v2.h" -#include "nested_pass_context.h" #include "pass_state_tags.h" #include "vulkan/logical_device.h" #include "vulkan/resource_manager.h" @@ -41,13 +42,18 @@ namespace mirage { /// /// 负责遮罩效果的渲染,支持多种遮罩形状(圆形、椭圆、矩形、圆角矩形)。 /// 使用 SDF(有向距离场)算法实现高质量的遮罩效果。 - /// 支持嵌套遮罩(通过 nested_pass_context 实现类型安全的状态管理)。 /// - /// @details 新的类型安全接口确保: - /// - begin_mask 消耗父上下文,返回嵌套上下文 + 遮罩帧上下文 - /// - 遮罩渲染在 mask_pass 状态中进行 - /// - end_mask 返回父上下文 + 遮罩结果 - /// - composite_mask 在恢复的父 Pass 中绘制遮罩四边形 + /// @details 在树形渲染架构中的角色: + /// - **状态转换**:通过 nested_pass_context 管理 Pass 状态的进入和退出 + /// - **遮罩合成**:将遮罩内容合成到父渲染目标 + /// - **递归支持**:配合 render_tree_executor 实现嵌套遮罩的递归渲染 + /// + /// @details 类型安全接口: + /// - begin_mask/begin_nested_mask:消耗父上下文,返回嵌套上下文 + 遮罩帧上下文 + /// - end_mask/end_nested_mask:返回父上下文 + 遮罩结果 + /// - composite_mask:在父 Pass 中绘制遮罩四边形,应用遮罩效果 + /// + /// @note 递归渲染逻辑由 render_tree_executor 管理,mask_renderer 只负责状态转换 class mask_renderer { public: /// @brief 配置参数 @@ -93,7 +99,8 @@ namespace mirage { // ======================================================================== /// @brief 开始遮罩渲染 - 从离屏 Pass 进入遮罩 Pass - /// @details 暂存父上下文,创建嵌套遮罩 Pass,进入遮罩渲染状态 + /// @details 管理状态转换:结束父 Pass,开始遮罩 Pass。 + /// 递归渲染子节点由 render_tree_executor 负责。 /// /// @param parent_ctx 父级帧上下文(离屏 Pass,右值引用确保所有权转移) /// @param cmd_data 遮罩开始命令数据 @@ -103,9 +110,10 @@ namespace mirage { /// @post 返回的 frame_context 处于 mask_pass_tag 状态 /// /// @code - /// auto [nested_ctx, mask_frame_ctx] = mask_renderer.begin_mask( + /// // 由 render_tree_executor 调用: + /// auto [nested_ctx, mask_ctx] = mask_renderer.begin_mask( /// std::move(offscreen_ctx), mask_begin_cmd); - /// // 在 mask_frame_ctx 中绘制遮罩内容... + /// // executor 递归渲染子节点到 mask_ctx... /// @endcode [[nodiscard]] auto begin_mask(pass_state::frame_context&& parent_ctx, @@ -113,7 +121,7 @@ namespace mirage { -> std::pair>; /// @brief 开始嵌套遮罩渲染 - 从遮罩 Pass 进入更深层遮罩 Pass - /// @details 用于遮罩内部嵌套遮罩的场景 + /// @details 用于遮罩内部嵌套遮罩的场景。管理状态转换,递归由 executor 负责。 /// /// @param parent_ctx 父级帧上下文(遮罩 Pass,右值引用确保所有权转移) /// @param cmd_data 遮罩开始命令数据 @@ -127,7 +135,7 @@ namespace mirage { -> std::pair>; /// @brief 结束遮罩渲染 - 从遮罩 Pass 返回离屏 Pass - /// @details 结束遮罩渲染,恢复父上下文状态,返回遮罩结果供合成使用 + /// @details 管理状态转换:结束遮罩 Pass,恢复父 Pass,返回遮罩结果。 /// /// @param nested_ctx 嵌套上下文(右值引用确保所有权转移) /// @param mask_ctx 遮罩帧上下文(右值引用确保所有权转移) @@ -137,10 +145,10 @@ namespace mirage { /// @post 返回的 frame_context 处于 offscreen_pass_tag 状态 /// /// @code + /// // 由 render_tree_executor 调用: /// auto [offscreen_ctx, result] = mask_renderer.end_mask( - /// std::move(nested_ctx), std::move(mask_frame_ctx)); - /// // 使用 result 进行遮罩合成 - /// mask_renderer.composite_mask(offscreen_ctx, result); + /// std::move(nested_ctx), std::move(mask_ctx)); + /// // executor 调用 composite_mask 合成遮罩 /// @endcode [[nodiscard]] auto end_mask(pass_state::mask_context&& nested_ctx, @@ -159,18 +167,21 @@ namespace mirage { -> std::pair, pass_state::mask_pass_result>; /// @brief 合成遮罩到父目标 - /// @details 在父 Pass 中绘制遮罩四边形,应用遮罩效果 + /// @details 在父 Pass 中绘制遮罩四边形,应用遮罩效果。 + /// 使用遮罩纹理作为采样源,将遮罩内容混合到父目标。 /// /// @tparam ParentState 父 Pass 的状态标签类型 /// @param ctx 父帧上下文引用(非 const,可能更新状态) - /// @param result 遮罩渲染结果 - /// @param params 遮罩参数 - /// @param bounds 内容边界 + /// @param result 遮罩渲染结果(包含遮罩纹理视图) + /// @param params 遮罩参数(形状、软化等) + /// @param bounds 内容边界(用于定位遮罩四边形) /// /// @pre ctx 必须处于活动的 Pass 状态(offscreen_pass_tag 或 mask_pass_tag) /// /// @code - /// mask_renderer.composite_mask(offscreen_ctx, result, mask_params, bounds); + /// // 由 render_tree_executor 调用: + /// mask_renderer.composite_mask(offscreen_ctx, std::move(result), + /// mask_params, bounds); /// @endcode template void composite_mask(pass_state::frame_context& ctx, diff --git a/src/render/pipeline/nested_pass_context.h b/src/render/pipeline/nested_pass_context.h deleted file mode 100644 index 5507861..0000000 --- a/src/render/pipeline/nested_pass_context.h +++ /dev/null @@ -1,395 +0,0 @@ -#pragma once - -/// @file nested_pass_context.h -/// @brief 嵌套 Pass 上下文管理 - 支持遮罩渲染等场景 -/// @details 实现嵌套 Pass 上下文,解决遮罩渲染需要暂存父上下文的问题 -/// @see docs/DESIGN_RENDER_PIPELINE_REFACTOR_2025.md 第3.3节 - -#include "frame_context_v2.h" -#include "offscreen_target.h" -#include "pass_state_tags.h" -#include "types.h" - -#include -#include - -namespace mirage::pass_state { - // ============================================================================ - // 遮罩渲染中间结果 - // ============================================================================ - - /// @brief 遮罩渲染的中间结果 - /// @details 包含合成遮罩所需的所有信息,用于在结束遮罩 Pass 后应用遮罩效果 - /// - /// @note 此结构体设计为仅可移动(move-only),确保资源所有权明确 - /// - /// @code - /// // 使用示例: - /// auto [ctx, result] = end_mask_pass(std::move(mask_ctx), std::move(mask_frame_ctx)); - /// // 使用 result.mask_texture_view 进行遮罩合成 - /// @endcode - struct mask_pass_result { - /// 父渲染目标 - 遮罩应用的目标 - offscreen_target* parent_target = nullptr; - - /// 遮罩纹理视图 - 用于采样遮罩内容 - vk::ImageView mask_texture_view{}; - - /// 遮罩边界 - 用于优化遮罩应用区域 - aabb2d_t bounds{}; - - /// 遮罩目标指针 - 保持遮罩渲染目标的引用 - offscreen_target* mask_target = nullptr; - - /// @name 拷贝控制 - /// @{ - - /// 禁止拷贝构造 - mask_pass_result(const mask_pass_result&) = delete; - - /// 禁止拷贝赋值 - auto operator=(const mask_pass_result&) -> mask_pass_result& = delete; - - /// @} - - /// @name 移动语义 - /// @{ - - /// 允许移动构造 - mask_pass_result(mask_pass_result&&) noexcept = default; - - /// 允许移动赋值 - auto operator=(mask_pass_result&&) noexcept -> mask_pass_result& = default; - - /// @} - - /// 默认构造函数 - mask_pass_result() = default; - - /// 带参数的构造函数 - /// @param parent 父渲染目标指针 - /// @param mask_view 遮罩纹理视图 - /// @param mask_bounds 遮罩边界 - /// @param mask 遮罩渲染目标指针 - mask_pass_result(offscreen_target* parent, - vk::ImageView mask_view, - const aabb2d_t& mask_bounds, - offscreen_target* mask) : parent_target(parent) - , mask_texture_view(mask_view) - , bounds(mask_bounds) - , mask_target(mask) { - } - }; - - // ============================================================================ - // 嵌套 Pass 上下文模板类 - // ============================================================================ - - /// @brief 嵌套 Pass 上下文管理器 - /// @details 管理嵌套渲染 Pass 的状态转换,暂存父上下文资源并在退出时恢复 - /// - /// @tparam ParentState 父 Pass 的状态标签类型 - /// @tparam NestedState 嵌套 Pass 的状态标签类型 - /// @tparam Depth 嵌套深度(默认为1,支持多层嵌套) - /// - /// @note 嵌套上下文"暂存"父上下文的资源,退出嵌套时恢复父上下文 - /// - /// @code - /// // 使用示例: - /// auto [nested_ctx, mask_frame_ctx] = nested_pass_context::enter( - /// std::move(parent_ctx), mask_target, true); - /// // 在 mask_frame_ctx 中绘制遮罩内容... - /// auto parent_ctx = nested_ctx.exit(std::move(mask_frame_ctx)); - /// @endcode - /// - /// @see pass_state_tags.h 状态标签定义 - /// @see frame_context_v2.h 帧上下文定义 - template - class nested_pass_context { - public: - /// 父状态标签类型 - using parent_state_tag = ParentState; - - /// 嵌套状态标签类型 - using nested_state_tag = NestedState; - - /// 嵌套深度 - static constexpr std::size_t depth = Depth; - - /// @name 拷贝控制 - /// @{ - - /// 禁止拷贝构造 - 嵌套上下文是唯一的资源持有者 - nested_pass_context(const nested_pass_context&) = delete; - - /// 禁止拷贝赋值 - auto operator=(const nested_pass_context&) -> nested_pass_context& = delete; - - /// @} - - /// @name 移动语义 - /// @{ - - /// 允许移动构造 - 转移资源所有权 - nested_pass_context(nested_pass_context&&) noexcept = default; - - /// 允许移动赋值 - auto operator=(nested_pass_context&&) noexcept -> nested_pass_context& = default; - - /// @} - - /// @brief 从父上下文进入嵌套 Pass - /// @details 暂存父上下文资源,创建嵌套 Pass 的帧上下文 - /// - /// @param parent 父帧上下文(右值引用,确保所有权转移) - /// @param nested_target 嵌套渲染目标引用 - /// @param clear 是否在开始时清除渲染目标 - /// @return 嵌套上下文与嵌套帧上下文的 pair - /// - /// @pre 当前状态必须是 ParentState - /// @post 返回的帧上下文处于 NestedState 状态 - /// - /// @code - /// auto [nested_ctx, nested_frame_ctx] = nested_pass_context::enter( - /// std::move(parent_ctx), mask_target, true); - /// @endcode - [[nodiscard]] - static auto enter(frame_context&& parent, - offscreen_target& nested_target, - bool clear) - -> std::pair> { - // 编译时验证状态转换合法性 - static_assert(is_valid_transition_v, - "Invalid transition: ParentState -> NestedState"); - - // 获取父目标(在移动资源前获取) - auto* parent_target = parent.current_target(); - - // 结束父 Pass 的渲染(准备好进行嵌套渲染) - // 注意:这里不调用 end_render,因为我们会在退出时恢复父 Pass - if (parent_target != nullptr) { - // 结束父 RenderPass,但不转换为 shader_read(会在退出时恢复) - parent_target->end_render(parent.cmd(), false); - } - - // 提取父资源 - frame_resources parent_resources{ - parent.cmd(), - parent.frame_index(), - parent.main_target() - }; - - // 开始嵌套目标渲染 - nested_target.begin_render(parent_resources.cmd, clear); - - // 创建嵌套帧上下文 - frame_resources nested_resources{ - parent_resources.cmd, - parent_resources.frame_index, - parent_resources.main_target - }; - - // 构造嵌套上下文和嵌套帧上下文 - auto nested_ctx = nested_pass_context(std::move(parent_resources), parent_target); - auto nested_frame_ctx = frame_context( - std::move(nested_resources), - &nested_target - ); - - return {std::move(nested_ctx), std::move(nested_frame_ctx)}; - } - - /// @brief 退出嵌套 Pass,返回父上下文 - /// @details 结束嵌套渲染,恢复父上下文状态 - /// - /// @param nested 嵌套帧上下文(右值引用,确保所有权转移) - /// @return 处于 ParentState 状态的帧上下文 - /// - /// @pre 嵌套帧上下文必须处于 NestedState 状态 - /// @post 返回的上下文处于 ParentState 状态 - /// - /// @code - /// auto parent_ctx = nested_ctx.exit(std::move(nested_frame_ctx)); - /// @endcode - [[nodiscard]] - auto exit(frame_context&& nested) -> frame_context { - // 编译时验证状态转换合法性 - static_assert(is_valid_transition_v, - "Invalid transition: NestedState -> ParentState"); - - // 获取嵌套目标 - offscreen_target* nested_target = nested.current_target(); - - // 结束嵌套渲染,准备用于着色器读取 - if (nested_target != nullptr) { - nested_target->end_render(nested.cmd(), true); - } - - // 恢复父 Pass 渲染 - if (parent_target_ != nullptr) { - // 重新开始父目标的 RenderPass(不清除,保留之前的内容) - parent_target_->begin_render(parent_resources_.cmd, false); - } - - // 构造父状态的上下文 - return frame_context(std::move(parent_resources_), parent_target_); - } - - /// @brief 获取父渲染目标 - /// @return 父渲染目标指针 - [[nodiscard]] auto parent_target() const noexcept -> offscreen_target* { - return parent_target_; - } - - private: - /// 暂存的父级帧资源 - frame_resources parent_resources_; - - /// 父渲染目标指针 - offscreen_target* parent_target_ = nullptr; - - /// 私有构造函数(通过 enter 静态方法创建) - /// @param resources 父级帧资源(移动语义) - /// @param target 父渲染目标指针 - nested_pass_context(frame_resources&& resources, offscreen_target* target) : - parent_resources_(std::move(resources)) - , parent_target_(target) { - } - - /// 允许 frame_context 访问私有成员(用于状态转换) - template - friend class frame_context; - }; - - // ============================================================================ - // 遮罩特化别名 - // ============================================================================ - - /// @brief 遮罩上下文别名 - /// @details 从离屏 Pass 进入遮罩 Pass 的嵌套上下文 - using mask_context = nested_pass_context; - - /// @brief 嵌套遮罩上下文别名(深度2) - /// @details 用于遮罩内部嵌套遮罩的场景 - using nested_mask_context = nested_pass_context; - - // ============================================================================ - // 辅助函数 - // ============================================================================ - - /// @brief 开始遮罩渲染 - /// @details 从离屏 Pass 状态进入遮罩 Pass 状态的便捷函数 - /// - /// @param ctx 离屏 Pass 帧上下文(右值引用,确保所有权转移) - /// @param mask_target 遮罩渲染目标引用 - /// @param clear 是否在开始时清除遮罩目标(默认为 true) - /// @return 遮罩上下文与遮罩帧上下文的 pair - /// - /// @pre ctx 必须处于 offscreen_pass_tag 状态 - /// @post 返回的帧上下文处于 mask_pass_tag 状态 - /// - /// @code - /// auto [mask_ctx, mask_frame_ctx] = begin_mask_pass(std::move(offscreen_ctx), mask_target); - /// // 在 mask_frame_ctx 中绘制遮罩内容... - /// @endcode - [[nodiscard]] - inline auto begin_mask_pass(frame_context&& ctx, - offscreen_target& mask_target, - bool clear = true) - -> std::pair> { - return mask_context::enter(std::move(ctx), mask_target, clear); - } - - /// @brief 结束遮罩渲染,返回中间结果 - /// @details 结束遮罩 Pass,返回父上下文和遮罩渲染结果 - /// - /// @param ctx 遮罩上下文(右值引用,确保所有权转移) - /// @param mask_ctx 遮罩帧上下文(右值引用,确保所有权转移) - /// @param bounds 遮罩边界(可选,用于优化合成区域) - /// @return 父帧上下文与遮罩结果的 pair - /// - /// @pre mask_ctx 必须处于 mask_pass_tag 状态 - /// @post 返回的帧上下文处于 offscreen_pass_tag 状态 - /// - /// @code - /// auto [offscreen_ctx, result] = end_mask_pass(std::move(mask_ctx), std::move(mask_frame_ctx)); - /// // 使用 result.mask_texture_view 进行遮罩合成 - /// @endcode - [[nodiscard]] - inline auto end_mask_pass(mask_context&& ctx, - frame_context&& mask_ctx, - const aabb2d_t& bounds = aabb2d_t{}) - -> std::pair, mask_pass_result> { - // 获取遮罩目标(在移动前获取) - offscreen_target* mask_target = mask_ctx.current_target(); - offscreen_target* parent_target = ctx.parent_target(); - - // 获取遮罩纹理视图(在结束渲染前获取) - vk::ImageView mask_view = (mask_target != nullptr) ? mask_target->view() : vk::ImageView{}; - - // 退出嵌套,返回父上下文 - auto parent_ctx = ctx.exit(std::move(mask_ctx)); - - // 构造遮罩结果 - mask_pass_result result{ - parent_target, - mask_view, - bounds, - mask_target - }; - - return {std::move(parent_ctx), std::move(result)}; - } - - /// @brief 在嵌套遮罩中开始更深层遮罩渲染 - /// @details 用于遮罩内部嵌套遮罩的场景 - /// - /// @param ctx 遮罩 Pass 帧上下文(右值引用,确保所有权转移) - /// @param inner_mask_target 内层遮罩渲染目标引用 - /// @param clear 是否在开始时清除遮罩目标(默认为 true) - /// @return 嵌套遮罩上下文与内层遮罩帧上下文的 pair - /// - /// @pre ctx 必须处于 mask_pass_tag 状态 - /// @post 返回的帧上下文处于 mask_pass_tag 状态(深度+1) - [[nodiscard]] - inline auto begin_nested_mask_pass(frame_context&& ctx, - offscreen_target& inner_mask_target, - bool clear = true) - -> std::pair> { - return nested_mask_context::enter(std::move(ctx), inner_mask_target, clear); - } - - /// @brief 结束嵌套遮罩渲染 - /// @details 结束内层遮罩 Pass,返回外层遮罩上下文 - /// - /// @param ctx 嵌套遮罩上下文(右值引用,确保所有权转移) - /// @param inner_mask_ctx 内层遮罩帧上下文(右值引用,确保所有权转移) - /// @return 外层遮罩帧上下文与内层遮罩结果的 pair - [[nodiscard]] - inline auto end_nested_mask_pass(nested_mask_context&& ctx, - frame_context&& inner_mask_ctx, - const aabb2d_t& bounds = aabb2d_t{}) - -> std::pair, mask_pass_result> { - // 获取遮罩目标(在移动前获取) - offscreen_target* inner_mask_target = inner_mask_ctx.current_target(); - offscreen_target* outer_mask_target = ctx.parent_target(); - - // 获取遮罩纹理视图 - vk::ImageView mask_view = (inner_mask_target != nullptr) - ? inner_mask_target->view() - : vk::ImageView{}; - - // 退出嵌套,返回外层遮罩上下文 - auto outer_ctx = ctx.exit(std::move(inner_mask_ctx)); - - // 构造遮罩结果 - mask_pass_result result{ - outer_mask_target, - mask_view, - bounds, - inner_mask_target - }; - - return {std::move(outer_ctx), std::move(result)}; - } -} // namespace mirage::pass_state diff --git a/src/render/pipeline/offscreen_target.cpp b/src/render/pipeline/offscreen_target.cpp index f6f1d25..2ca2978 100644 --- a/src/render/pipeline/offscreen_target.cpp +++ b/src/render/pipeline/offscreen_target.cpp @@ -202,9 +202,10 @@ namespace mirage { dependencies[0].dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; if (config_.enable_depth_stencil) { + dependencies[0].srcStageMask |= vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests; dependencies[0].dstStageMask |= vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests; dependencies[0].dstAccessMask |= vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; - dependencies[0].srcAccessMask |= vk::AccessFlagBits::eDepthStencilAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentRead; + dependencies[0].srcAccessMask |= vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; } // Subpass 0 -> External @@ -217,6 +218,7 @@ namespace mirage { if (config_.enable_depth_stencil) { dependencies[1].srcStageMask |= vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests; + dependencies[1].dstStageMask |= vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests; dependencies[1].srcAccessMask |= vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; dependencies[1].dstAccessMask |= vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; } @@ -443,6 +445,37 @@ namespace mirage { cmd.beginRenderPass(begin_info, vk::SubpassContents::eInline); } + void offscreen_target::begin_render_no_transition(vk::CommandBuffer cmd, bool clear) { + // 假设图像已经处于 ColorAttachmentOptimal 布局,不进行状态转换 + // 直接开始 RenderPass + vk::RenderPassBeginInfo begin_info{}; + begin_info.renderPass = clear ? render_pass_clear_ : render_pass_load_; + begin_info.framebuffer = framebuffer_; + begin_info.renderArea.offset = vk::Offset2D{0, 0}; + begin_info.renderArea.extent = vk::Extent2D{width_, height_}; + + std::vector clear_values; + if (clear) { + vk::ClearValue color_clear{}; + color_clear.color = vk::ClearColorValue{std::array{0.0f, 0.0f, 0.0f, 0.0f}}; + clear_values.push_back(color_clear); + + if (config_.enable_depth_stencil) { + vk::ClearValue depth_clear{}; + depth_clear.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; + clear_values.push_back(depth_clear); + } + + begin_info.clearValueCount = static_cast(clear_values.size()); + begin_info.pClearValues = clear_values.data(); + } + + cmd.beginRenderPass(begin_info, vk::SubpassContents::eInline); + + // 更新内部状态以反映当前布局 + current_state_ = state::render_target; + } + void offscreen_target::end_render(vk::CommandBuffer cmd, bool prepare_for_read) { cmd.endRenderPass(); diff --git a/src/render/pipeline/offscreen_target.h b/src/render/pipeline/offscreen_target.h index f118a5d..be11822 100644 --- a/src/render/pipeline/offscreen_target.h +++ b/src/render/pipeline/offscreen_target.h @@ -54,10 +54,18 @@ namespace mirage { void transition_to(vk::CommandBuffer cmd, state new_state); /// 开始渲染(转换到 render_target 状态) - void begin_render(vk::CommandBuffer cmd, bool clear = false); + void begin_render(vk::CommandBuffer cmd, bool clear); - /// 结束渲染(可选转换到 shader_read) - void end_render(vk::CommandBuffer cmd, bool prepare_for_read = true); + /// 开始渲染(不进行状态转换) + /// @details 假设图像已经处于 ColorAttachmentOptimal 布局 + /// @param cmd 命令缓冲 + /// @param clear 是否清除 + void begin_render_no_transition(vk::CommandBuffer cmd, bool clear); + + /// 结束渲染 + /// @param cmd 命令缓冲 + /// @param prepare_for_read 是否转换到着色器读取状态 + void end_render(vk::CommandBuffer cmd, bool prepare_for_read); /// 获取是否启用深度/模板 [[nodiscard]] auto has_depth_stencil() const -> bool; diff --git a/src/render/pipeline/pass_state_tags.h b/src/render/pipeline/pass_state_tags.h index ccd29b0..d308da2 100644 --- a/src/render/pipeline/pass_state_tags.h +++ b/src/render/pipeline/pass_state_tags.h @@ -5,10 +5,21 @@ /// @details 使用 Typestate 模式实现编译时状态转换验证 /// @see docs/DESIGN_RENDER_PIPELINE_REFACTOR_2025.md 第4节 +#include "types.h" #include #include #include +// 前向声明 +namespace mirage { + class offscreen_target; +} + +// Vulkan 前向声明 +namespace vk { + class ImageView; +} + namespace mirage::pass_state { // ============================================================================ // 状态标签定义 @@ -316,4 +327,62 @@ namespace mirage::pass_state { return "unknown"; } } + + // ============================================================================ + // 遮罩渲染结果 + // ============================================================================ + + /// @brief 遮罩渲染的中间结果 + /// @details 包含合成遮罩所需的所有信息,用于在结束遮罩 Pass 后应用遮罩效果 + struct mask_pass_result { + /// 父渲染目标 - 遮罩应用的目标 + offscreen_target* parent_target = nullptr; + + /// 遮罩纹理视图 - 用于采样遮罩内容 + vk::ImageView mask_texture_view{}; + + /// 遮罩边界 - 用于优化遮罩应用区域 + aabb2d_t bounds{}; + + /// 遮罩目标指针 - 保持遮罩渲染目标的引用 + offscreen_target* mask_target = nullptr; + + /// 禁止拷贝 + mask_pass_result(const mask_pass_result&) = delete; + auto operator=(const mask_pass_result&) -> mask_pass_result& = delete; + + /// 允许移动 + mask_pass_result(mask_pass_result&&) noexcept = default; + auto operator=(mask_pass_result&&) noexcept -> mask_pass_result& = default; + + /// 默认构造函数 + mask_pass_result() = default; + + /// 带参数的构造函数 + mask_pass_result(offscreen_target* parent, + vk::ImageView mask_view, + const aabb2d_t& mask_bounds, + offscreen_target* mask) : parent_target(parent) + , mask_texture_view(mask_view) + , bounds(mask_bounds) + , mask_target(mask) { + } + }; + + // ============================================================================ + // 嵌套上下文类型别名(前向声明) + // ============================================================================ + + /// @brief 遮罩上下文类型(占位符) + /// @details 实际实现在 mask_renderer 中,这里仅作为类型别名 + struct mask_context { + // 占位符结构,实际功能由 mask_renderer 实现 + }; + + /// @brief 嵌套遮罩上下文类型(占位符) + /// @details 实际实现在 mask_renderer 中,这里仅作为类型别名 + struct nested_mask_context { + // 占位符结构,实际功能由 mask_renderer 实现 + }; + } // namespace mirage::pass_state diff --git a/src/render/pipeline/render_pipeline.cpp b/src/render/pipeline/render_pipeline.cpp index ea12917..0c218be 100644 --- a/src/render/pipeline/render_pipeline.cpp +++ b/src/render/pipeline/render_pipeline.cpp @@ -38,14 +38,22 @@ namespace mirage { auto extent = swapchain_.get_extent(); - // 1. 创建命令处理器 - processor_ = std::make_unique(); - processor_->set_viewport_size(vec2f_t{ - static_cast(extent.width), - static_cast(extent.height) - }); + // 1. 创建渲染树构建器 + tree_builder_ = std::make_unique(); - // 2. 创建帧调度器 + // 2. 创建渲染树执行器 + tree_executor_ = std::make_unique(); + + // 3. 创建渲染目标池 + target_pool_ = std::make_unique( + device_, res_mgr_, + render::render_target_pool::config{ + .max_pool_size = 16, + .use_transient_memory = false + } + ); + + // 4. 创建帧调度器 scheduler_ = std::make_unique( device_, swapchain_, @@ -55,7 +63,7 @@ namespace mirage { ); scheduler_->initialize(); - // 3. 创建离屏渲染目标(启用 Depth/Stencil) + // 5. 创建离屏渲染目标(启用 Depth/Stencil) offscreen_ = std::make_unique( device_, res_mgr_, offscreen_target::config{ @@ -65,7 +73,7 @@ namespace mirage { ); offscreen_->initialize(extent.width, extent.height); - // 4. 创建几何渲染器 + // 6. 创建几何渲染器 geometry_ = std::make_unique( device_, res_mgr_, @@ -78,7 +86,7 @@ namespace mirage { // 使用离屏目标的 load RenderPass(因为可能需要多次渲染) geometry_->initialize(offscreen_->render_pass(false)); - // 4.5 创建图片渲染器 + // 7. 创建图片渲染器 imager_ = std::make_unique( device_, res_mgr_, @@ -91,7 +99,7 @@ namespace mirage { ); imager_->initialize(offscreen_->render_pass(false)); - // 4.6 创建遮罩渲染器 + // 8. 创建遮罩渲染器 mask_ = std::make_unique( device_, res_mgr_, @@ -102,26 +110,11 @@ namespace mirage { ); mask_->initialize(offscreen_->render_pass(false), extent); - // 4.7 创建 Stencil 遮罩渲染器 - stencil_mask_ = std::make_unique( - device_, res_mgr_, - stencil_mask_renderer::config{ - .frames_in_flight = config_.frames_in_flight, - .max_nested_depth = 8 - } - ); - - // 初始化 stencil_mask_renderer(需要使用带 Stencil 的 RenderPass) - stencil_mask_->initialize( - offscreen_->render_pass(false), // 使用不清除的 RenderPass - extent - ); - - // 5. 创建后效应用器 + // 9. 创建后效应用器 effects_ = std::make_unique(device_, res_mgr_); effects_->initialize(extent.width, extent.height, config_.frames_in_flight); - // 6. 创建 Blit Pipeline + // 10. 创建 Blit Pipeline create_blit_pipeline(); create_blit_descriptors(); update_blit_descriptor(); @@ -372,8 +365,8 @@ namespace mirage { // 2. 重置后效描述符池(在等待 fence 之后,传递帧索引) effects_->begin_frame(frame_index); - // 3. 处理命令,生成渲染段 - auto segments = processor_->process(commands); + // 3. 构建渲染树 + auto tree = tree_builder_->build(commands); // 4. 开始各渲染器的帧 auto extent = swapchain_.get_extent(); @@ -390,19 +383,127 @@ namespace mirage { static_cast(extent.height) }); - // 添加 stencil_mask_ 的帧开始 - if (stencil_mask_) { - stencil_mask_->begin_frame(frame_index, vec2f_t{ - static_cast(extent.width), - static_cast(extent.height) - }); - } - // 5. 开始离屏渲染 Pass(idle -> offscreen_pass) auto offscreen_ctx = std::move(idle_ctx).begin_offscreen_pass(*offscreen_, true); - // 6. 渲染所有段(状态由类型系统保证) - idle_ctx = render_segments(std::move(offscreen_ctx), segments); + // 6. 执行渲染树 + render::executor_context exec_ctx{ + .geometry = geometry_.get(), + .image = imager_.get(), + .mask = mask_.get(), + .post_effect = effects_.get(), + .pool = target_pool_.get(), + .frame_ctx = &offscreen_ctx, + .time = frame_time_ + }; + tree_executor_->execute(tree, exec_ctx); + + // 7. 结束离屏渲染 Pass + idle_ctx = std::move(offscreen_ctx).end_offscreen_pass(); + + // 8. 准备 Blit 到 Swapchain + // 确保离屏目标在 shader_read 状态 + offscreen_->transition_to(idle_ctx.cmd(), offscreen_target::state::shader_read); + + // 9. 获取当前 swapchain 图像索引(由 begin_frame 获取并保存) + const uint32_t image_index = scheduler_->current_image_index(); + + // 10. 开始 Swapchain Pass(idle -> swapchain_pass) + auto swapchain_ctx = std::move(idle_ctx).begin_swapchain_pass( + scheduler_->swapchain_render_pass(), + scheduler_->get_framebuffer(image_index), + extent + ); + + // 11. Blit 离屏纹理到 Swapchain + blit_to_swapchain(swapchain_ctx, frame_index); + + // 12. 结束 Swapchain Pass(swapchain_pass -> idle) + idle_ctx = std::move(swapchain_ctx).end_swapchain_pass(); + + // 13. 结束各渲染器的帧 + geometry_->end_frame(); + imager_->end_frame(frame_index); + mask_->end_frame(); + + // 14. 重置渲染目标池 + target_pool_->reset_frame(); + + // 15. 完成帧,获取资源用于提交 + auto resources = std::move(idle_ctx).finish(); + + // 16. 结束帧,提交和呈现 + bool present_ok = scheduler_->end_frame(std::move(resources), image_index); + if (!present_ok) { + // 需要重建 Swapchain + return false; + } + + // 更新帧时间(简单递增,实际应用可能需要真实时间) + frame_time_ += 0.016f; // ~60 FPS + + return true; + } + + auto render_pipeline::render_frame(const render::render_tree& tree) -> bool { + if (!initialized_) { + throw std::runtime_error("render_pipeline not initialized"); + } + + // 检查是否有待处理的 resize,在帧开始时执行 + if (resize_pending_) { + perform_resize(); + } + + // 重置后效标志 + has_post_effects_ = false; + + // 1. 开始新帧,获取类型安全的帧上下文(idle 状态) + auto maybe_ctx = scheduler_->begin_frame(); + if (!maybe_ctx.has_value()) { + // 需要重建 Swapchain + return false; + } + auto idle_ctx = std::move(*maybe_ctx); + + // 获取帧索引和命令缓冲(在开始渲染前保存) + const uint32_t frame_index = idle_ctx.frame_index(); + + // 2. 重置后效描述符池(在等待 fence 之后,传递帧索引) + effects_->begin_frame(frame_index); + + // 3. 开始各渲染器的帧 + auto extent = swapchain_.get_extent(); + geometry_->begin_frame(frame_index, vec2f_t{ + static_cast(extent.width), + static_cast(extent.height) + }); + imager_->begin_frame(frame_index, vec2f_t{ + static_cast(extent.width), + static_cast(extent.height) + }); + mask_->begin_frame(frame_index, vec2f_t{ + static_cast(extent.width), + static_cast(extent.height) + }); + + // 4. 开始离屏渲染 Pass(idle -> offscreen_pass) + auto offscreen_ctx = std::move(idle_ctx).begin_offscreen_pass(*offscreen_, true); + + // 5. 执行渲染树 + render::executor_context exec_ctx{ + .geometry = geometry_.get(), + .image = imager_.get(), + .mask = mask_.get(), + .post_effect = effects_.get(), + .pool = target_pool_.get(), + .frame_ctx = &offscreen_ctx, + .time = frame_time_ + }; + tree_executor_->execute(tree, exec_ctx); + + // 6. 结束离屏渲染 Pass + idle_ctx = std::move(offscreen_ctx).end_offscreen_pass(); // 7. 准备 Blit 到 Swapchain // 确保离屏目标在 shader_read 状态 @@ -429,15 +530,13 @@ namespace mirage { imager_->end_frame(frame_index); mask_->end_frame(); - // 添加 stencil_mask_ 的帧结束 - if (stencil_mask_) { - stencil_mask_->end_frame(); - } + // 13. 重置渲染目标池 + target_pool_->reset_frame(); - // 13. 完成帧,获取资源用于提交 + // 14. 完成帧,获取资源用于提交 auto resources = std::move(idle_ctx).finish(); - // 14. 结束帧,提交和呈现 + // 15. 结束帧,提交和呈现 bool present_ok = scheduler_->end_frame(std::move(resources), image_index); if (!present_ok) { // 需要重建 Swapchain @@ -450,86 +549,6 @@ namespace mirage { return true; } - // ============================================================================ - // 渲染段处理(使用类型安全的帧上下文) - // ============================================================================ - - auto render_pipeline::render_segments( - pass_state::frame_context&& ctx, - std::span segments - ) -> pass_state::frame_context { - // 状态由类型系统保证,初始状态处于 offscreen_pass_tag,RenderPass 已经开始 - // - // 使用新的 Stencil 遮罩系统: - // - mask_begin: 调用 stencil_mask_->begin_mask() 写入 Stencil - // - geometry: 设置渲染器的 Stencil 参考值 - // - post_effect: 直接在当前目标上应用后效 - // - mask_end: 调用 stencil_mask_->end_mask() 清理 Stencil - - // 当前活动的 offscreen 上下文 - auto offscreen_ctx = std::move(ctx); - - for (const auto& segment : segments) { - // 获取命令缓冲区 - vk::CommandBuffer cmd = offscreen_ctx.cmd(); - - // 获取当前 Stencil 参考值 - uint8_t stencil_ref = stencil_mask_ ? stencil_mask_->current_stencil_ref() : 0; - - if (segment.type == segment_type::geometry) { - // === 几何段:渲染到当前活动目标 === - // 设置渲染器的 Stencil 参考值 - geometry_->set_stencil_ref(stencil_ref); - imager_->set_stencil_ref(stencil_ref); - - // 渲染所有批次 - for (const auto& batch : segment.batches) { - if (!batch.empty()) { - if (batch.texture_id.has_value()) { - // 有纹理ID,使用图片渲染器 - imager_->render(cmd, batch); - } else { - // 无纹理ID,使用几何渲染器 - geometry_->render(cmd, batch); - } - } - } - } - else if (segment.type == segment_type::post_effect) { - // === 后效段:应用后效到离屏目标 === - // 后效处理简化 - 不再需要特殊的源目标处理 - // 结束当前 offscreen pass 以应用后效 - auto idle_ctx = std::move(offscreen_ctx).end_offscreen_pass(); - - if (segment.effect.has_value()) { - effects_->apply(idle_ctx.cmd(), *offscreen_, segment.effect.value(), frame_time_); - has_post_effects_ = true; - } - - // 重新开始 offscreen pass(不清除,保留内容) - offscreen_ctx = std::move(idle_ctx).begin_offscreen_pass(*offscreen_, false); - } - else if (segment.type == segment_type::mask_begin) { - // === 遮罩开始段:开始 Stencil 遮罩 === - - if (segment.mask_begin.has_value() && stencil_mask_) { - const auto& mask_cmd = segment.mask_begin.value(); - stencil_mask_->begin_mask(cmd, mask_cmd.params, mask_cmd.content_bounds); - } - } - else if (segment.type == segment_type::mask_end) { - // === 遮罩结束段:结束 Stencil 遮罩 === - - if (stencil_mask_) { - stencil_mask_->end_mask(cmd); - } - } - } - - // 返回 idle 状态 - return std::move(offscreen_ctx).end_offscreen_pass(); - } - // ============================================================================ // Blit 到 Swapchain(使用类型安全的帧上下文) // ============================================================================ @@ -651,31 +670,20 @@ namespace mirage { // 2. 重建离屏目标 offscreen_->resize(width, height); - // 3. 更新命令处理器的视口大小 - processor_->set_viewport_size(vec2f_t{ - static_cast(width), - static_cast(height) - }); - - // 4. 重新初始化几何渲染器(因为 RenderPass 可能改变) + // 3. 重新初始化几何渲染器(因为 RenderPass 可能改变) geometry_->cleanup(); geometry_->initialize(offscreen_->render_pass(false)); imager_->cleanup(); imager_->initialize(offscreen_->render_pass(false)); - // 4.5 重新初始化遮罩渲染器 + // 4. 重新初始化遮罩渲染器 mask_->resize(vk::Extent2D{width, height}); - // 4.6 调整 stencil_mask_renderer - if (stencil_mask_) { - stencil_mask_->resize(vk::Extent2D{width, height}); - } - - // 5. 更新后效应用器的尺寸和临时目标 + // 6. 更新后效应用器的尺寸和临时目标 effects_->resize(width, height); - // 6. 更新 Blit Descriptor(因为离屏纹理改变了) + // 7. 更新 Blit Descriptor(因为离屏纹理改变了) update_blit_descriptor(); // 注意:Blit Pipeline 不需要重建,因为它使用动态视口 @@ -702,17 +710,17 @@ namespace mirage { geometry_.reset(); imager_.reset(); mask_.reset(); - - // 清理 stencil_mask_ - if (stencil_mask_) { - stencil_mask_->cleanup(); - stencil_mask_.reset(); - } - effects_.reset(); offscreen_.reset(); scheduler_.reset(); - processor_.reset(); + + // 清理新架构组件 + if (target_pool_) { + target_pool_->cleanup(); + target_pool_.reset(); + } + tree_executor_.reset(); + tree_builder_.reset(); // 清理 Blit 资源 cleanup_blit_resources(); diff --git a/src/render/pipeline/render_pipeline.h b/src/render/pipeline/render_pipeline.h index 1c7817e..0e669f9 100644 --- a/src/render/pipeline/render_pipeline.h +++ b/src/render/pipeline/render_pipeline.h @@ -2,15 +2,15 @@ #include "types.h" #include "offscreen_target.h" -#include "command_processor.h" +#include "render_tree_builder.h" +#include "render_tree_executor.h" +#include "render_target_pool.h" #include "geometry_renderer.h" #include "image_renderer.h" #include "mask_renderer.h" -#include "stencil_mask_renderer.h" #include "post_effect_applicator.h" #include "frame_scheduler.h" #include "frame_context_v2.h" -#include "nested_pass_context.h" #include "vulkan/logical_device.h" #include "vulkan/swapchain.h" #include "vulkan/resource_manager.h" @@ -26,8 +26,8 @@ namespace mirage { /// 渲染管线 - 整合所有组件的主入口 /// /// 职责: - /// 1. 管理所有渲染子组件(命令处理、几何渲染、后效、帧调度等) - /// 2. 协调完整的渲染流程:命令处理 → 离屏渲染 → 后效应用 → Blit到Swapchain + /// 1. 管理所有渲染子组件(树构建、树执行、几何渲染、后效、帧调度等) + /// 2. 协调完整的渲染流程:构建渲染树 → 执行渲染树 → Blit到Swapchain /// 3. 处理窗口大小变化和资源重建 class render_pipeline { public: @@ -64,6 +64,11 @@ namespace mirage { /// @return 是否成功渲染(返回 false 表示需要重建 Swapchain) [[nodiscard]] auto render_frame(const std::vector& commands) -> bool; + /// 渲染一帧(使用预构建的渲染树) + /// @param tree 预构建的渲染树 + /// @return 是否成功渲染(返回 false 表示需要重建 Swapchain) + [[nodiscard]] auto render_frame(const render::render_tree& tree) -> bool; + /// 处理窗口大小变化 /// @param width 新宽度 /// @param height 新高度 @@ -91,14 +96,15 @@ namespace mirage { // ========================================================================= // 子组件 // ========================================================================= - std::unique_ptr processor_; - std::unique_ptr offscreen_; - std::unique_ptr geometry_; - std::unique_ptr imager_; - std::unique_ptr mask_; - std::unique_ptr stencil_mask_; - std::unique_ptr effects_; - std::unique_ptr scheduler_; + std::unique_ptr tree_builder_; + std::unique_ptr tree_executor_; + std::unique_ptr target_pool_; + std::unique_ptr offscreen_; + std::unique_ptr geometry_; + std::unique_ptr imager_; + std::unique_ptr mask_; + std::unique_ptr effects_; + std::unique_ptr scheduler_; // ========================================================================= // Blit Pipeline 资源(离屏 → Swapchain) @@ -138,16 +144,6 @@ namespace mirage { /// 更新 Blit Descriptor Set(绑定离屏纹理) void update_blit_descriptor(); - /// 渲染所有渲染段(使用类型安全的帧上下文) - /// @param ctx 处于 offscreen_pass_tag 状态的帧上下文引用 - /// @param segments 渲染段列表 - /// @return 返回处理完遮罩和后效后的帧上下文(可能仍在 offscreen_pass 或已结束) - /// @note 状态由类型系统保证,不再需要 render_pass_active 布尔变量 - [[nodiscard]] auto render_segments( - pass_state::frame_context&& ctx, - std::span segments - ) -> pass_state::frame_context; - /// Blit 离屏纹理到 Swapchain /// @param ctx 处于 swapchain_pass_tag 状态的帧上下文引用 /// @param frame_index 当前帧索引(用于选择描述符集) diff --git a/src/render/pipeline/render_target_pool.cpp b/src/render/pipeline/render_target_pool.cpp new file mode 100644 index 0000000..46bd55b --- /dev/null +++ b/src/render/pipeline/render_target_pool.cpp @@ -0,0 +1,162 @@ +#include "render_target_pool.h" +#include +#include + +namespace mirage::render { + + render_target_pool::render_target_pool( + logical_device& device, + resource_manager& res_mgr, + const config& cfg + ) : device_(device) + , res_mgr_(res_mgr) + , config_(cfg) { + } + + render_target_pool::~render_target_pool() { + cleanup(); + } + + auto render_target_pool::acquire(uint32_t width, uint32_t height, vk::Format format, bool enable_depth) -> offscreen_target* { + target_key key{ + .width = width, + .height = height, + .format = format, + .enable_depth_stencil = enable_depth, + .depth_stencil_format = vk::Format::eD24UnormS8Uint // 使用默认深度格式 + }; + + // 1. 尝试查找完全匹配的可用目标 + for (auto& item : pool_) { + if (!item.in_use && item.key == key) { + item.in_use = true; + item.last_used_frame = current_frame_; + return item.target.get(); + } + } + + // 2. 如果没找到,且池未满,创建新目标 + if (pool_.size() < config_.max_pool_size) { + auto target_config = offscreen_target::config{ + .enable_depth_stencil = enable_depth, + .depth_stencil_format = key.depth_stencil_format + }; + + auto target = std::make_unique(device_, res_mgr_, target_config); + target->initialize(width, height); + + pool_.push_back(pooled_target{ + .target = std::move(target), + .key = key, + .last_used_frame = current_frame_, + .in_use = true + }); + + return pool_.back().target.get(); + } + + // 3. 池已满,尝试复用最近最少使用(LRU)的可用目标 + auto lru_it = pool_.end(); + uint64_t min_frame = UINT64_MAX; + + for (auto it = pool_.begin(); it != pool_.end(); ++it) { + if (!it->in_use) { + if (it->last_used_frame < min_frame) { + min_frame = it->last_used_frame; + lru_it = it; + } + } + } + + if (lru_it != pool_.end()) { + // 复用找到的目标 + // 检查配置是否兼容(主要是深度缓冲配置) + // 如果深度缓冲配置不同,我们需要重建目标 + bool need_recreate = (lru_it->key.enable_depth_stencil != enable_depth) || + (lru_it->key.depth_stencil_format != key.depth_stencil_format); + + // 注意:目前 offscreen_target 不支持动态修改 format,如果 format 不同也可能需要重建 + // 但 offscreen_target 目前只支持默认 format,所以这里主要关注深度配置 + + if (need_recreate) { + // 重建目标 + auto target_config = offscreen_target::config{ + .enable_depth_stencil = enable_depth, + .depth_stencil_format = key.depth_stencil_format + }; + lru_it->target = std::make_unique(device_, res_mgr_, target_config); + lru_it->target->initialize(width, height); + } else { + // 配置兼容,只需调整大小 + if (lru_it->key.width != width || lru_it->key.height != height) { + lru_it->target->resize(width, height); + } + } + + lru_it->key = key; + lru_it->in_use = true; + lru_it->last_used_frame = current_frame_; + return lru_it->target.get(); + } + + // 4. 如果所有目标都在使用中(池已满且无可用),不得不创建新目标(超出池大小限制) + // 这种情况应该很少发生,除非 max_pool_size 设置得太小 + auto target_config = offscreen_target::config{ + .enable_depth_stencil = enable_depth, + .depth_stencil_format = key.depth_stencil_format + }; + + auto target = std::make_unique(device_, res_mgr_, target_config); + target->initialize(width, height); + + pool_.push_back(pooled_target{ + .target = std::move(target), + .key = key, + .last_used_frame = current_frame_, + .in_use = true + }); + + return pool_.back().target.get(); + } + + void render_target_pool::release(offscreen_target* target) { + for (auto& item : pool_) { + if (item.target.get() == target) { + item.in_use = false; + return; + } + } + // 如果没找到,可能是已经被销毁或者不属于这个池 + } + + void render_target_pool::reset_frame() { + current_frame_++; + + // 将所有目标标记为可用 + // 注意:这假设所有从池中获取的目标只在当前帧有效 + for (auto& item : pool_) { + item.in_use = false; + } + + // 可选:清理长时间未使用的目标 + // 例如:超过 60 帧未使用的目标 + constexpr uint64_t max_unused_frames = 60; + + // 使用 erase-remove idiom 清理 + // 注意:这里我们只清理未使用的目标 + auto it = std::remove_if(pool_.begin(), pool_.end(), + [this](const pooled_target& item) { + return !item.in_use && (current_frame_ - item.last_used_frame > max_unused_frames); + }); + + pool_.erase(it, pool_.end()); + } + + void render_target_pool::cleanup() { + // 清空池,unique_ptr 会自动销毁 offscreen_target + // offscreen_target 的析构函数会调用其 cleanup() + pool_.clear(); + current_frame_ = 0; + } + +} // namespace mirage::render \ No newline at end of file diff --git a/src/render/pipeline/render_target_pool.h b/src/render/pipeline/render_target_pool.h new file mode 100644 index 0000000..8b02d8e --- /dev/null +++ b/src/render/pipeline/render_target_pool.h @@ -0,0 +1,86 @@ +#pragma once + +#include "offscreen_target.h" +#include +#include +#include +#include + +namespace mirage::render { + + /// @brief 渲染目标池 - 管理临时渲染目标的分配和复用 + class render_target_pool { + public: + /// @brief 配置选项 + struct config { + uint32_t max_pool_size = 16; ///< 池的最大容量 + bool use_transient_memory = false; ///< 是否使用瞬态内存(移动端优化) + }; + + /// @brief 目标匹配键 + struct target_key { + uint32_t width; + uint32_t height; + vk::Format format = vk::Format::eR8G8B8A8Srgb; ///< 颜色格式 + bool enable_depth_stencil = false; ///< 是否启用深度/模板 + vk::Format depth_stencil_format = vk::Format::eD24UnormS8Uint; ///< 深度/模板格式 + + bool operator==(const target_key& other) const { + return width == other.width && + height == other.height && + format == other.format && + enable_depth_stencil == other.enable_depth_stencil && + depth_stencil_format == other.depth_stencil_format; + } + }; + + /// @brief 构造函数 + /// @param device 逻辑设备 + /// @param res_mgr 资源管理器 + /// @param cfg 配置参数 + render_target_pool(logical_device& device, resource_manager& res_mgr, const config& cfg = {}); + + ~render_target_pool(); + + // 禁止拷贝 + render_target_pool(const render_target_pool&) = delete; + render_target_pool& operator=(const render_target_pool&) = delete; + + /// @brief 获取指定大小的渲染目标 + /// @param width 宽度 + /// @param height 高度 + /// @param format 颜色格式(目前 offscreen_target 仅支持默认格式) + /// @param enable_depth 是否启用深度缓冲 + /// @return 渲染目标指针 + auto acquire(uint32_t width, uint32_t height, + vk::Format format = vk::Format::eR8G8B8A8Srgb, + bool enable_depth = false) -> offscreen_target*; + + /// @brief 释放渲染目标回池 + /// @param target 要释放的渲染目标 + void release(offscreen_target* target); + + /// @brief 帧结束时重置可用列表 + /// @details 将所有已使用的目标标记为可用,供下一帧复用 + void reset_frame(); + + /// @brief 清理所有资源 + void cleanup(); + + private: + struct pooled_target { + std::unique_ptr target; + target_key key; + uint64_t last_used_frame = 0; + bool in_use = false; + }; + + logical_device& device_; + resource_manager& res_mgr_; + config config_; + + std::vector pool_; + uint64_t current_frame_ = 0; + }; + +} // namespace mirage::render \ No newline at end of file diff --git a/src/render/pipeline/render_tree.h b/src/render/pipeline/render_tree.h new file mode 100644 index 0000000..d781e5a --- /dev/null +++ b/src/render/pipeline/render_tree.h @@ -0,0 +1,638 @@ +#pragma once + +#include "types.h" +#include "render_command.h" +#include "types.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mirage::render { + +// ============================================================================ +// 渲染节点类型枚举 +// ============================================================================ + +/// @brief 渲染节点类型枚举 +/// 定义渲染树中不同类型的节点 +enum class render_node_type { + geometry, ///< 几何渲染节点 - 包含实际的绘制批次 + mask, ///< 遮罩容器节点 - 管理遮罩效果及其子节点 + post_effect, ///< 后效容器节点 - 管理后效处理及其子节点 + group ///< 分组容器节点 - 用于组织多个子节点 +}; + +// ============================================================================ +// 渲染节点基类 +// ============================================================================ + +/// @brief 渲染节点基类 +/// 所有渲染节点的基类,包含通用属性 +class render_node { +public: + /// @brief 构造函数 + /// @param type 节点类型 + /// @param bounds 节点边界框 + /// @param z_order 渲染顺序 + render_node(render_node_type type, const aabb2d_t& bounds, int z_order = 0) + : type_(type), bounds_(bounds), z_order_(z_order) { + } + + /// @brief 虚析构函数 + virtual ~render_node() = default; + + /// @brief 获取节点类型 + /// @return 节点类型 + [[nodiscard]] auto get_type() const -> render_node_type { + return type_; + } + + /// @brief 获取边界框 + /// @return 节点边界框 + [[nodiscard]] auto get_bounds() const -> const aabb2d_t& { + return bounds_; + } + + /// @brief 设置边界框 + /// @param bounds 新的边界框 + void set_bounds(const aabb2d_t& bounds) { + bounds_ = bounds; + } + + /// @brief 获取渲染顺序 + /// @return 渲染顺序值 + [[nodiscard]] auto get_z_order() const -> int { + return z_order_; + } + + /// @brief 设置渲染顺序 + /// @param z_order 新的渲染顺序值 + void set_z_order(int z_order) { + z_order_ = z_order; + } + +private: + render_node_type type_; ///< 节点类型 + aabb2d_t bounds_; ///< 节点边界框 + int z_order_; ///< 渲染顺序 +}; + +// ============================================================================ +// 几何渲染节点 +// ============================================================================ + +/// @brief 几何渲染节点 +/// 包含实际的几何绘制批次数据 +class geometry_node : public render_node { +public: + /// @brief 构造函数 + /// @param bounds 节点边界框 + /// @param z_order 渲染顺序 + geometry_node(const aabb2d_t& bounds = aabb2d_t(), int z_order = 0) + : render_node(render_node_type::geometry, bounds, z_order) { + } + + /// @brief 获取绘制批次列表 + /// @return 绘制批次列表的引用 + [[nodiscard]] auto get_batches() -> std::vector& { + return batches_; + } + + /// @brief 获取绘制批次列表(只读) + /// @return 绘制批次列表的常量引用 + [[nodiscard]] auto get_batches() const -> const std::vector& { + return batches_; + } + + /// @brief 添加绘制批次 + /// @param batch 要添加的绘制批次 + void add_batch(const draw_batch& batch) { + batches_.push_back(batch); + } + + /// @brief 移动添加绘制批次 + /// @param batch 要移动的绘制批次 + void add_batch(draw_batch&& batch) { + batches_.push_back(std::move(batch)); + } + + /// @brief 清空所有批次 + void clear_batches() { + batches_.clear(); + } + + /// @brief 检查是否为空 + /// @return 如果没有批次数据则返回 true + [[nodiscard]] auto empty() const -> bool { + return batches_.empty(); + } + +private: + std::vector batches_; ///< 绘制批次列表 +}; + +// ============================================================================ +// 遮罩容器节点 +// ============================================================================ + +/// @brief 遮罩容器节点 +/// 管理遮罩效果及其子节点的渲染 +class mask_node : public render_node { +public: + /// @brief 构造函数 + /// @param params 遮罩参数 + /// @param content_bounds 内容边界框 + /// @param z_order 渲染顺序 + mask_node(const mask_params& params, const aabb2d_t& content_bounds, int z_order = 0) + : render_node(render_node_type::mask, content_bounds, z_order) + , mask_params_(params) + , content_bounds_(content_bounds) { + } + + /// @brief 获取遮罩参数 + /// @return 遮罩参数的引用 + [[nodiscard]] auto get_mask_params() -> mask_params& { + return mask_params_; + } + + /// @brief 获取遮罩参数(只读) + /// @return 遮罩参数的常量引用 + [[nodiscard]] auto get_mask_params() const -> const mask_params& { + return mask_params_; + } + + /// @brief 设置遮罩参数 + /// @param params 新的遮罩参数 + void set_mask_params(const mask_params& params) { + mask_params_ = params; + } + + /// @brief 获取内容边界框 + /// @return 内容边界框的引用 + [[nodiscard]] auto get_content_bounds() -> aabb2d_t& { + return content_bounds_; + } + + /// @brief 获取内容边界框(只读) + /// @return 内容边界框的常量引用 + [[nodiscard]] auto get_content_bounds() const -> const aabb2d_t& { + return content_bounds_; + } + + /// @brief 设置内容边界框 + /// @param bounds 新的内容边界框 + void set_content_bounds(const aabb2d_t& bounds) { + content_bounds_ = bounds; + } + + /// @brief 获取子节点列表 + /// @return 子节点列表的引用 + [[nodiscard]] auto get_children() -> std::vector>& { + return children_; + } + + /// @brief 获取子节点列表(只读) + /// @return 子节点列表的常量引用 + [[nodiscard]] auto get_children() const -> const std::vector>& { + return children_; + } + + /// @brief 添加子节点 + /// @param child 要添加的子节点(移动语义) + void add_child(std::unique_ptr child) { + children_.push_back(std::move(child)); + } + + /// @brief 移除指定索引的子节点 + /// @param index 要移除的子节点索引 + /// @return 被移除的子节点 + auto remove_child(size_t index) -> std::unique_ptr { + if (index >= children_.size()) { + return nullptr; + } + auto child = std::move(children_[index]); + children_.erase(children_.begin() + index); + return child; + } + + /// @brief 清空所有子节点 + void clear_children() { + children_.clear(); + } + + /// @brief 获取子节点数量 + /// @return 子节点数量 + [[nodiscard]] auto get_child_count() const -> size_t { + return children_.size(); + } + +private: + mask_params mask_params_; ///< 遮罩参数 + aabb2d_t content_bounds_; ///< 内容边界框 + std::vector> children_; ///< 子节点列表 +}; + +// ============================================================================ +// 后效容器节点 +// ============================================================================ + +/// @brief 后效容器节点 +/// 管理后效处理及其子节点的渲染 +class post_effect_node : public render_node { +public: + /// @brief 构造函数 + /// @param effect_command 后效命令 + /// @param bounds 节点边界框 + /// @param z_order 渲染顺序 + post_effect_node(const post_effect_command& effect_command, const aabb2d_t& bounds, int z_order = 0) + : render_node(render_node_type::post_effect, bounds, z_order) + , effect_command_(effect_command) { + } + + /// @brief 获取后效命令 + /// @return 后效命令的引用 + [[nodiscard]] auto get_effect_command() -> post_effect_command& { + return effect_command_; + } + + /// @brief 获取后效命令(只读) + /// @return 后效命令的常量引用 + [[nodiscard]] auto get_effect_command() const -> const post_effect_command& { + return effect_command_; + } + + /// @brief 设置后效命令 + /// @param command 新的后效命令 + void set_effect_command(const post_effect_command& command) { + effect_command_ = command; + } + + /// @brief 获取子节点列表 + /// @return 子节点列表的引用 + [[nodiscard]] auto get_children() -> std::vector>& { + return children_; + } + + /// @brief 获取子节点列表(只读) + /// @return 子节点列表的常量引用 + [[nodiscard]] auto get_children() const -> const std::vector>& { + return children_; + } + + /// @brief 添加子节点 + /// @param child 要添加的子节点(移动语义) + void add_child(std::unique_ptr child) { + children_.push_back(std::move(child)); + } + + /// @brief 移除指定索引的子节点 + /// @param index 要移除的子节点索引 + /// @return 被移除的子节点 + auto remove_child(size_t index) -> std::unique_ptr { + if (index >= children_.size()) { + return nullptr; + } + auto child = std::move(children_[index]); + children_.erase(children_.begin() + index); + return child; + } + + /// @brief 清空所有子节点 + void clear_children() { + children_.clear(); + } + + /// @brief 获取子节点数量 + /// @return 子节点数量 + [[nodiscard]] auto get_child_count() const -> size_t { + return children_.size(); + } + +private: + post_effect_command effect_command_; ///< 后效命令 + std::vector> children_; ///< 子节点列表 +}; + +// ============================================================================ +// 分组容器节点 +// ============================================================================ + +/// @brief 分组容器节点 +/// 用于组织多个子节点,不提供特殊的渲染效果 +class group_node : public render_node { +public: + /// @brief 构造函数 + /// @param bounds 节点边界框 + /// @param z_order 渲染顺序 + group_node(const aabb2d_t& bounds = aabb2d_t(), int z_order = 0) + : render_node(render_node_type::group, bounds, z_order) { + } + + /// @brief 获取子节点列表 + /// @return 子节点列表的引用 + [[nodiscard]] auto get_children() -> std::vector>& { + return children_; + } + + /// @brief 获取子节点列表(只读) + /// @return 子节点列表的常量引用 + [[nodiscard]] auto get_children() const -> const std::vector>& { + return children_; + } + + /// @brief 添加子节点 + /// @param child 要添加的子节点(移动语义) + void add_child(std::unique_ptr child) { + children_.push_back(std::move(child)); + } + + /// @brief 移除指定索引的子节点 + /// @param index 要移除的子节点索引 + /// @return 被移除的子节点 + auto remove_child(size_t index) -> std::unique_ptr { + if (index >= children_.size()) { + return nullptr; + } + auto child = std::move(children_[index]); + children_.erase(children_.begin() + index); + return child; + } + + /// @brief 清空所有子节点 + void clear_children() { + children_.clear(); + } + + /// @brief 获取子节点数量 + /// @return 子节点数量 + [[nodiscard]] auto get_child_count() const -> size_t { + return children_.size(); + } + +private: + std::vector> children_; ///< 子节点列表 +}; + +// ============================================================================ +// 渲染树结构 +// ============================================================================ + +/// @brief 渲染树结构 +/// 表示整个渲染场景的树形结构 +struct render_tree { + /// @brief 构造函数 + /// @param root 根节点 + explicit render_tree(std::unique_ptr root = nullptr) + : root_(std::move(root)) { + } + + /// @brief 获取根节点 + /// @return 根节点的指针 + [[nodiscard]] auto get_root() -> render_node* { + return root_.get(); + } + + /// @brief 获取根节点(只读) + /// @return 根节点的常量指针 + [[nodiscard]] auto get_root() const -> const render_node* { + return root_.get(); + } + + /// @brief 设置根节点 + /// @param root 新的根节点(移动语义) + void set_root(std::unique_ptr root) { + root_ = std::move(root); + } + + /// @brief 检查树是否为空 + /// @return 如果没有根节点则返回 true + [[nodiscard]] auto empty() const -> bool { + return root_ == nullptr; + } + + /// @brief 清空整个树 + void clear() { + root_.reset(); + } + +private: + std::unique_ptr root_; ///< 根节点 +}; + +// ============================================================================ +// 渲染树打印功能 +// ============================================================================ + +/// @brief 获取渲染节点类型名称 +/// @param type 节点类型 +/// @return 类型名称字符串 +[[nodiscard]] inline auto get_node_type_name(render_node_type type) -> const char* { + switch (type) { + case render_node_type::geometry: return "Geometry"; + case render_node_type::mask: return "Mask"; + case render_node_type::post_effect: return "PostEffect"; + case render_node_type::group: return "Group"; + default: return "Unknown"; + } +} + +/// @brief 获取遮罩类型名称 +/// @param type 遮罩类型 +/// @return 类型名称字符串 +[[nodiscard]] inline auto get_mask_type_name(mask_type type) -> const char* { + switch (type) { + case mask_type::circle: return "Circle"; + case mask_type::ellipse: return "Ellipse"; + case mask_type::rect: return "Rect"; + case mask_type::rounded_rect: return "RoundedRect"; + default: return "Unknown"; + } +} + +/// @brief 获取后效类型名称 +/// @param cmd 后效命令 +/// @return 类型名称字符串 +[[nodiscard]] inline auto get_post_effect_type_name(const post_effect_command& cmd) -> const char* { + return std::visit([](const auto& effect) -> const char* { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return "Blur"; + } else if constexpr (std::is_same_v) { + return "Vignette"; + } else if constexpr (std::is_same_v) { + return "ChromaticAberration"; + } else if constexpr (std::is_same_v) { + return "ColorAdjust"; + } else if constexpr (std::is_same_v) { + return "ColorTint"; + } else if constexpr (std::is_same_v) { + return "Noise"; + } else if constexpr (std::is_same_v) { + return "CustomShader"; + } else { + return "Unknown"; + } + }, cmd); +} + +namespace detail { + /// @brief 打印缩进 + /// @param os 输出流 + /// @param depth 缩进深度 + /// @param prefix 前缀字符串(用于树形结构) + inline void print_indent(std::ostream& os, int depth, const std::string& prefix = "") { + os << prefix; + for (int i = 0; i < depth; ++i) { + os << " "; + } + } + + /// @brief 打印 AABB 信息 + /// @param os 输出流 + /// @param bounds AABB 边界框 + inline void print_aabb(std::ostream& os, const aabb2d_t& bounds) { + os << "[(" << bounds.min().x() << ", " << bounds.min().y() << ") - (" + << bounds.max().x() << ", " << bounds.max().y() << ")]"; + } + + /// @brief 递归打印渲染节点 + /// @param os 输出流 + /// @param node 要打印的节点 + /// @param depth 当前深度 + /// @param is_last 是否是父节点的最后一个子节点 + /// @param prefix 前缀字符串 + inline void print_node(std::ostream& os, const render_node* node, int depth = 0, + bool is_last = true, const std::string& prefix = "") { + if (!node) { + print_indent(os, depth, prefix); + os << "(null)\n"; + return; + } + + // 打印树形连接符 + std::string connector = is_last ? "└── " : "├── "; + std::string child_prefix = prefix + (is_last ? " " : "│ "); + + if (depth > 0) { + os << prefix << connector; + } + + // 打印节点类型 + os << get_node_type_name(node->get_type()); + + // 打印通用信息 + os << " z=" << node->get_z_order() << " bounds="; + print_aabb(os, node->get_bounds()); + + // 根据节点类型打印特定信息 + switch (node->get_type()) { + case render_node_type::geometry: { + const auto* geo = static_cast(node); + os << " batches=" << geo->get_batches().size(); + size_t total_vertices = 0; + size_t total_indices = 0; + for (const auto& batch : geo->get_batches()) { + total_vertices += batch.vertices.size(); + total_indices += batch.indices.size(); + } + os << " (verts=" << total_vertices << ", indices=" << total_indices << ")"; + os << "\n"; + break; + } + case render_node_type::mask: { + const auto* mask = static_cast(node); + const auto& params = mask->get_mask_params(); + os << " type=" << get_mask_type_name(params.type); + os << " center=(" << params.center.x() << ", " << params.center.y() << ")"; + os << " size=(" << params.size.x() << ", " << params.size.y() << ")"; + if (params.softness > 0.0f) { + os << " softness=" << params.softness; + } + os << " children=" << mask->get_child_count(); + os << "\n"; + // 递归打印子节点 + const auto& children = mask->get_children(); + for (size_t i = 0; i < children.size(); ++i) { + print_node(os, children[i].get(), depth + 1, + i == children.size() - 1, child_prefix); + } + break; + } + case render_node_type::post_effect: { + const auto* pe = static_cast(node); + os << " effect=" << get_post_effect_type_name(pe->get_effect_command()); + os << " children=" << pe->get_child_count(); + os << "\n"; + // 递归打印子节点 + const auto& children = pe->get_children(); + for (size_t i = 0; i < children.size(); ++i) { + print_node(os, children[i].get(), depth + 1, + i == children.size() - 1, child_prefix); + } + break; + } + case render_node_type::group: { + const auto* grp = static_cast(node); + os << " children=" << grp->get_child_count(); + os << "\n"; + // 递归打印子节点 + const auto& children = grp->get_children(); + for (size_t i = 0; i < children.size(); ++i) { + print_node(os, children[i].get(), depth + 1, + i == children.size() - 1, child_prefix); + } + break; + } + default: + os << "\n"; + break; + } + } +} // namespace detail + +/// @brief 打印渲染树到输出流 +/// @param os 输出流 +/// @param tree 要打印的渲染树 +/// @param title 可选的标题 +inline void print_render_tree(std::ostream& os, const render_tree& tree, + const char* title = nullptr) { + if (title) { + os << "=== " << title << " ===\n"; + } else { + os << "=== Render Tree ===\n"; + } + + if (tree.empty()) { + os << "(empty tree)\n"; + } else { + detail::print_node(os, tree.get_root()); + } + + os << "==================\n"; +} + +/// @brief 打印渲染树到标准输出 +/// @param tree 要打印的渲染树 +/// @param title 可选的标题 +inline void print_render_tree(const render_tree& tree, const char* title = nullptr) { + print_render_tree(std::cout, tree, title); + std::cout << std::flush; +} + +/// @brief 将渲染树转换为字符串 +/// @param tree 要转换的渲染树 +/// @param title 可选的标题 +/// @return 渲染树的字符串表示 +[[nodiscard]] inline auto render_tree_to_string(const render_tree& tree, + const char* title = nullptr) -> std::string { + std::ostringstream oss; + print_render_tree(oss, tree, title); + return oss.str(); +} + +} // namespace mirage::render \ No newline at end of file diff --git a/src/render/pipeline/render_tree_builder.cpp b/src/render/pipeline/render_tree_builder.cpp new file mode 100644 index 0000000..f5157cc --- /dev/null +++ b/src/render/pipeline/render_tree_builder.cpp @@ -0,0 +1,400 @@ +#include "render_tree_builder.h" +#include "renderer/vertex_types.h" +#include +#include +#include +#include + +namespace mirage::render { + +namespace { + + // ============================================================================ + // Helper Functions (Adapted from command_processor.cpp) + // ============================================================================ + + auto get_command_z_order(const render_command& cmd) -> int32_t { + return std::visit([](const CMD& c) -> int32_t { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::visit([](const auto& effect) { + return effect.order.z_order; + }, c); + } + else { + return c.order.z_order; + } + }, cmd); + } + + auto get_command_texture_id(const render_command& cmd) -> std::optional { + return std::visit([](const T0& c) -> std::optional { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return c.texture_id; + } + return std::nullopt; + }, cmd); + } + + struct quad_params { + vec2f_t position; + vec2f_t size; + std::array color; + std::array data_a; + std::array data_b; + std::array uv_rect = {0.0f, 0.0f, 1.0f, 1.0f}; + }; + + void append_quad(draw_batch& batch, const quad_params& params) { + const auto base_index = static_cast(batch.vertices.size()); + + ui_vertex vertices[4]; + const float u_min = params.uv_rect[0]; + const float v_min = params.uv_rect[1]; + const float u_max = params.uv_rect[2]; + const float v_max = params.uv_rect[3]; + + // Vertex 0: Top-Left + vertices[0].position[0] = params.position.x(); + vertices[0].position[1] = params.position.y(); + std::copy_n(params.color.data(), 4, vertices[0].color); + vertices[0].uv[0] = u_min; + vertices[0].uv[1] = v_min; + std::copy_n(params.data_a.data(), 4, vertices[0].data_a); + std::copy_n(params.data_b.data(), 4, vertices[0].data_b); + + // Vertex 1: Top-Right + vertices[1].position[0] = params.position.x() + params.size.x(); + vertices[1].position[1] = params.position.y(); + std::copy_n(params.color.data(), 4, vertices[1].color); + vertices[1].uv[0] = u_max; + vertices[1].uv[1] = v_min; + std::copy_n(params.data_a.data(), 4, vertices[1].data_a); + std::copy_n(params.data_b.data(), 4, vertices[1].data_b); + + // Vertex 2: Bottom-Right + vertices[2].position[0] = params.position.x() + params.size.x(); + vertices[2].position[1] = params.position.y() + params.size.y(); + std::copy_n(params.color.data(), 4, vertices[2].color); + vertices[2].uv[0] = u_max; + vertices[2].uv[1] = v_max; + std::copy_n(params.data_a.data(), 4, vertices[2].data_a); + std::copy_n(params.data_b.data(), 4, vertices[2].data_b); + + // Vertex 3: Bottom-Left + vertices[3].position[0] = params.position.x(); + vertices[3].position[1] = params.position.y() + params.size.y(); + std::copy_n(params.color.data(), 4, vertices[3].color); + vertices[3].uv[0] = u_min; + vertices[3].uv[1] = v_max; + std::copy_n(params.data_a.data(), 4, vertices[3].data_a); + std::copy_n(params.data_b.data(), 4, vertices[3].data_b); + + batch.vertices.insert(batch.vertices.end(), vertices, vertices + 4); + + batch.indices.push_back(base_index + 0); + batch.indices.push_back(base_index + 1); + batch.indices.push_back(base_index + 2); + batch.indices.push_back(base_index + 0); + batch.indices.push_back(base_index + 2); + batch.indices.push_back(base_index + 3); + } + + void append_command_to_batch(draw_batch& batch, const render_command& cmd) { + std::visit([&batch](const T& concrete_cmd) { + using CmdType = std::decay_t; + + if constexpr (std::is_same_v) { + quad_params params; + params.position = concrete_cmd.position; + params.size = concrete_cmd.size; + params.color = { + concrete_cmd.fill_color.r, + concrete_cmd.fill_color.g, + concrete_cmd.fill_color.b, + concrete_cmd.fill_color.a + }; + const auto border_width = concrete_cmd.border_width; + const auto border_only = concrete_cmd.border_only ? 1.0f : 0.0f; + params.data_a = { + concrete_cmd.size.x(), + concrete_cmd.size.y(), + border_width, + border_only + }; + const auto tl = concrete_cmd.corner_radius.top_left; + const auto tr = concrete_cmd.corner_radius.top_right; + const auto rb = concrete_cmd.corner_radius.bottom_right; + const auto lb = concrete_cmd.corner_radius.bottom_left; + params.data_b = {tl, tr, rb, lb}; + + append_quad(batch, params); + } + else if constexpr (std::is_same_v) { + quad_params params; + params.position = concrete_cmd.position; + params.size = concrete_cmd.size; + params.color = {1.0f, 1.0f, 1.0f, 1.0f}; + params.data_a = {}; + params.data_b = {}; + + append_quad(batch, params); + } + else if constexpr (std::is_same_v) { + quad_params params; + params.position = concrete_cmd.position; + params.size = concrete_cmd.size; + params.color = { + concrete_cmd.border_color.r, + concrete_cmd.border_color.g, + concrete_cmd.border_color.b, + concrete_cmd.border_color.a + }; + params.data_a = { + concrete_cmd.size.x(), + concrete_cmd.size.y(), + concrete_cmd.border_width, + 1.0f // border_only = true + }; + const auto tl = concrete_cmd.corner_radius.top_left; + const auto tr = concrete_cmd.corner_radius.top_right; + const auto rb = concrete_cmd.corner_radius.bottom_right; + const auto lb = concrete_cmd.corner_radius.bottom_left; + params.data_b = {tl, tr, rb, lb}; + + append_quad(batch, params); + } + else if constexpr (std::is_same_v) { + // TODO: Implement text rendering + } + }, cmd); + } + +} // namespace + +auto render_tree_builder::build(const std::vector& commands) -> render_tree { + if (commands.empty()) { + return render_tree(std::make_unique()); + } + + // 1. Sort commands by z_order + std::vector sorted_commands = commands; + std::ranges::stable_sort(sorted_commands, [](const render_command& a, const render_command& b) { + return get_command_z_order(a) < get_command_z_order(b); + }); + + // 2. Initialize context + build_context context; + auto root = std::make_unique(); + context.current = root.get(); + context.pending_nodes.push(std::move(root)); + + // 3. Process commands + for (const auto& cmd : sorted_commands) { + process_command(cmd, context); + } + + // 4. Finalize + // Ensure we are not in a geometry node + ensure_not_geometry(context); + + // If there are still nodes in the stack (e.g. unclosed masks), pop them + // This handles malformed command lists gracefully + while (context.node_stack.size() > 0) { + auto* child = context.current; + auto* parent = context.node_stack.top(); + + // Move ownership from pending_nodes to parent + if (!context.pending_nodes.empty()) { + auto child_ptr = std::move(context.pending_nodes.top()); + context.pending_nodes.pop(); + + // Verify pointer match (sanity check) + if (child_ptr.get() == child) { + // Add to parent + if (parent->get_type() == render_node_type::mask) { + static_cast(parent)->add_child(std::move(child_ptr)); + } else if (parent->get_type() == render_node_type::group) { + static_cast(parent)->add_child(std::move(child_ptr)); + } else if (parent->get_type() == render_node_type::post_effect) { + static_cast(parent)->add_child(std::move(child_ptr)); + } + } + } + + context.node_stack.pop(); + context.current = parent; + } + + // The last remaining node in pending_nodes is the root + if (context.pending_nodes.empty()) { + return render_tree(nullptr); // Should not happen if initialized correctly + } + + return render_tree(std::move(context.pending_nodes.top())); +} + +void render_tree_builder::process_command(const render_command& cmd, build_context& context) { + std::visit([this, &context](const auto& c) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + process_mask_begin(c, context); + } + else if constexpr (std::is_same_v) { + process_mask_end(c, context); + } + else if constexpr (std::is_same_v) { + process_post_effect(c, context); + } + else { + // Assume geometry command + render_command cmd_variant = c; + process_geometry(cmd_variant, context); + } + }, cmd); +} + +void render_tree_builder::process_geometry(const render_command& cmd, build_context& context) { + // Check if we need to switch to a geometry node + if (context.current->get_type() != render_node_type::geometry) { + // Create new geometry node + auto geo_node = std::make_unique(); + auto* geo_ptr = geo_node.get(); + + // Add to current container + if (context.current->get_type() == render_node_type::group) { + static_cast(context.current)->add_child(std::move(geo_node)); + } + else if (context.current->get_type() == render_node_type::mask) { + static_cast(context.current)->add_child(std::move(geo_node)); + } + else if (context.current->get_type() == render_node_type::post_effect) { + static_cast(context.current)->add_child(std::move(geo_node)); + } + + // Push current container to stack so we can return to it + context.node_stack.push(context.current); + context.current = geo_ptr; + } + + // Now context.current is a geometry_node + auto* geo_node = static_cast(context.current); + auto texture_id = get_command_texture_id(cmd); + + // Check if we can merge with the last batch + bool merged = false; + if (!geo_node->get_batches().empty()) { + auto& last_batch = geo_node->get_batches().back(); + if (last_batch.texture_id == texture_id) { + append_command_to_batch(last_batch, cmd); + merged = true; + } + } + + if (!merged) { + draw_batch new_batch; + new_batch.texture_id = texture_id; + append_command_to_batch(new_batch, cmd); + geo_node->add_batch(std::move(new_batch)); + } +} + +void render_tree_builder::process_mask_begin(const mask_begin_command& cmd, build_context& context) { + ensure_not_geometry(context); + + // Create new mask node + auto mask_node_ptr = std::make_unique(cmd.params, cmd.content_bounds, cmd.order.z_order); + auto* raw_ptr = mask_node_ptr.get(); + + // Push to pending nodes (ownership) + context.pending_nodes.push(std::move(mask_node_ptr)); + + // Push current parent to stack + context.node_stack.push(context.current); + + // Set as current + context.current = raw_ptr; +} + +void render_tree_builder::process_mask_end(const mask_end_command& cmd, build_context& context) { + ensure_not_geometry(context); + + if (context.node_stack.empty()) { + // Error: Unmatched mask_end + return; + } + + // Current node is the mask node we just finished + auto* mask_ptr = context.current; + + // Parent is on top of stack + auto* parent = context.node_stack.top(); + context.node_stack.pop(); + + // Retrieve ownership from pending_nodes + if (context.pending_nodes.empty()) { + // Error: Logic error + return; + } + auto mask_unique_ptr = std::move(context.pending_nodes.top()); + context.pending_nodes.pop(); + + // Verify we got the right node + if (mask_unique_ptr.get() != mask_ptr) { + // Error: Stack mismatch + // Put it back to try to recover? + return; + } + + // Add to parent + if (parent->get_type() == render_node_type::group) { + static_cast(parent)->add_child(std::move(mask_unique_ptr)); + } + else if (parent->get_type() == render_node_type::mask) { + static_cast(parent)->add_child(std::move(mask_unique_ptr)); + } + else if (parent->get_type() == render_node_type::post_effect) { + static_cast(parent)->add_child(std::move(mask_unique_ptr)); + } + + // Restore parent as current + context.current = parent; +} + +void render_tree_builder::process_post_effect(const post_effect_command& cmd, build_context& context) { + ensure_not_geometry(context); + + // Create post effect node + // Note: Post effects in the command list are treated as leaf nodes here + // because the command list structure doesn't support explicit scope for them. + // They are added to the current container. + + auto aabb = get_effect_aabb(cmd); + int z_order = 0; + std::visit([&z_order](const auto& e) { z_order = e.order.z_order; }, cmd); + + auto effect_node = std::make_unique(cmd, aabb, z_order); + + // Add to current container + if (context.current->get_type() == render_node_type::group) { + static_cast(context.current)->add_child(std::move(effect_node)); + } + else if (context.current->get_type() == render_node_type::mask) { + static_cast(context.current)->add_child(std::move(effect_node)); + } + else if (context.current->get_type() == render_node_type::post_effect) { + static_cast(context.current)->add_child(std::move(effect_node)); + } +} + +void render_tree_builder::ensure_not_geometry(build_context& context) { + if (context.current->get_type() == render_node_type::geometry) { + if (!context.node_stack.empty()) { + context.current = context.node_stack.top(); + context.node_stack.pop(); + } + } +} + +} // namespace mirage::render \ No newline at end of file diff --git a/src/render/pipeline/render_tree_builder.h b/src/render/pipeline/render_tree_builder.h new file mode 100644 index 0000000..6e0f6f0 --- /dev/null +++ b/src/render/pipeline/render_tree_builder.h @@ -0,0 +1,68 @@ +#pragma once + +#include "render_tree.h" +#include "render_command.h" +#include +#include +#include + +namespace mirage::render { + +/// @brief 渲染树构建器 +/// 将线性的渲染命令列表转换为树形结构 +class render_tree_builder { +public: + /// @brief 构建渲染树 + /// @param commands 渲染命令列表 + /// @return 构建好的渲染树 + auto build(const std::vector& commands) -> render_tree; + +private: + /// @brief 构建上下文 + /// 用于跟踪构建过程中的状态 + struct build_context { + /// 节点栈,用于跟踪当前嵌套层级 + /// 存储的是父节点指针 + std::stack node_stack; + + /// 当前正在构建的节点指针 + /// 新的子节点将添加到此节点(如果是容器)或此节点的父节点(如果是叶子) + render_node* current = nullptr; + + /// 待处理的节点栈 + /// 用于存储尚未添加到父节点的节点(如正在构建的 mask_node) + std::stack> pending_nodes; + }; + + /// @brief 处理单个命令 + /// @param cmd 渲染命令 + /// @param context 构建上下文 + void process_command(const render_command& cmd, build_context& context); + + /// @brief 处理几何命令 + /// @param cmd 渲染命令 + /// @param context 构建上下文 + void process_geometry(const render_command& cmd, build_context& context); + + /// @brief 处理遮罩开始命令 + /// @param cmd 遮罩开始命令 + /// @param context 构建上下文 + void process_mask_begin(const mask_begin_command& cmd, build_context& context); + + /// @brief 处理遮罩结束命令 + /// @param cmd 遮罩结束命令 + /// @param context 构建上下文 + void process_mask_end(const mask_end_command& cmd, build_context& context); + + /// @brief 处理后效命令 + /// @param cmd 后效命令 + /// @param context 构建上下文 + void process_post_effect(const post_effect_command& cmd, build_context& context); + + /// @brief 确保当前节点不是几何节点 + /// 如果当前是几何节点,则将其弹出栈,恢复到父容器 + /// @param context 构建上下文 + void ensure_not_geometry(build_context& context); +}; + +} // namespace mirage::render \ No newline at end of file diff --git a/src/render/pipeline/render_tree_executor.cpp b/src/render/pipeline/render_tree_executor.cpp new file mode 100644 index 0000000..0e056c7 --- /dev/null +++ b/src/render/pipeline/render_tree_executor.cpp @@ -0,0 +1,229 @@ +#include "render_tree_executor.h" + +namespace mirage::render { + + // ============================================================================ + // 内部辅助函数前向声明 + // ============================================================================ + + template + void execute_node(const render_node& node, executor_context& ctx); + + template + void execute_geometry(const geometry_node& node, executor_context& ctx); + + template + void execute_mask(const mask_node& node, executor_context& ctx); + + template + void execute_post_effect(const post_effect_node& node, executor_context& ctx); + + template + void execute_group(const group_node& node, executor_context& ctx); + + // ============================================================================ + // render_tree_executor 实现 + // ============================================================================ + + void render_tree_executor::execute(const render_tree& tree, executor_context& ctx) { + if (tree.get_root()) { + execute_node(*tree.get_root(), ctx); + } + } + + // ============================================================================ + // 节点执行实现 + // ============================================================================ + + template + void execute_node(const render_node& node, executor_context& ctx) { + switch (node.get_type()) { + case render_node_type::geometry: + execute_geometry(static_cast(node), ctx); + break; + case render_node_type::mask: + execute_mask(static_cast(node), ctx); + break; + case render_node_type::post_effect: + execute_post_effect(static_cast(node), ctx); + break; + case render_node_type::group: + execute_group(static_cast(node), ctx); + break; + } + } + + template + void execute_geometry(const geometry_node& node, executor_context& ctx) { + if (node.get_batches().empty()) { + return; + } + // 根据批次是否有纹理ID选择渲染器 + for (const auto& batch : node.get_batches()) { + if (batch.texture_id.has_value()) { + // 有纹理ID,使用图片渲染器 + ctx.image->render(*ctx.frame_ctx, batch); + } else { + // 无纹理ID,使用几何渲染器 + ctx.geometry->render(*ctx.frame_ctx, batch); + } + } + } + + template + void execute_mask(const mask_node& node, executor_context& ctx) { + // 准备遮罩开始命令 + mask_begin_command cmd(node.get_mask_params(), node.get_content_bounds()); + + if constexpr (std::is_same_v) { + // 1. 开始遮罩渲染 (Offscreen -> Mask) + auto [nested_ctx, mask_ctx] = ctx.mask->begin_mask(std::move(*ctx.frame_ctx), cmd); + + // 2. 创建新的执行上下文 + executor_context sub_ctx{ + ctx.geometry, + ctx.image, + ctx.mask, + ctx.post_effect, + ctx.pool, + &mask_ctx, + ctx.time + }; + + // 3. 递归渲染子节点 + for (const auto& child : node.get_children()) { + execute_node(*child, sub_ctx); + } + + // 4. 结束遮罩渲染 (Mask -> Offscreen) + auto [restored_ctx, result] = ctx.mask->end_mask(std::move(nested_ctx), std::move(mask_ctx)); + + // 5. 恢复原上下文 + *ctx.frame_ctx = std::move(restored_ctx); + + // 6. 合成遮罩 + ctx.mask->composite_mask(*ctx.frame_ctx, std::move(result), node.get_mask_params(), node.get_content_bounds()); + + } else if constexpr (std::is_same_v) { + // 1. 开始嵌套遮罩渲染 (Mask -> Mask) + auto [nested_ctx, inner_mask_ctx] = ctx.mask->begin_nested_mask(std::move(*ctx.frame_ctx), cmd); + + // 2. 创建新的执行上下文 + executor_context sub_ctx{ + ctx.geometry, + ctx.image, + ctx.mask, + ctx.post_effect, + ctx.pool, + &inner_mask_ctx, + ctx.time + }; + + // 3. 递归渲染子节点 + for (const auto& child : node.get_children()) { + execute_node(*child, sub_ctx); + } + + // 4. 结束嵌套遮罩渲染 (Mask -> Mask) + auto [restored_ctx, result] = ctx.mask->end_nested_mask(std::move(nested_ctx), std::move(inner_mask_ctx)); + + // 5. 恢复原上下文 + *ctx.frame_ctx = std::move(restored_ctx); + + // 6. 合成遮罩 + ctx.mask->composite_mask(*ctx.frame_ctx, std::move(result), node.get_mask_params(), node.get_content_bounds()); + } + } + + template + void execute_post_effect(const post_effect_node& node, executor_context& ctx) { + auto* current_target = ctx.frame_ctx->current_target(); + if (!current_target) return; + + // Backdrop Mode: 如果没有子节点,直接对当前目标应用后效 + if (node.get_children().empty()) { + // 必须中断当前 Pass 才能应用后效 + if constexpr (std::is_same_v) { + auto idle_ctx = std::move(*ctx.frame_ctx).end_offscreen_pass(); + ctx.post_effect->apply_impl(idle_ctx.cmd(), *current_target, node.get_effect_command(), ctx.time); + *ctx.frame_ctx = std::move(idle_ctx).begin_offscreen_pass(*current_target, false); + } else { + // 对于 Mask Pass 或其他,手动中断 RenderPass + auto cmd = ctx.frame_ctx->cmd(); + + // 结束当前 RenderPass 并准备读取 (转换到 ShaderRead) + // 这会更新 offscreen_target 的内部状态,确保后续 apply_impl 能正确处理布局 + current_target->end_render(cmd, true); + + // 应用后效 + // apply_impl 会处理必要的布局转换 (ShaderRead -> TransferSrc/Dst -> ShaderRead) + ctx.post_effect->apply_impl(cmd, *current_target, node.get_effect_command(), ctx.time); + + // 重启 RenderPass (Load) + // begin_render 会自动将布局从 ShaderRead 转换回 ColorAttachmentOptimal + current_target->begin_render(cmd, false); // clear = false + } + return; + } + + if constexpr (std::is_same_v) { + auto extent = current_target->extent(); + + // 从池中获取临时渲染目标 + auto* temp_target = ctx.pool->acquire(extent.width, extent.height); + if (!temp_target) return; + + // 1. 结束当前 Pass (Offscreen -> Idle) + auto idle_ctx = std::move(*ctx.frame_ctx).end_offscreen_pass(); + + // 2. 开始临时目标的 Pass (Idle -> Offscreen) + auto temp_pass_ctx = std::move(idle_ctx).begin_offscreen_pass(*temp_target, true); + + // 3. 在临时目标上递归渲染子节点 + { + executor_context sub_ctx{ + ctx.geometry, + ctx.image, + ctx.mask, + ctx.post_effect, + ctx.pool, + &temp_pass_ctx, + ctx.time + }; + + for (const auto& child : node.get_children()) { + execute_node(*child, sub_ctx); + } + } + + // 4. 结束临时目标的 Pass (Offscreen -> Idle) + idle_ctx = std::move(temp_pass_ctx).end_offscreen_pass(); + + // 5. 应用后效 (Idle 状态下调用) + // 源:temp_target (刚才渲染的子节点内容) + // 目标:current_target (当前上下文绑定的目标) + ctx.post_effect->apply_impl(idle_ctx.cmd(), *current_target, node.get_effect_command(), ctx.time, temp_target); + + // 6. 恢复原目标的 Pass (Idle -> Offscreen) + *ctx.frame_ctx = std::move(idle_ctx).begin_offscreen_pass(*current_target, false); + + // 7. 释放临时目标 + ctx.pool->release(temp_target); + + } else { + // 在非 Offscreen Pass (如 Mask Pass) 中,暂时不支持 Content Mode 后效处理 + // 直接渲染子节点,忽略后效 + for (const auto& child : node.get_children()) { + execute_node(*child, ctx); + } + } + } + + template + void execute_group(const group_node& node, executor_context& ctx) { + for (const auto& child : node.get_children()) { + execute_node(*child, ctx); + } + } + +} // namespace mirage::render \ No newline at end of file diff --git a/src/render/pipeline/render_tree_executor.h b/src/render/pipeline/render_tree_executor.h new file mode 100644 index 0000000..d34918a --- /dev/null +++ b/src/render/pipeline/render_tree_executor.h @@ -0,0 +1,36 @@ +#pragma once + +#include "render_tree.h" +#include "render_target_pool.h" +#include "geometry_renderer.h" +#include "image_renderer.h" +#include "mask_renderer.h" +#include "post_effect_applicator.h" +#include "frame_context_v2.h" + +namespace mirage::render { + + /// @brief 执行上下文 + /// 包含执行渲染树所需的所有渲染器和当前帧上下文 + template + struct executor_context { + geometry_renderer* geometry; ///< 几何渲染器 + image_renderer* image; ///< 图片渲染器 + mask_renderer* mask; ///< 遮罩渲染器 + post_effect_applicator* post_effect; ///< 后效应用器 + render_target_pool* pool; ///< 渲染目标池 + pass_state::frame_context* frame_ctx; ///< 当前帧上下文 + float time = 0.0f; ///< 当前时间(用于后效) + }; + + /// @brief 渲染树执行器 + /// 负责递归遍历渲染树并执行渲染命令 + class render_tree_executor { + public: + /// @brief 执行渲染树 + /// @param tree 渲染树 + /// @param ctx 执行上下文(必须处于 offscreen_pass 状态) + void execute(const render_tree& tree, executor_context& ctx); + }; + +} // namespace mirage::render \ No newline at end of file diff --git a/src/render/pipeline/stencil_mask_renderer.cpp b/src/render/pipeline/stencil_mask_renderer.cpp deleted file mode 100644 index ebd4fee..0000000 --- a/src/render/pipeline/stencil_mask_renderer.cpp +++ /dev/null @@ -1,656 +0,0 @@ -#include "stencil_mask_renderer.h" -#include "vulkan/pipeline/graphics_pipeline.h" -#include "renderer/uniform_types.h" -#include -#include -#include - -// 引入着色器 SPIR-V 数据 -#include "stencil_mask_vert_frag.h" - -namespace mirage { - -// ============================================================================ -// 构造函数和析构函数 -// ============================================================================ - -stencil_mask_renderer::stencil_mask_renderer( - logical_device& device, - resource_manager& res_mgr, - const config& cfg -) : device_(device) - , res_mgr_(res_mgr) - , config_(cfg) { -} - -stencil_mask_renderer::~stencil_mask_renderer() { - cleanup(); -} - -// ============================================================================ -// 初始化和清理 -// ============================================================================ - -void stencil_mask_renderer::initialize(vk::RenderPass render_pass, vk::Extent2D viewport_extent) { - if (initialized_) return; - - viewport_extent_ = viewport_extent; - viewport_size_ = vec2f_t{ - static_cast(viewport_extent.width), - static_cast(viewport_extent.height) - }; - - // 创建帧资源 - frame_data_.resize(config_.frames_in_flight); - for (auto& fd : frame_data_) { - // 每帧最多支持 max_nested_depth 个遮罩,每个遮罩 6 个顶点 - const uint32_t max_vertices = config_.max_nested_depth * 6; - - // 创建顶点缓冲 - auto vb_result = res_mgr_.create_buffer( - max_vertices * sizeof(ui_vertex), - vk::BufferUsageFlagBits::eVertexBuffer, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - if (!vb_result) { - throw std::runtime_error("Failed to create vertex buffer for stencil mask renderer"); - } - - // 创建 typed_buffer 包装器 - fd.vertices = typed_buffer( - device_.get_handle(), - std::move(vb_result.value()), - max_vertices - ); - } - - // 创建着色器模块 - create_shader_modules(); - - // 创建管线布局 - create_pipeline_layout(); - - // 创建 Stencil 写入管线 - create_stencil_write_pipeline(render_pass); - - // 创建 Stencil 清除管线 - create_stencil_clear_pipeline(render_pass); - - initialized_ = true; -} - -void stencil_mask_renderer::cleanup() { - if (!initialized_) return; - - auto dev = device_.get_handle(); - - // 销毁管线 - if (stencil_write_pipeline_) { - dev.destroyPipeline(stencil_write_pipeline_); - stencil_write_pipeline_ = nullptr; - } - if (stencil_clear_pipeline_) { - dev.destroyPipeline(stencil_clear_pipeline_); - stencil_clear_pipeline_ = nullptr; - } - - // 销毁管线布局 - if (pipeline_layout_) { - dev.destroyPipelineLayout(pipeline_layout_); - pipeline_layout_ = nullptr; - } - - // 销毁着色器模块 - if (vert_shader_) { - dev.destroyShaderModule(vert_shader_); - vert_shader_ = nullptr; - } - if (frag_shader_) { - dev.destroyShaderModule(frag_shader_); - frag_shader_ = nullptr; - } - - // 清理帧资源 - for (auto& frame : frame_data_) { - if (frame.vertices.has_value()) - res_mgr_.destroy_buffer(frame.vertices->get_buffer()); - } - frame_data_.clear(); - - initialized_ = false; -} - -// ============================================================================ -// 帧管理 -// ============================================================================ - -void stencil_mask_renderer::begin_frame(uint32_t frame_index, const vec2f_t& viewport_size) { - current_frame_ = frame_index % config_.frames_in_flight; - viewport_size_ = viewport_size; - - // 重置遮罩栈 - while (!mask_stack_.empty()) { - mask_stack_.pop(); - } - current_stencil_ref_ = 0; - - // 重置顶点计数 - frame_data_[current_frame_].vertex_count = 0; -} - -void stencil_mask_renderer::end_frame() { - // 确保所有遮罩都已结束 - if (!mask_stack_.empty()) { - std::cerr << "[STENCIL_MASK_RENDERER] 警告:遮罩未正确结束" << std::endl; - } -} - -void stencil_mask_renderer::resize(vk::Extent2D new_extent) { - viewport_extent_ = new_extent; - viewport_size_ = vec2f_t{ - static_cast(new_extent.width), - static_cast(new_extent.height) - }; -} - -// ============================================================================ -// 查询方法 -// ============================================================================ - -auto stencil_mask_renderer::current_stencil_ref() const -> uint8_t { - return current_stencil_ref_; -} - -auto stencil_mask_renderer::is_in_mask() const -> bool { - return !mask_stack_.empty(); -} - -auto stencil_mask_renderer::get_mask_depth() const -> size_t { - return mask_stack_.size(); -} - -// ============================================================================ -// 着色器和管线创建(占位符,下一步实现) -// ============================================================================ - -void stencil_mask_renderer::create_shader_modules() { - // 使用专用的 stencil mask 着色器 - auto dev = device_.get_handle(); - - auto vert_shader_result = graphics_pipeline::load_shader_module_from_memory( - device_, shaders::stencil_mask_vert_spirv); - if (!vert_shader_result) { - throw std::runtime_error("Failed to load vertex shader for stencil mask renderer"); - } - auto vert_shader = vert_shader_result.value(); - - auto frag_shader_result = graphics_pipeline::load_shader_module_from_memory( - device_, shaders::stencil_mask_frag_spirv); - if (!frag_shader_result) { - dev.destroyShaderModule(vert_shader); - throw std::runtime_error("Failed to load fragment shader for stencil mask renderer"); - } - auto frag_shader = frag_shader_result.value(); - - // 保存着色器模块 - vert_shader_ = vert_shader; - frag_shader_ = frag_shader; -} - -void stencil_mask_renderer::create_pipeline_layout() { - // Push Constants 用于传递 MVP 矩阵 - vk::PushConstantRange push_constant{}; - push_constant.stageFlags = vk::ShaderStageFlagBits::eVertex; - push_constant.offset = 0; - push_constant.size = sizeof(Eigen::Matrix4f); // 只需要 MVP 矩阵 - - vk::PipelineLayoutCreateInfo layout_info{}; - layout_info.pushConstantRangeCount = 1; - layout_info.pPushConstantRanges = &push_constant; - - auto [result, layout] = device_.get_handle().createPipelineLayout(layout_info); - if (result != vk::Result::eSuccess) { - throw std::runtime_error("Failed to create pipeline layout for stencil mask renderer"); - } - pipeline_layout_ = layout; -} - -void stencil_mask_renderer::create_stencil_write_pipeline(vk::RenderPass render_pass) { - auto dev = device_.get_handle(); - - // 着色器阶段 - vk::PipelineShaderStageCreateInfo vert_stage{}; - vert_stage.stage = vk::ShaderStageFlagBits::eVertex; - vert_stage.module = vert_shader_; - vert_stage.pName = "main"; - - vk::PipelineShaderStageCreateInfo frag_stage{}; - frag_stage.stage = vk::ShaderStageFlagBits::eFragment; - frag_stage.module = frag_shader_; - frag_stage.pName = "main"; - - std::array shader_stages = {vert_stage, frag_stage}; - - // 顶点输入 - auto binding_desc = ui_vertex::get_binding_description(); - auto attribute_descs = ui_vertex::get_attribute_descriptions(); - auto vertex_input_info = ui_vertex::get_vertex_input_state(binding_desc, attribute_descs); - - // 输入装配 - vk::PipelineInputAssemblyStateCreateInfo input_assembly{}; - input_assembly.topology = vk::PrimitiveTopology::eTriangleList; - - // 视口状态(动态) - vk::PipelineViewportStateCreateInfo viewport_state{}; - viewport_state.viewportCount = 1; - viewport_state.scissorCount = 1; - - // 光栅化状态 - vk::PipelineRasterizationStateCreateInfo rasterizer{}; - rasterizer.polygonMode = vk::PolygonMode::eFill; - rasterizer.cullMode = vk::CullModeFlagBits::eNone; - rasterizer.frontFace = vk::FrontFace::eCounterClockwise; - rasterizer.lineWidth = 1.0f; - - // 多重采样 - vk::PipelineMultisampleStateCreateInfo multisampling{}; - multisampling.rasterizationSamples = vk::SampleCountFlagBits::e1; - - // 颜色混合 - 禁用颜色写入 - vk::PipelineColorBlendAttachmentState color_blend_attachment{}; - color_blend_attachment.blendEnable = VK_FALSE; - color_blend_attachment.colorWriteMask = vk::ColorComponentFlags{}; // 不写入颜色 - - vk::PipelineColorBlendStateCreateInfo color_blending{}; - color_blending.attachmentCount = 1; - color_blending.pAttachments = &color_blend_attachment; - - // 深度模板状态 - 关键的 Stencil 配置 - vk::PipelineDepthStencilStateCreateInfo depth_stencil{}; - depth_stencil.depthTestEnable = VK_FALSE; - depth_stencil.depthWriteEnable = VK_FALSE; - depth_stencil.stencilTestEnable = VK_TRUE; // 启用 Stencil 测试 - - // Stencil 前面状态 - depth_stencil.front.compareOp = vk::CompareOp::eAlways; // 总是通过 - depth_stencil.front.failOp = vk::StencilOp::eKeep; // 测试失败时保持 - depth_stencil.front.depthFailOp = vk::StencilOp::eKeep; // 深度测试失败时保持 - depth_stencil.front.passOp = vk::StencilOp::eReplace; // 测试通过时替换 - depth_stencil.front.compareMask = 0xFF; // 比较掩码 - depth_stencil.front.writeMask = 0xFF; // 写入掩码 - - // Stencil 后面状态 - depth_stencil.back.compareOp = vk::CompareOp::eAlways; - depth_stencil.back.failOp = vk::StencilOp::eKeep; - depth_stencil.back.depthFailOp = vk::StencilOp::eKeep; - depth_stencil.back.passOp = vk::StencilOp::eReplace; - depth_stencil.back.compareMask = 0xFF; // 比较掩码 - depth_stencil.back.writeMask = 0xFF; // 写入掩码 - - // 动态状态 - std::vector dynamic_states = { - vk::DynamicState::eViewport, - vk::DynamicState::eScissor, - vk::DynamicState::eStencilReference // 动态 Stencil 参考值 - }; - - vk::PipelineDynamicStateCreateInfo dynamic_state{}; - dynamic_state.dynamicStateCount = static_cast(dynamic_states.size()); - dynamic_state.pDynamicStates = dynamic_states.data(); - - // 管线创建信息 - vk::GraphicsPipelineCreateInfo pipeline_info{}; - pipeline_info.stageCount = static_cast(shader_stages.size()); - pipeline_info.pStages = shader_stages.data(); - pipeline_info.pVertexInputState = &vertex_input_info; - pipeline_info.pInputAssemblyState = &input_assembly; - pipeline_info.pViewportState = &viewport_state; - pipeline_info.pRasterizationState = &rasterizer; - pipeline_info.pMultisampleState = &multisampling; - pipeline_info.pDepthStencilState = &depth_stencil; - pipeline_info.pColorBlendState = &color_blending; - pipeline_info.pDynamicState = &dynamic_state; - pipeline_info.layout = pipeline_layout_; - pipeline_info.renderPass = render_pass; - pipeline_info.subpass = 0; - - auto [result, pipeline] = dev.createGraphicsPipeline(nullptr, pipeline_info); - if (result != vk::Result::eSuccess) { - throw std::runtime_error("Failed to create stencil write pipeline"); - } - - stencil_write_pipeline_ = pipeline; -} - -void stencil_mask_renderer::create_stencil_clear_pipeline(vk::RenderPass render_pass) { - auto dev = device_.get_handle(); - - // 着色器阶段 - vk::PipelineShaderStageCreateInfo vert_stage{}; - vert_stage.stage = vk::ShaderStageFlagBits::eVertex; - vert_stage.module = vert_shader_; - vert_stage.pName = "main"; - - vk::PipelineShaderStageCreateInfo frag_stage{}; - frag_stage.stage = vk::ShaderStageFlagBits::eFragment; - frag_stage.module = frag_shader_; - frag_stage.pName = "main"; - - std::array shader_stages = {vert_stage, frag_stage}; - - // 顶点输入 - auto binding_desc = ui_vertex::get_binding_description(); - auto attribute_descs = ui_vertex::get_attribute_descriptions(); - auto vertex_input_info = ui_vertex::get_vertex_input_state(binding_desc, attribute_descs); - - // 输入装配 - vk::PipelineInputAssemblyStateCreateInfo input_assembly{}; - input_assembly.topology = vk::PrimitiveTopology::eTriangleList; - - // 视口状态(动态) - vk::PipelineViewportStateCreateInfo viewport_state{}; - viewport_state.viewportCount = 1; - viewport_state.scissorCount = 1; - - // 光栅化状态 - vk::PipelineRasterizationStateCreateInfo rasterizer{}; - rasterizer.polygonMode = vk::PolygonMode::eFill; - rasterizer.cullMode = vk::CullModeFlagBits::eNone; - rasterizer.frontFace = vk::FrontFace::eCounterClockwise; - rasterizer.lineWidth = 1.0f; - - // 多重采样 - vk::PipelineMultisampleStateCreateInfo multisampling{}; - multisampling.rasterizationSamples = vk::SampleCountFlagBits::e1; - - // 颜色混合 - 禁用颜色写入 - vk::PipelineColorBlendAttachmentState color_blend_attachment{}; - color_blend_attachment.blendEnable = VK_FALSE; - color_blend_attachment.colorWriteMask = vk::ColorComponentFlags{}; // 不写入颜色 - - vk::PipelineColorBlendStateCreateInfo color_blending{}; - color_blending.attachmentCount = 1; - color_blending.pAttachments = &color_blend_attachment; - - // 深度模板状态 - Stencil 清除配置 - vk::PipelineDepthStencilStateCreateInfo depth_stencil{}; - depth_stencil.depthTestEnable = VK_FALSE; - depth_stencil.depthWriteEnable = VK_FALSE; - depth_stencil.stencilTestEnable = VK_TRUE; // 启用 Stencil 测试 - - // Stencil 前面状态 - 清除当前层 - depth_stencil.front.compareOp = vk::CompareOp::eEqual; // 只处理等于参考值的像素 - depth_stencil.front.failOp = vk::StencilOp::eKeep; // 测试失败时保持 - depth_stencil.front.depthFailOp = vk::StencilOp::eKeep; // 深度测试失败时保持 - depth_stencil.front.passOp = vk::StencilOp::eDecrementAndClamp; // 通过时递减并限制到0 - depth_stencil.front.compareMask = 0xFF; // 比较掩码 - depth_stencil.front.writeMask = 0xFF; // 写入掩码 - - // Stencil 后面状态 - depth_stencil.back.compareOp = vk::CompareOp::eEqual; - depth_stencil.back.failOp = vk::StencilOp::eKeep; - depth_stencil.back.depthFailOp = vk::StencilOp::eKeep; - depth_stencil.back.passOp = vk::StencilOp::eDecrementAndClamp; - depth_stencil.back.compareMask = 0xFF; // 比较掩码 - depth_stencil.back.writeMask = 0xFF; // 写入掩码 - - // 动态状态 - std::vector dynamic_states = { - vk::DynamicState::eViewport, - vk::DynamicState::eScissor, - vk::DynamicState::eStencilReference // 动态 Stencil 参考值 - }; - - vk::PipelineDynamicStateCreateInfo dynamic_state{}; - dynamic_state.dynamicStateCount = static_cast(dynamic_states.size()); - dynamic_state.pDynamicStates = dynamic_states.data(); - - // 管线创建信息 - vk::GraphicsPipelineCreateInfo pipeline_info{}; - pipeline_info.stageCount = static_cast(shader_stages.size()); - pipeline_info.pStages = shader_stages.data(); - pipeline_info.pVertexInputState = &vertex_input_info; - pipeline_info.pInputAssemblyState = &input_assembly; - pipeline_info.pViewportState = &viewport_state; - pipeline_info.pRasterizationState = &rasterizer; - pipeline_info.pMultisampleState = &multisampling; - pipeline_info.pDepthStencilState = &depth_stencil; - pipeline_info.pColorBlendState = &color_blending; - pipeline_info.pDynamicState = &dynamic_state; - pipeline_info.layout = pipeline_layout_; - pipeline_info.renderPass = render_pass; - pipeline_info.subpass = 0; - - auto [result, pipeline] = dev.createGraphicsPipeline(nullptr, pipeline_info); - if (result != vk::Result::eSuccess) { - throw std::runtime_error("Failed to create stencil clear pipeline"); - } - - stencil_clear_pipeline_ = pipeline; -} - -// ============================================================================ -// 遮罩操作(占位符,下一步实现) -// ============================================================================ - -void stencil_mask_renderer::begin_mask(vk::CommandBuffer cmd, const mask_params& params, const aabb2d_t& bounds) { - // 递增 stencil 参考值 - if (current_stencil_ref_ >= config_.max_nested_depth) { - std::cerr << "[STENCIL_MASK_RENDERER] 警告:超过最大嵌套深度 " << config_.max_nested_depth << std::endl; - return; - } - - ++current_stencil_ref_; - - // 压栈保存遮罩状态 - mask_stack_.push({ - params, - bounds, - current_stencil_ref_ - }); - - // 绘制遮罩形状到 Stencil Buffer - draw_mask_shape(cmd, params, bounds, false); // false = 写入模式 -} - -void stencil_mask_renderer::end_mask(vk::CommandBuffer cmd) { - if (mask_stack_.empty()) { - std::cerr << "[STENCIL_MASK_RENDERER] 警告:尝试结束不存在的遮罩" << std::endl; - return; - } - - auto& mask_state = mask_stack_.top(); - - // 清除 Stencil 值 - draw_mask_shape(cmd, mask_state.params, mask_state.content_bounds, true); // true = 清除模式 - - // 出栈 - mask_stack_.pop(); - - // 递减 stencil 参考值 - if (current_stencil_ref_ > 0) { - --current_stencil_ref_; - } -} - -void stencil_mask_renderer::draw_mask_shape(vk::CommandBuffer cmd, const mask_params& params, const aabb2d_t& bounds, bool is_clear) { - auto& frame = frame_data_[current_frame_]; - - // 生成遮罩顶点 - generate_mask_vertices(params, bounds); - - // 检查顶点缓冲空间 - if (frame.vertex_count + 6 > frame.vertices->size()) { - std::cerr << "[STENCIL_MASK_RENDERER] 顶点缓冲溢出" << std::endl; - return; - } - - // 绑定相应的管线 - if (is_clear) { - cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, stencil_clear_pipeline_); - } else { - cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, stencil_write_pipeline_); - } - - // 设置视口和裁剪 - vk::Viewport viewport{ - 0.0f, 0.0f, - viewport_size_.x(), viewport_size_.y(), - 0.0f, 1.0f - }; - cmd.setViewport(0, 1, &viewport); - - vk::Rect2D scissor{ - {0, 0}, - {static_cast(viewport_size_.x()), static_cast(viewport_size_.y())} - }; - cmd.setScissor(0, 1, &scissor); - - // 设置动态 Stencil 参考值 - cmd.setStencilReference(vk::StencilFaceFlagBits::eFrontAndBack, current_stencil_ref_); - - // 创建简化的 MVP 矩阵(只需要投影矩阵) - auto proj = create_ortho_projection(viewport_size_.x(), viewport_size_.y()); - - // 更新 Push Constants - 只传递 MVP 矩阵 - cmd.pushConstants( - pipeline_layout_, - vk::ShaderStageFlagBits::eVertex, - 0, - sizeof(std::array), - &proj - ); - - // 上传顶点数据 - std::vector vertices(mask_vertices_.begin(), mask_vertices_.end()); - frame.vertices->upload(vertices, frame.vertex_count); - - // 绑定顶点缓冲 - vk::Buffer vertex_buffers[] = {frame.vertices->get_buffer().buffer}; - vk::DeviceSize offsets[] = {0}; - cmd.bindVertexBuffers(0, 1, vertex_buffers, offsets); - - // 绘制四边形(6个顶点,2个三角形) - cmd.draw(6, 1, 0, frame.vertex_count); - - // 更新顶点计数 - frame.vertex_count += 6; -} - -void stencil_mask_renderer::generate_mask_vertices(const mask_params& params, const aabb2d_t& bounds) { - float left = bounds.min().x(); - float top = bounds.min().y(); - float right = bounds.max().x(); - float bottom = bounds.max().y(); - - // 计算 UV 坐标(基于视口大小) - float uv_left = left / viewport_size_.x(); - float uv_top = top / viewport_size_.y(); - float uv_right = right / viewport_size_.x(); - float uv_bottom = bottom / viewport_size_.y(); - - // 使用实际边界框尺寸 - float width = bounds.sizes().x(); - float height = bounds.sizes().y(); - - float mask_type_f = static_cast(static_cast(params.type)); - float corner_tl = params.corner_radii.top_left; - float corner_tr = params.corner_radii.top_right; - float corner_br = params.corner_radii.bottom_right; - float corner_bl = params.corner_radii.bottom_left; - - // 顶点 0: 左上 - mask_vertices_[0].position[0] = left; - mask_vertices_[0].position[1] = top; - mask_vertices_[0].color[0] = 1.0f; - mask_vertices_[0].color[1] = 1.0f; - mask_vertices_[0].color[2] = 1.0f; - mask_vertices_[0].color[3] = 1.0f; - mask_vertices_[0].uv[0] = uv_left; - mask_vertices_[0].uv[1] = uv_top; - mask_vertices_[0].data_a[0] = mask_type_f; - mask_vertices_[0].data_a[1] = params.softness; - mask_vertices_[0].data_a[2] = width; - mask_vertices_[0].data_a[3] = height; - mask_vertices_[0].data_b[0] = corner_tl; - mask_vertices_[0].data_b[1] = corner_tr; - mask_vertices_[0].data_b[2] = corner_br; - mask_vertices_[0].data_b[3] = corner_bl; - mask_vertices_[0].data_c[0] = 0.0f; // local_u - mask_vertices_[0].data_c[1] = 0.0f; // local_v - mask_vertices_[0].data_c[2] = 0.0f; // reserved - mask_vertices_[0].data_c[3] = 0.0f; // reserved - - // 顶点 1: 右上 - mask_vertices_[1].position[0] = right; - mask_vertices_[1].position[1] = top; - mask_vertices_[1].color[0] = 1.0f; - mask_vertices_[1].color[1] = 1.0f; - mask_vertices_[1].color[2] = 1.0f; - mask_vertices_[1].color[3] = 1.0f; - mask_vertices_[1].uv[0] = uv_right; - mask_vertices_[1].uv[1] = uv_top; - mask_vertices_[1].data_a[0] = mask_type_f; - mask_vertices_[1].data_a[1] = params.softness; - mask_vertices_[1].data_a[2] = width; - mask_vertices_[1].data_a[3] = height; - mask_vertices_[1].data_b[0] = corner_tl; - mask_vertices_[1].data_b[1] = corner_tr; - mask_vertices_[1].data_b[2] = corner_br; - mask_vertices_[1].data_b[3] = corner_bl; - mask_vertices_[1].data_c[0] = 1.0f; // local_u - mask_vertices_[1].data_c[1] = 0.0f; // local_v - mask_vertices_[1].data_c[2] = 0.0f; // reserved - mask_vertices_[1].data_c[3] = 0.0f; // reserved - - // 顶点 2: 右下 - mask_vertices_[2].position[0] = right; - mask_vertices_[2].position[1] = bottom; - mask_vertices_[2].color[0] = 1.0f; - mask_vertices_[2].color[1] = 1.0f; - mask_vertices_[2].color[2] = 1.0f; - mask_vertices_[2].color[3] = 1.0f; - mask_vertices_[2].uv[0] = uv_right; - mask_vertices_[2].uv[1] = uv_bottom; - mask_vertices_[2].data_a[0] = mask_type_f; - mask_vertices_[2].data_a[1] = params.softness; - mask_vertices_[2].data_a[2] = width; - mask_vertices_[2].data_a[3] = height; - mask_vertices_[2].data_b[0] = corner_tl; - mask_vertices_[2].data_b[1] = corner_tr; - mask_vertices_[2].data_b[2] = corner_br; - mask_vertices_[2].data_b[3] = corner_bl; - mask_vertices_[2].data_c[0] = 1.0f; // local_u - mask_vertices_[2].data_c[1] = 1.0f; // local_v - mask_vertices_[2].data_c[2] = 0.0f; // reserved - mask_vertices_[2].data_c[3] = 0.0f; // reserved - - // 顶点 3: 左下 - mask_vertices_[3].position[0] = left; - mask_vertices_[3].position[1] = bottom; - mask_vertices_[3].color[0] = 1.0f; - mask_vertices_[3].color[1] = 1.0f; - mask_vertices_[3].color[2] = 1.0f; - mask_vertices_[3].color[3] = 1.0f; - mask_vertices_[3].uv[0] = uv_left; - mask_vertices_[3].uv[1] = uv_bottom; - mask_vertices_[3].data_a[0] = mask_type_f; - mask_vertices_[3].data_a[1] = params.softness; - mask_vertices_[3].data_a[2] = width; - mask_vertices_[3].data_a[3] = height; - mask_vertices_[3].data_b[0] = corner_tl; - mask_vertices_[3].data_b[1] = corner_tr; - mask_vertices_[3].data_b[2] = corner_br; - mask_vertices_[3].data_b[3] = corner_bl; - mask_vertices_[3].data_c[0] = 0.0f; // local_u - mask_vertices_[3].data_c[1] = 1.0f; // local_v - mask_vertices_[3].data_c[2] = 0.0f; // reserved - mask_vertices_[3].data_c[3] = 0.0f; // reserved - - // 复制顶点用于两个三角形 (0,1,2) 和 (2,3,0) - mask_vertices_[4] = mask_vertices_[2]; - mask_vertices_[5] = mask_vertices_[0]; -} - -} // namespace mirage \ No newline at end of file diff --git a/src/render/pipeline/stencil_mask_renderer.h b/src/render/pipeline/stencil_mask_renderer.h deleted file mode 100644 index dec556e..0000000 --- a/src/render/pipeline/stencil_mask_renderer.h +++ /dev/null @@ -1,141 +0,0 @@ -#pragma once - -#include "types.h" -#include "offscreen_target.h" -#include "vulkan/logical_device.h" -#include "vulkan/resource_manager.h" -#include "vulkan/typed_buffer.h" -#include "renderer/vertex_types.h" -#include "render_command.h" -#include -#include -#include -#include - -namespace mirage { - -/// Stencil 遮罩状态 -struct stencil_mask_state { - mask_params params; ///< 遮罩参数 - aabb2d_t content_bounds; ///< 内容边界 - uint8_t stencil_ref; ///< Stencil 参考值 -}; - -/// Stencil 遮罩渲染器 -/// 使用 Stencil Buffer 实现遮罩,避免离屏目标切换 -class stencil_mask_renderer { -public: - struct config { - uint32_t frames_in_flight = 2; - uint32_t max_nested_depth = 8; ///< 最大嵌套深度 - }; - - stencil_mask_renderer( - logical_device& device, - resource_manager& res_mgr, - const config& cfg = {} - ); - - ~stencil_mask_renderer(); - - // 禁止拷贝 - stencil_mask_renderer(const stencil_mask_renderer&) = delete; - stencil_mask_renderer& operator=(const stencil_mask_renderer&) = delete; - - /// 初始化 - /// @param render_pass 必须是带 Stencil 附件的 RenderPass - /// @param viewport_extent 视口大小 - void initialize(vk::RenderPass render_pass, vk::Extent2D viewport_extent); - - void cleanup(); - void begin_frame(uint32_t frame_index, const vec2f_t& viewport_size); - void end_frame(); - - // ======================================================================== - // Stencil 遮罩操作 - // ======================================================================== - - /// 开始遮罩 - 写入 Stencil 值 - /// @param cmd 命令缓冲 - /// @param params 遮罩参数 - /// @param bounds 遮罩边界 - /// @note 不切换渲染目标,只修改 Stencil 状态 - void begin_mask(vk::CommandBuffer cmd, const mask_params& params, const aabb2d_t& bounds); - - /// 结束遮罩 - 清理 Stencil 值 - /// @param cmd 命令缓冲 - void end_mask(vk::CommandBuffer cmd); - - /// 获取当前 Stencil 参考值(用于内容渲染管线) - [[nodiscard]] auto current_stencil_ref() const -> uint8_t; - - /// 是否在遮罩中 - [[nodiscard]] auto is_in_mask() const -> bool; - - /// 获取嵌套深度 - [[nodiscard]] auto get_mask_depth() const -> size_t; - - void resize(vk::Extent2D new_extent); - -private: - // 创建着色器模块 - void create_shader_modules(); - - // 创建 Pipeline Layout - void create_pipeline_layout(); - - // 创建 Stencil 写入管线(只写 Stencil,不写颜色) - void create_stencil_write_pipeline(vk::RenderPass render_pass); - - // 创建 Stencil 清除管线 - void create_stencil_clear_pipeline(vk::RenderPass render_pass); - - // 绘制遮罩形状到 Stencil - void draw_mask_shape(vk::CommandBuffer cmd, const mask_params& params, const aabb2d_t& bounds, bool is_clear); - - // 生成遮罩四边形顶点 - void generate_mask_vertices(const mask_params& params, const aabb2d_t& bounds); - - logical_device& device_; - resource_manager& res_mgr_; - config config_; - - // 着色器模块 - vk::ShaderModule vert_shader_{}; - vk::ShaderModule frag_shader_{}; - - // Pipeline Layout - vk::PipelineLayout pipeline_layout_{}; - - // Stencil 写入管线(写入 Stencil,不写颜色) - vk::Pipeline stencil_write_pipeline_{}; - - // Stencil 清除管线(递减 Stencil) - vk::Pipeline stencil_clear_pipeline_{}; - - // 遮罩栈 - std::stack mask_stack_; - - // 当前 Stencil 参考值(随嵌套深度递增) - uint8_t current_stencil_ref_ = 0; - - // 视口信息 - vk::Extent2D viewport_extent_{}; - vec2f_t viewport_size_{0.0f, 0.0f}; - - // 帧资源 - struct frame_data { - std::optional> vertices; - uint32_t vertex_count = 0; - }; - std::vector frame_data_; - uint32_t current_frame_ = 0; - - // 临时顶点存储(用于 generate_mask_vertices) - std::array mask_vertices_; - - // 是否已初始化 - bool initialized_ = false; -}; - -} // namespace mirage \ No newline at end of file diff --git a/src/render/pipeline/types.h b/src/render/pipeline/types.h index 226089d..829ea95 100644 --- a/src/render/pipeline/types.h +++ b/src/render/pipeline/types.h @@ -8,18 +8,6 @@ #include "renderer/vertex_types.h" namespace mirage { - // ============================================================================ - // 渲染段类型 - // ============================================================================ - - /// 渲染段类型枚举 - enum class segment_type { - geometry, ///< 几何渲染段 - post_effect, ///< 后效处理段 - mask_begin, ///< 遮罩开始段 - mask_end ///< 遮罩结束段 - }; - // ============================================================================ // 绘制批次 // ============================================================================ @@ -48,22 +36,4 @@ namespace mirage { indices.reserve(index_count); } }; - - // ============================================================================ - // 渲染段 - // ============================================================================ - - /// 渲染段 - 表示一个连续的渲染操作 - struct render_segment { - segment_type type; - - // 几何段数据(type == geometry 时有效) - std::vector batches; - - // 后效段数据(type == post_effect 时有效) - std::optional effect; - - // 遮罩段数据(type == mask_begin 时有效) - std::optional mask_begin; - }; } // namespace mirage diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ad7e0f3..ee20ec8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,142 +6,7 @@ cmake_minimum_required(VERSION 3.21) find_package(GTest CONFIG REQUIRED) include(GoogleTest) -# Find Vulkan (required for tests) -find_package(Vulkan REQUIRED) - -# ============================================================================ -# Test Utilities Library -# ============================================================================ -add_library(test_utils STATIC - utils/vulkan_test_base.cpp - utils/test_helpers.cpp - utils/shader_loader.cpp - utils/mock_window.cpp -) - -target_include_directories(test_utils PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_SOURCE_DIR}/src -) - -target_link_libraries(test_utils PUBLIC - GTest::gtest - GTest::gtest_main - Vulkan::Vulkan - render_core -) - -target_compile_definitions(test_utils PUBLIC - VULKAN_HPP_NO_EXCEPTIONS -) - -# ============================================================================ -# Test Shader Compilation -# ============================================================================ -# Define test shader directory -set(TEST_SHADER_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders" CACHE PATH "Test shader output directory") -file(MAKE_DIRECTORY ${TEST_SHADER_DIR}) - -# Add test shaders for compilation -add_mirage_shader_directory(${CMAKE_CURRENT_SOURCE_DIR}/shaders) - -# Pass shader directory to test utils -target_compile_definitions(test_utils PUBLIC - TEST_SHADER_DIR="${TEST_SHADER_DIR}" -) - -# ============================================================================ -# Unit Tests -# ============================================================================ - -# Core component tests -add_executable(test_vulkan_core - unit/core/test_context.cpp - unit/core/test_device_manager.cpp - unit/core/test_logical_device.cpp -) - -target_link_libraries(test_vulkan_core PRIVATE - test_utils - GTest::gtest - GTest::gtest_main -) - -# Register tests with CTest -gtest_discover_tests(test_vulkan_core - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - PROPERTIES - LABELS "vulkan;core" -) - -# Pipeline component tests -add_executable(test_vulkan_pipeline - unit/pipeline/test_pipeline_builder.cpp - unit/pipeline/test_graphics_pipeline.cpp - unit/pipeline/test_compute_pipeline.cpp - unit/pipeline/test_pass_state.cpp -) - -target_link_libraries(test_vulkan_pipeline PRIVATE - test_utils - GTest::gtest - GTest::gtest_main -) - -# Register tests with CTest -gtest_discover_tests(test_vulkan_pipeline - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - PROPERTIES - LABELS "vulkan;pipeline" -) - -# Resource management tests -add_executable(test_vulkan_resource - unit/resource/test_resource_manager.cpp - unit/resource/test_typed_buffer.cpp - unit/resource/test_command_buffer.cpp -) - -target_link_libraries(test_vulkan_resource PRIVATE - test_utils - GTest::gtest - GTest::gtest_main -) - -# Register tests with CTest -gtest_discover_tests(test_vulkan_resource - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - PROPERTIES - LABELS "vulkan;resource" -) - -# Scheduler tests -add_executable(test_vulkan_scheduler - unit/scheduler/test_compute_scheduler.cpp -) - -target_link_libraries(test_vulkan_scheduler PRIVATE - test_utils - GTest::gtest - GTest::gtest_main -) - -# Register tests with CTest -gtest_discover_tests(test_vulkan_scheduler - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - PROPERTIES - LABELS "vulkan;scheduler" -) - -# ============================================================================ -# Test Configuration -# ============================================================================ - -# Enable testing -enable_testing() - -# Print configuration -message(STATUS "Test configuration:") -message(STATUS " Test shader directory: ${TEST_SHADER_DIR}") -message(STATUS " Google Test found: ${GTest_FOUND}") -message(STATUS " Vulkan found: ${Vulkan_FOUND}") +add_subdirectory(render_tree) +add_subdirectory(render_tree_builder) +add_subdirectory(render_tree_integration) +add_subdirectory(test_refactor_syntax) diff --git a/tests/generated/runtime_array_test_comp.h b/tests/generated/runtime_array_test_comp.h deleted file mode 100644 index de3bcf8..0000000 --- a/tests/generated/runtime_array_test_comp.h +++ /dev/null @@ -1,490 +0,0 @@ - -#pragma once - -#include -#include -#include -#include -#include -#include "vulkan/typed_buffer.h" - -namespace mirage::shaders { - -// ============ 缓冲区结构 ============ -// 根据 SPIR-V 反射自动生成 - -// ============ Buffer Structures ============ -// Auto-generated from SPIR-V reflection - - -// 由 SPIR-V 反射生成 -// 绑定:0,描述符集:0 -// 类型:storage_buffer -struct alignas(16) PositionBuffer { - - // 偏移量:0,大小:runtime,对齐方式:16 - uint32_t positions_count; // 运行时数组元素计数 - alignas(16) Eigen::Vector4f positions[]; // 运行时数组数据 - - // std::span 访问器(非 const 版本) - std::span get_positions() { - return std::span(positions, positions_count); - } - - // std::span 访问器(const 版本) - std::span get_positions() const { - return std::span(positions, positions_count); - } -}; - - -// 由 SPIR-V 反射生成 -// 绑定:1,描述符集:0 -// 类型:storage_buffer -struct alignas(16) VelocityBuffer { - - // 偏移量:0,大小:runtime,对齐方式:16 - uint32_t velocities_count; // 运行时数组元素计数 - alignas(16) Eigen::Vector3f velocities[]; // 运行时数组数据 - - // std::span 访问器(非 const 版本) - std::span get_velocities() { - return std::span(velocities, velocities_count); - } - - // std::span 访问器(const 版本) - std::span get_velocities() const { - return std::span(velocities, velocities_count); - } -}; - - -// 由 SPIR-V 反射生成 -// 绑定:2,描述符集:0 -// 类型:uniform_buffer -struct alignas(4) ControlParams { - - // 偏移量:0,大小:4,对齐方式:4 - float deltaTime; - - // 偏移量:4,大小:4,对齐方式:4 - uint32_t particleCount; - - // 偏移量:8,大小:4,对齐方式:4 - float damping; -}; - - -// ============ 缓冲区辅助函数 ============ -// 自动生成缓冲区数据管理的便利函数 - -// ============ 缓冲区辅助函数 ============ -// 自动生成缓冲区数据管理的便利函数 - - -// 为 buffer_0 创建缓冲区(绑定 0) -inline auto create_buffer_0_buffer( - mirage::render::vulkan::resource_manager& rm, - size_t element_count -) -> mirage::render::vulkan::expected -{ - return rm.create_buffer( - element_count * sizeof(PositionBuffer), - vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); -} - - -// 将数据上传到 buffer_0 缓冲区 -inline void upload_buffer_0( - vk::Device device, - const mirage::render::vulkan::resource_manager::buffer_resource& buffer, - std::span data -) -{ - if (data.size_bytes() > buffer.size) { - throw std::runtime_error("Data size exceeds buffer capacity"); - } - - void* mapped = device.mapMemory(buffer.memory, 0, buffer.size); - std::memcpy(mapped, data.data(), data.size_bytes()); - device.unmapMemory(buffer.memory); -} - - -// 从 buffer_0 缓冲区下载数据 -inline void download_buffer_0( - vk::Device device, - const mirage::render::vulkan::resource_manager::buffer_resource& buffer, - std::span out_data -) -{ - if (out_data.size_bytes() > buffer.size) { - throw std::runtime_error("Output buffer size exceeds buffer capacity"); - } - - void* mapped = device.mapMemory(buffer.memory, 0, buffer.size); - std::memcpy(out_data.data(), mapped, out_data.size_bytes()); - device.unmapMemory(buffer.memory); -} - - -// 为 buffer_1 创建缓冲区(绑定 1) -inline auto create_buffer_1_buffer( - mirage::render::vulkan::resource_manager& rm, - size_t element_count -) -> mirage::render::vulkan::expected -{ - return rm.create_buffer( - element_count * sizeof(VelocityBuffer), - vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); -} - - -// 将数据上传到 buffer_1 缓冲区 -inline void upload_buffer_1( - vk::Device device, - const mirage::render::vulkan::resource_manager::buffer_resource& buffer, - std::span data -) -{ - if (data.size_bytes() > buffer.size) { - throw std::runtime_error("Data size exceeds buffer capacity"); - } - - void* mapped = device.mapMemory(buffer.memory, 0, buffer.size); - std::memcpy(mapped, data.data(), data.size_bytes()); - device.unmapMemory(buffer.memory); -} - - -// 从 buffer_1 缓冲区下载数据 -inline void download_buffer_1( - vk::Device device, - const mirage::render::vulkan::resource_manager::buffer_resource& buffer, - std::span out_data -) -{ - if (out_data.size_bytes() > buffer.size) { - throw std::runtime_error("Output buffer size exceeds buffer capacity"); - } - - void* mapped = device.mapMemory(buffer.memory, 0, buffer.size); - std::memcpy(out_data.data(), mapped, out_data.size_bytes()); - device.unmapMemory(buffer.memory); -} - - -// 为 buffer_2 创建缓冲区(绑定 2) -inline auto create_buffer_2_buffer( - mirage::render::vulkan::resource_manager& rm, - size_t element_count -) -> mirage::render::vulkan::expected -{ - return rm.create_buffer( - element_count * sizeof(ControlParams), - vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); -} - - -// 将数据上传到 buffer_2 缓冲区 -inline void upload_buffer_2( - vk::Device device, - const mirage::render::vulkan::resource_manager::buffer_resource& buffer, - std::span data -) -{ - if (data.size_bytes() > buffer.size) { - throw std::runtime_error("Data size exceeds buffer capacity"); - } - - void* mapped = device.mapMemory(buffer.memory, 0, buffer.size); - std::memcpy(mapped, data.data(), data.size_bytes()); - device.unmapMemory(buffer.memory); -} - - -// 从 buffer_2 缓冲区下载数据 -inline void download_buffer_2( - vk::Device device, - const mirage::render::vulkan::resource_manager::buffer_resource& buffer, - std::span out_data -) -{ - if (out_data.size_bytes() > buffer.size) { - throw std::runtime_error("Output buffer size exceeds buffer capacity"); - } - - void* mapped = device.mapMemory(buffer.memory, 0, buffer.size); - std::memcpy(out_data.data(), mapped, out_data.size_bytes()); - device.unmapMemory(buffer.memory); -} - - -// ============ 类型化缓冲区别名 ============ -// 类型安全缓冲区包装器的类型别名和工厂函数 - -// ============ Typed Buffer Aliases ============ -// Type aliases and factory functions for type-safe buffer wrappers - - -// buffer_0 类型缓冲区的类型别名 -using buffer_0_buffer = mirage::render::vulkan::typed_buffer; - -// 为 buffer_0 创建类型化缓冲区(绑定 0) -inline auto create_buffer_0_typed( - vk::Device device, - mirage::render::vulkan::resource_manager& rm, - size_t count -) -> mirage::render::vulkan::expected -{ - auto buffer_result = create_buffer_0_buffer(rm, count); - if (!buffer_result) { - return std::unexpected(buffer_result.error()); - } - - return buffer_0_buffer(device, std::move(*buffer_result), count); -} - - -// buffer_1 类型缓冲区的类型别名 -using buffer_1_buffer = mirage::render::vulkan::typed_buffer; - -// 为 buffer_1 创建类型化缓冲区(绑定 1) -inline auto create_buffer_1_typed( - vk::Device device, - mirage::render::vulkan::resource_manager& rm, - size_t count -) -> mirage::render::vulkan::expected -{ - auto buffer_result = create_buffer_1_buffer(rm, count); - if (!buffer_result) { - return std::unexpected(buffer_result.error()); - } - - return buffer_1_buffer(device, std::move(*buffer_result), count); -} - - -// buffer_2 类型缓冲区的类型别名 -using buffer_2_buffer = mirage::render::vulkan::typed_buffer; - -// 为 buffer_2 创建类型化缓冲区(绑定 2) -inline auto create_buffer_2_typed( - vk::Device device, - mirage::render::vulkan::resource_manager& rm, - size_t count -) -> mirage::render::vulkan::expected -{ - auto buffer_result = create_buffer_2_buffer(rm, count); - if (!buffer_result) { - return std::unexpected(buffer_result.error()); - } - - return buffer_2_buffer(device, std::move(*buffer_result), count); -} - - -// ============ 缓冲区管理器类 ============ -// 所有着色器缓冲区的自动生命周期管理 - -// ============ Buffer Manager Class ============ -// Automatic lifecycle management for all shader buffers - - -class runtime_array_test_buffer_manager { -public: - // 所有缓冲区的容器 - struct buffers { - buffer_0_buffer buffer_0; - buffer_1_buffer buffer_1; - buffer_2_buffer buffer_2; - }; - - // 创建缓冲区管理器的工厂方法 - static auto create( - vk::Device device, - mirage::render::vulkan::resource_manager& rm, - size_t element_count - ) -> mirage::render::vulkan::expected - { - auto buffer_0_result = create_buffer_0_typed(device, rm, element_count); - if (!buffer_0_result) { - return std::unexpected(buffer_0_result.error()); - } - - - - auto buffer_1_result = create_buffer_1_typed(device, rm, element_count); - if (!buffer_1_result) { - return std::unexpected(buffer_1_result.error()); - } - - - - auto buffer_2_result = create_buffer_2_typed(device, rm, element_count); - if (!buffer_2_result) { - return std::unexpected(buffer_2_result.error()); - } - - - return runtime_array_test_buffer_manager( - device, - std::move(*buffer_0_result), - std::move(*buffer_1_result), - std::move(*buffer_2_result) - ); - } - - // 用数据初始化所有缓冲区 - void initialize( - std::span buffer_0_data, - std::span buffer_1_data, - std::span buffer_2_data - ) { - buffers_.buffer_0.upload(buffer_0_data); - buffers_.buffer_1.upload(buffer_1_data); - buffers_.buffer_2.upload(buffer_2_data); - } - - // 将所有缓冲区绑定到描述符集 - void bind_to_descriptor_set( - const mirage::render::vulkan::compute_pipeline& pipeline, - vk::DescriptorSet descriptor_set - ) const { - pipeline.bind_buffer( - descriptor_set, - 0, - buffers_.buffer_0.get_buffer(), - vk::DescriptorType::eStorageBuffer - ); - pipeline.bind_buffer( - descriptor_set, - 1, - buffers_.buffer_1.get_buffer(), - vk::DescriptorType::eStorageBuffer - ); - pipeline.bind_buffer( - descriptor_set, - 2, - buffers_.buffer_2.get_buffer(), - vk::DescriptorType::eUniformBuffer - ); - } - - // 访问缓冲区 - auto& get_buffers() { return buffers_; } - const auto& get_buffers() const { return buffers_; } - -private: - runtime_array_test_buffer_manager( - vk::Device device, - buffer_0_buffer buffer_0, - buffer_1_buffer buffer_1, - buffer_2_buffer buffer_2 - ) : device_(device), - buffers_{ - .buffer_0 = std::move(buffer_0), - .buffer_1 = std::move(buffer_1), - .buffer_2 = std::move(buffer_2) - } {} - - vk::Device device_; - buffers buffers_; -}; - -// COMP 着色器:runtime_array_test.comp.glsl -// 编译为:glslc ... --target-env=vulkan1.2 - -static constexpr uint32_t runtime_array_test_comp_spirv[] = { - 0x07230203, 0x00010500, 0x000d000b, 0x0000004b, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, - 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0009000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x00000014, 0x00000023, - 0x0000002a, 0x00060010, 0x00000004, 0x00000011, 0x00000100, 0x00000001, 0x00000001, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, - 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, - 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00030005, 0x00000008, 0x00786469, 0x00080005, - 0x0000000b, 0x475f6c67, 0x61626f6c, 0x766e496c, 0x7461636f, 0x496e6f69, 0x00000044, 0x00060005, 0x00000012, 0x746e6f43, 0x506c6f72, 0x6d617261, - 0x00000073, 0x00060006, 0x00000012, 0x00000000, 0x746c6564, 0x6d695461, 0x00000065, 0x00070006, 0x00000012, 0x00000001, 0x74726170, 0x656c6369, - 0x6e756f43, 0x00000074, 0x00050006, 0x00000012, 0x00000002, 0x706d6164, 0x00676e69, 0x00030005, 0x00000014, 0x00000000, 0x00060005, 0x00000021, - 0x69736f50, 0x6e6f6974, 0x66667542, 0x00007265, 0x00060006, 0x00000021, 0x00000000, 0x69736f70, 0x6e6f6974, 0x00000073, 0x00030005, 0x00000023, - 0x00000000, 0x00060005, 0x00000028, 0x6f6c6556, 0x79746963, 0x66667542, 0x00007265, 0x00060006, 0x00000028, 0x00000000, 0x6f6c6576, 0x69746963, - 0x00007365, 0x00030005, 0x0000002a, 0x00000000, 0x00040047, 0x0000000b, 0x0000000b, 0x0000001c, 0x00030047, 0x00000012, 0x00000002, 0x00050048, - 0x00000012, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000012, 0x00000001, 0x00000023, 0x00000004, 0x00050048, 0x00000012, 0x00000002, - 0x00000023, 0x00000008, 0x00040047, 0x00000014, 0x00000021, 0x00000002, 0x00040047, 0x00000014, 0x00000022, 0x00000000, 0x00040047, 0x00000020, - 0x00000006, 0x00000010, 0x00030047, 0x00000021, 0x00000002, 0x00050048, 0x00000021, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x00000023, - 0x00000021, 0x00000000, 0x00040047, 0x00000023, 0x00000022, 0x00000000, 0x00040047, 0x00000027, 0x00000006, 0x00000010, 0x00030047, 0x00000028, - 0x00000002, 0x00050048, 0x00000028, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000002a, 0x00000021, 0x00000001, 0x00040047, 0x0000002a, - 0x00000022, 0x00000000, 0x00040047, 0x0000004a, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, - 0x00000006, 0x00000020, 0x00000000, 0x00040020, 0x00000007, 0x00000007, 0x00000006, 0x00040017, 0x00000009, 0x00000006, 0x00000003, 0x00040020, - 0x0000000a, 0x00000001, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000001, 0x0004002b, 0x00000006, 0x0000000c, 0x00000000, 0x00040020, - 0x0000000d, 0x00000001, 0x00000006, 0x00030016, 0x00000011, 0x00000020, 0x0005001e, 0x00000012, 0x00000011, 0x00000006, 0x00000011, 0x00040020, - 0x00000013, 0x00000002, 0x00000012, 0x0004003b, 0x00000013, 0x00000014, 0x00000002, 0x00040015, 0x00000015, 0x00000020, 0x00000001, 0x0004002b, - 0x00000015, 0x00000016, 0x00000001, 0x00040020, 0x00000017, 0x00000002, 0x00000006, 0x00020014, 0x0000001a, 0x00040017, 0x0000001f, 0x00000011, - 0x00000004, 0x0003001d, 0x00000020, 0x0000001f, 0x0003001e, 0x00000021, 0x00000020, 0x00040020, 0x00000022, 0x0000000c, 0x00000021, 0x0004003b, - 0x00000022, 0x00000023, 0x0000000c, 0x0004002b, 0x00000015, 0x00000024, 0x00000000, 0x00040017, 0x00000026, 0x00000011, 0x00000003, 0x0003001d, - 0x00000027, 0x00000026, 0x0003001e, 0x00000028, 0x00000027, 0x00040020, 0x00000029, 0x0000000c, 0x00000028, 0x0004003b, 0x00000029, 0x0000002a, - 0x0000000c, 0x00040020, 0x0000002c, 0x0000000c, 0x00000026, 0x00040020, 0x0000002f, 0x00000002, 0x00000011, 0x00040020, 0x00000033, 0x0000000c, - 0x0000001f, 0x00040020, 0x00000038, 0x0000000c, 0x00000011, 0x0004002b, 0x00000006, 0x0000003b, 0x00000001, 0x0004002b, 0x00000006, 0x0000003e, - 0x00000002, 0x0004002b, 0x00000015, 0x00000042, 0x00000002, 0x0004002b, 0x00000006, 0x00000049, 0x00000100, 0x0006002c, 0x00000009, 0x0000004a, - 0x00000049, 0x0000003b, 0x0000003b, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x00000007, - 0x00000008, 0x00000007, 0x00050041, 0x0000000d, 0x0000000e, 0x0000000b, 0x0000000c, 0x0004003d, 0x00000006, 0x0000000f, 0x0000000e, 0x0003003e, - 0x00000008, 0x0000000f, 0x0004003d, 0x00000006, 0x00000010, 0x00000008, 0x00050041, 0x00000017, 0x00000018, 0x00000014, 0x00000016, 0x0004003d, - 0x00000006, 0x00000019, 0x00000018, 0x000500ae, 0x0000001a, 0x0000001b, 0x00000010, 0x00000019, 0x000300f7, 0x0000001d, 0x00000000, 0x000400fa, - 0x0000001b, 0x0000001c, 0x0000001d, 0x000200f8, 0x0000001c, 0x000100fd, 0x000200f8, 0x0000001d, 0x0004003d, 0x00000006, 0x00000025, 0x00000008, - 0x0004003d, 0x00000006, 0x0000002b, 0x00000008, 0x00060041, 0x0000002c, 0x0000002d, 0x0000002a, 0x00000024, 0x0000002b, 0x0004003d, 0x00000026, - 0x0000002e, 0x0000002d, 0x00050041, 0x0000002f, 0x00000030, 0x00000014, 0x00000024, 0x0004003d, 0x00000011, 0x00000031, 0x00000030, 0x0005008e, - 0x00000026, 0x00000032, 0x0000002e, 0x00000031, 0x00060041, 0x00000033, 0x00000034, 0x00000023, 0x00000024, 0x00000025, 0x0004003d, 0x0000001f, - 0x00000035, 0x00000034, 0x0008004f, 0x00000026, 0x00000036, 0x00000035, 0x00000035, 0x00000000, 0x00000001, 0x00000002, 0x00050081, 0x00000026, - 0x00000037, 0x00000036, 0x00000032, 0x00070041, 0x00000038, 0x00000039, 0x00000023, 0x00000024, 0x00000025, 0x0000000c, 0x00050051, 0x00000011, - 0x0000003a, 0x00000037, 0x00000000, 0x0003003e, 0x00000039, 0x0000003a, 0x00070041, 0x00000038, 0x0000003c, 0x00000023, 0x00000024, 0x00000025, - 0x0000003b, 0x00050051, 0x00000011, 0x0000003d, 0x00000037, 0x00000001, 0x0003003e, 0x0000003c, 0x0000003d, 0x00070041, 0x00000038, 0x0000003f, - 0x00000023, 0x00000024, 0x00000025, 0x0000003e, 0x00050051, 0x00000011, 0x00000040, 0x00000037, 0x00000002, 0x0003003e, 0x0000003f, 0x00000040, - 0x0004003d, 0x00000006, 0x00000041, 0x00000008, 0x00050041, 0x0000002f, 0x00000043, 0x00000014, 0x00000042, 0x0004003d, 0x00000011, 0x00000044, - 0x00000043, 0x00060041, 0x0000002c, 0x00000045, 0x0000002a, 0x00000024, 0x00000041, 0x0004003d, 0x00000026, 0x00000046, 0x00000045, 0x0005008e, - 0x00000026, 0x00000047, 0x00000046, 0x00000044, 0x00060041, 0x0000002c, 0x00000048, 0x0000002a, 0x00000024, 0x00000041, 0x0003003e, 0x00000048, - 0x00000047, 0x000100fd, 0x00010038 -}; - -struct runtime_array_test_metadata { - static constexpr std::string_view name = "runtime_array_test"; - static constexpr std::string_view entry_point = "main"; -}; - -inline constexpr std::array -runtime_array_test_bindings = {{ - mirage::render::vulkan::compute_pipeline::binding_info{ - .binding = 0, - .type = vk::DescriptorType::eStorageBuffer, - .count = 1, - .stage = vk::ShaderStageFlagBits::eCompute, - }, - mirage::render::vulkan::compute_pipeline::binding_info{ - .binding = 1, - .type = vk::DescriptorType::eStorageBuffer, - .count = 1, - .stage = vk::ShaderStageFlagBits::eCompute, - }, - mirage::render::vulkan::compute_pipeline::binding_info{ - .binding = 2, - .type = vk::DescriptorType::eUniformBuffer, - .count = 1, - .stage = vk::ShaderStageFlagBits::eCompute, - }, -}}; - -// ============ 工厂功能 ============ -inline auto create_runtime_array_test_pipeline(const mirage::render::vulkan::logical_device& device) - -> mirage::render::vulkan::expected -{ - return mirage::render::vulkan::compute_pipeline::create_from_spv( - device, - std::span(runtime_array_test_comp_spirv), - runtime_array_test_bindings - ); -} - -} // namespace mirage::render::shaders \ No newline at end of file diff --git a/tests/render_tree/CMakeLists.txt b/tests/render_tree/CMakeLists.txt new file mode 100644 index 0000000..4474064 --- /dev/null +++ b/tests/render_tree/CMakeLists.txt @@ -0,0 +1,10 @@ +project(test_render_tree) + +simple_executable() + +target_link_libraries(${PROJECT_NAME} PUBLIC + GTest::gtest + GTest::gtest_main + render_core + common +) diff --git a/tests/render_tree/test_render_tree.cpp b/tests/render_tree/test_render_tree.cpp new file mode 100644 index 0000000..bcaa4ee --- /dev/null +++ b/tests/render_tree/test_render_tree.cpp @@ -0,0 +1,172 @@ +#include +#include "pipeline/render_tree.h" +#include "render_command.h" +#include "types.h" + +using namespace mirage; +using namespace mirage::render; + +class RenderTreeTest : public ::testing::Test { +protected: + void SetUp() override { + // 测试前的设置 + } + + void TearDown() override { + // 测试后的清理 + } +}; + +// 测试几何节点的创建和基本操作 +TEST_F(RenderTreeTest, GeometryNodeCreation) { + auto geometry = std::make_unique(); + + EXPECT_EQ(geometry->get_type(), render_node_type::geometry); + EXPECT_TRUE(geometry->empty()); + + draw_batch batch; + geometry->add_batch(batch); + + EXPECT_FALSE(geometry->empty()); + EXPECT_EQ(geometry->get_batches().size(), 1); + + geometry->clear_batches(); + EXPECT_TRUE(geometry->empty()); +} + +// 测试遮罩节点的创建和子节点管理 +TEST_F(RenderTreeTest, MaskNodeCreation) { + mask_params params; + aabb2d_t bounds(vec2f_t(0, 0), vec2f_t(100, 100)); + auto mask = std::make_unique(params, bounds); + + EXPECT_EQ(mask->get_type(), render_node_type::mask); + EXPECT_EQ(mask->get_child_count(), 0); + + // 添加子节点 + auto geometry = std::make_unique(); + mask->add_child(std::move(geometry)); + + EXPECT_EQ(mask->get_child_count(), 1); + + // 移除子节点 + auto removed_child = mask->remove_child(0); + EXPECT_NE(removed_child, nullptr); + EXPECT_EQ(mask->get_child_count(), 0); +} + +// 测试后效节点的创建和子节点管理 +TEST_F(RenderTreeTest, PostEffectNodeCreation) { + blur_effect effect(vec2f_t(0, 0), vec2f_t(100, 100), 5.0f, 1.0f); + aabb2d_t bounds(vec2f_t(0, 0), vec2f_t(100, 100)); + auto post_effect = std::make_unique(effect, bounds); + + EXPECT_EQ(post_effect->get_type(), render_node_type::post_effect); + EXPECT_EQ(post_effect->get_child_count(), 0); + + // 添加子节点 + auto geometry = std::make_unique(); + post_effect->add_child(std::move(geometry)); + + EXPECT_EQ(post_effect->get_child_count(), 1); +} + +// 测试分组节点的创建和子节点管理 +TEST_F(RenderTreeTest, GroupNodeCreation) { + aabb2d_t bounds(vec2f_t(0, 0), vec2f_t(200, 200)); + auto group = std::make_unique(bounds, 1); + + EXPECT_EQ(group->get_type(), render_node_type::group); + EXPECT_EQ(group->get_z_order(), 1); + EXPECT_EQ(group->get_child_count(), 0); + + // 添加多个子节点 + auto geometry1 = std::make_unique(); + auto geometry2 = std::make_unique(); + + group->add_child(std::move(geometry1)); + group->add_child(std::move(geometry2)); + + EXPECT_EQ(group->get_child_count(), 2); + + // 清空所有子节点 + group->clear_children(); + EXPECT_EQ(group->get_child_count(), 0); +} + +// 测试渲染树的创建和管理 +TEST_F(RenderTreeTest, RenderTreeCreation) { + auto root = std::make_unique(); + render_tree tree(std::move(root)); + + EXPECT_FALSE(tree.empty()); + EXPECT_NE(tree.get_root(), nullptr); + EXPECT_EQ(tree.get_root()->get_type(), render_node_type::group); + + // 清空树 + tree.clear(); + EXPECT_TRUE(tree.empty()); + EXPECT_EQ(tree.get_root(), nullptr); +} + +// 测试复杂的嵌套结构 +TEST_F(RenderTreeTest, ComplexNestedStructure) { + // 创建复杂的嵌套结构:group -> post_effect -> mask -> geometry + auto geometry = std::make_unique(); + draw_batch batch; + geometry->add_batch(batch); + + mask_params mask_params; + auto mask = std::make_unique(mask_params, aabb2d_t()); + mask->add_child(std::move(geometry)); + + blur_effect blur_effect; + auto post_effect = std::make_unique(blur_effect, aabb2d_t()); + post_effect->add_child(std::move(mask)); + + auto group = std::make_unique(); + group->add_child(std::move(post_effect)); + + // 创建渲染树 + render_tree tree(std::move(group)); + + // 验证结构 + ASSERT_NE(tree.get_root(), nullptr); + EXPECT_EQ(tree.get_root()->get_type(), render_node_type::group); + + auto* root_group = static_cast(tree.get_root()); + EXPECT_EQ(root_group->get_child_count(), 1); + + auto* post_effect_ptr = static_cast(root_group->get_children()[0].get()); + EXPECT_EQ(post_effect_ptr->get_type(), render_node_type::post_effect); + EXPECT_EQ(post_effect_ptr->get_child_count(), 1); + + auto* mask_ptr = static_cast(post_effect_ptr->get_children()[0].get()); + EXPECT_EQ(mask_ptr->get_type(), render_node_type::mask); + EXPECT_EQ(mask_ptr->get_child_count(), 1); + + auto* geometry_ptr = static_cast(mask_ptr->get_children()[0].get()); + EXPECT_EQ(geometry_ptr->get_type(), render_node_type::geometry); + EXPECT_FALSE(geometry_ptr->empty()); +} + +// 测试边界框和 z_order 属性 +TEST_F(RenderTreeTest, BoundsAndZOrder) { + aabb2d_t bounds(vec2f_t(10, 20), vec2f_t(110, 120)); + int z_order = 5; + + auto geometry = std::make_unique(bounds, z_order); + + EXPECT_EQ(geometry->get_bounds().min(), vec2f_t(10, 20)); + EXPECT_EQ(geometry->get_bounds().max(), vec2f_t(110, 120)); + EXPECT_EQ(geometry->get_z_order(), 5); + + // 修改属性 + aabb2d_t new_bounds(vec2f_t(0, 0), vec2f_t(50, 50)); + geometry->set_bounds(new_bounds); + geometry->set_z_order(10); + + EXPECT_EQ(geometry->get_bounds().min(), vec2f_t(0, 0)); + EXPECT_EQ(geometry->get_bounds().max(), vec2f_t(50, 50)); + EXPECT_EQ(geometry->get_z_order(), 10); +} \ No newline at end of file diff --git a/tests/render_tree_builder/CMakeLists.txt b/tests/render_tree_builder/CMakeLists.txt new file mode 100644 index 0000000..b9d012a --- /dev/null +++ b/tests/render_tree_builder/CMakeLists.txt @@ -0,0 +1,10 @@ +project(test_render_tree_builder) + +simple_executable() + +target_link_libraries(${PROJECT_NAME} PUBLIC + GTest::gtest + GTest::gtest_main + render_core + common +) \ No newline at end of file diff --git a/tests/render_tree_builder/test_render_tree_builder.cpp b/tests/render_tree_builder/test_render_tree_builder.cpp new file mode 100644 index 0000000..6f5bd62 --- /dev/null +++ b/tests/render_tree_builder/test_render_tree_builder.cpp @@ -0,0 +1,113 @@ +#include +#include "pipeline/render_tree_builder.h" +#include "render_command.h" + +using namespace mirage; +using namespace mirage::render; + +TEST(RenderTreeBuilderTest, BuildEmpty) { + std::vector commands; + render_tree_builder builder; + auto tree = builder.build(commands); + + ASSERT_NE(tree.get_root(), nullptr); + EXPECT_EQ(tree.get_root()->get_type(), render_node_type::group); + EXPECT_EQ(static_cast(tree.get_root())->get_child_count(), 0); +} + +TEST(RenderTreeBuilderTest, BuildSimpleGeometry) { + std::vector commands; + render_command_builder builder; + builder.draw_rectangle({0, 0}, {100, 100}, color::red()); + + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + auto* root = static_cast(tree.get_root()); + ASSERT_EQ(root->get_child_count(), 1); + + auto* child = root->get_children()[0].get(); + EXPECT_EQ(child->get_type(), render_node_type::geometry); + + auto* geo_node = static_cast(child); + EXPECT_FALSE(geo_node->get_batches().empty()); +} + +TEST(RenderTreeBuilderTest, BuildMask) { + std::vector commands; + render_command_builder builder; + + mask_params params; + params.type = mask_type::rect; + + builder.begin_mask(params, aabb2d_t(vec2f_t(0.0f, 0.0f), vec2f_t(100.0f, 100.0f))); + builder.draw_rectangle({10, 10}, {50, 50}, color::blue()); + builder.end_mask(); + + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + auto* root = static_cast(tree.get_root()); + ASSERT_EQ(root->get_child_count(), 1); + + auto* child = root->get_children()[0].get(); + EXPECT_EQ(child->get_type(), render_node_type::mask); + + auto* mask_node_ptr = static_cast(child); + ASSERT_EQ(mask_node_ptr->get_child_count(), 1); + + auto* mask_child = mask_node_ptr->get_children()[0].get(); + EXPECT_EQ(mask_child->get_type(), render_node_type::geometry); +} + +TEST(RenderTreeBuilderTest, BuildPostEffect) { + std::vector commands; + render_command_builder builder; + + builder.draw_blur({0, 0}, {100, 100}, 10.0f); + + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + auto* root = static_cast(tree.get_root()); + ASSERT_EQ(root->get_child_count(), 1); + + auto* child = root->get_children()[0].get(); + EXPECT_EQ(child->get_type(), render_node_type::post_effect); +} + +TEST(RenderTreeBuilderTest, BuildNested) { + std::vector commands; + render_command_builder builder; + + // Root geometry + builder.draw_rectangle({0, 0}, {200, 200}, color::white()); + + // Mask + mask_params params; + builder.begin_mask(params, aabb2d_t(vec2f_t(0.0f, 0.0f), vec2f_t(100.0f, 100.0f))); + // Inside mask + builder.draw_rectangle({10, 10}, {50, 50}, color::red()); + + // Nested Post Effect + builder.draw_blur({10, 10}, {50, 50}); + builder.end_mask(); + + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + auto* root = static_cast(tree.get_root()); + // Should have 2 children: 1 geometry (root rect), 1 mask + // Note: The order depends on z-order. Default z-order is 0 for all. + // Stable sort preserves order. + ASSERT_EQ(root->get_child_count(), 2); + + EXPECT_EQ(root->get_children()[0]->get_type(), render_node_type::geometry); + EXPECT_EQ(root->get_children()[1]->get_type(), render_node_type::mask); + + auto* mask_node_ptr = static_cast(root->get_children()[1].get()); + // Mask should have 2 children: 1 geometry, 1 post effect + ASSERT_EQ(mask_node_ptr->get_child_count(), 2); + EXPECT_EQ(mask_node_ptr->get_children()[0]->get_type(), render_node_type::geometry); + EXPECT_EQ(mask_node_ptr->get_children()[1]->get_type(), render_node_type::post_effect); +} \ No newline at end of file diff --git a/tests/render_tree_integration/CMakeLists.txt b/tests/render_tree_integration/CMakeLists.txt new file mode 100644 index 0000000..a6e7c5b --- /dev/null +++ b/tests/render_tree_integration/CMakeLists.txt @@ -0,0 +1,11 @@ +project(test_render_tree_integration) + +simple_executable() + +target_link_libraries(${PROJECT_NAME} PUBLIC + GTest::gtest + GTest::gtest_main + render_core + common + widget_core +) \ No newline at end of file diff --git a/tests/render_tree_integration/test_render_tree_integration.cpp b/tests/render_tree_integration/test_render_tree_integration.cpp new file mode 100644 index 0000000..ce54d5a --- /dev/null +++ b/tests/render_tree_integration/test_render_tree_integration.cpp @@ -0,0 +1,854 @@ +#include +#include "pipeline/render_tree_builder.h" +#include "render_command.h" + +using namespace mirage; +using namespace mirage::render; + +// ============================================================================ +// 测试 1: 单层 mask 测试 +// ============================================================================ + +/// @brief 验证基本遮罩功能 +/// 创建包含 mask_begin、geometry、mask_end 的命令序列,验证构建的树结构正确 +TEST(RenderTreeIntegrationTest, SingleMask) { + render_command_builder builder; + + // 创建遮罩区域 + mask_params params; + params.type = mask_type::rect; + + builder.begin_mask(params, aabb2d_t(vec2f_t(0.0f, 0.0f), vec2f_t(100.0f, 100.0f))); + builder.draw_rectangle({10, 10}, {50, 50}, color::red()); + builder.draw_rectangle({20, 20}, {30, 30}, color::blue()); + builder.end_mask(); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 1); + + // 验证 mask 节点 + auto* mask_node_ptr = root->get_children()[0].get(); + EXPECT_EQ(mask_node_ptr->get_type(), render_node_type::mask); + + auto* mask = static_cast(mask_node_ptr); + ASSERT_EQ(mask->get_child_count(), 1); + + // 验证 mask 内的 geometry 节点 + auto* geo_node = mask->get_children()[0].get(); + EXPECT_EQ(geo_node->get_type(), render_node_type::geometry); + + auto* geo = static_cast(geo_node); + // 相邻的几何命令会被合并到一个批次中 + EXPECT_FALSE(geo->get_batches().empty()); +} + +// ============================================================================ +// 测试 2: 单层 post_effect 测试 +// ============================================================================ + +/// @brief 验证基本后效功能 +/// 创建包含 post_effect 和 geometry 的命令序列,验证构建的树结构正确 +TEST(RenderTreeIntegrationTest, SinglePostEffect) { + render_command_builder builder; + + // 创建后效区域 + builder.draw_blur({0, 0}, {100, 100}, 10.0f); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 1); + + // 验证 post_effect 节点 + auto* effect_node = root->get_children()[0].get(); + EXPECT_EQ(effect_node->get_type(), render_node_type::post_effect); + + auto* post_effect = static_cast(effect_node); + EXPECT_EQ(post_effect->get_child_count(), 0); // 后效节点没有子节点 +} + +// ============================================================================ +// 测试 3: mask 内 post_effect 测试 +// ============================================================================ + +/// @brief 验证遮罩内部有后效 +/// 创建嵌套结构:mask 包含 post_effect,验证树结构正确表达嵌套关系 +TEST(RenderTreeIntegrationTest, PostEffectInsideMask) { + render_command_builder builder; + + // 创建遮罩区域 + mask_params params; + params.type = mask_type::rounded_rect; + params.corner_radii = rect_corner_radius(10.0f); + + builder.begin_mask(params, aabb2d_t(vec2f_t(0.0f, 0.0f), vec2f_t(200.0f, 200.0f))); + // 在遮罩内绘制几何 + builder.draw_rectangle({10, 10}, {80, 80}, color::green()); + + // 在遮罩内应用后效 + builder.draw_blur({10, 10}, {80, 80}, 5.0f); + builder.end_mask(); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 1); + + // 验证 mask 节点 + auto* mask_node_ptr = root->get_children()[0].get(); + EXPECT_EQ(mask_node_ptr->get_type(), render_node_type::mask); + + auto* mask = static_cast(mask_node_ptr); + ASSERT_EQ(mask->get_child_count(), 2); // geometry + post_effect + + // 验证第一个子节点是 geometry + EXPECT_EQ(mask->get_children()[0]->get_type(), render_node_type::geometry); + + // 验证第二个子节点是 post_effect + EXPECT_EQ(mask->get_children()[1]->get_type(), render_node_type::post_effect); +} + +// ============================================================================ +// 测试 4: post_effect 内 mask 测试 +// ============================================================================ + +/// @brief 验证后效区域内有遮罩 +/// 创建嵌套结构:post_effect 包含 mask,验证树结构正确表达嵌套关系 +TEST(RenderTreeIntegrationTest, MaskInsidePostEffect) { + render_command_builder builder; + + // 创建后效区域 + builder.draw_vignette({0, 0}, {300, 300}, 0.6f, 0.4f); + + // 在后效区域内创建遮罩 + mask_params params; + params.type = mask_type::circle; + + builder.begin_mask(params, aabb2d_t(vec2f_t(50.0f, 50.0f), vec2f_t(150.0f, 150.0f))); + builder.draw_rectangle({60, 60}, {80, 80}, color::from_rgba(255, 255, 0)); // yellow + builder.end_mask(); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 2); // post_effect + mask + + // 验证第一个子节点是 post_effect + EXPECT_EQ(root->get_children()[0]->get_type(), render_node_type::post_effect); + + // 验证第二个子节点是 mask + auto* mask_node_ptr = root->get_children()[1].get(); + EXPECT_EQ(mask_node_ptr->get_type(), render_node_type::mask); + + auto* mask = static_cast(mask_node_ptr); + ASSERT_EQ(mask->get_child_count(), 1); + EXPECT_EQ(mask->get_children()[0]->get_type(), render_node_type::geometry); +} + +// ============================================================================ +// 测试 5: 多层嵌套测试 +// ============================================================================ + +/// @brief 验证 mask 和 post_effect 任意组合 +/// 创建复杂嵌套:mask -> post_effect -> mask,验证深度嵌套的正确性 +TEST(RenderTreeIntegrationTest, DeepNesting) { + render_command_builder builder; + + // 外层遮罩 + mask_params outer_mask; + outer_mask.type = mask_type::rect; + + builder.begin_mask(outer_mask, aabb2d_t(vec2f_t(0.0f, 0.0f), vec2f_t(400.0f, 400.0f))); + // 外层遮罩内的几何 + builder.draw_rectangle({10, 10}, {100, 100}, color::white()); + + // 外层遮罩内的后效 + builder.draw_blur({50, 50}, {200, 200}, 8.0f); + + // 内层遮罩(在外层遮罩内) + mask_params inner_mask; + inner_mask.type = mask_type::circle; + + builder.begin_mask(inner_mask, aabb2d_t(vec2f_t(100.0f, 100.0f), vec2f_t(200.0f, 200.0f))); + // 内层遮罩内的几何 + builder.draw_rectangle({120, 120}, {60, 60}, color::red()); + + // 内层遮罩内的后效 + builder.draw_chromatic_aberration({120, 120}, {60, 60}, 3.0f); + builder.end_mask(); + + // 外层遮罩内的更多几何 + builder.draw_rectangle({250, 250}, {100, 100}, color::blue()); + builder.end_mask(); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 1); + + // 验证外层 mask + auto* outer_mask_node = static_cast(root->get_children()[0].get()); + EXPECT_EQ(outer_mask_node->get_type(), render_node_type::mask); + ASSERT_EQ(outer_mask_node->get_child_count(), 4); // geo + post_effect + mask + geo + + // 验证外层 mask 的第一个子节点是 geometry + EXPECT_EQ(outer_mask_node->get_children()[0]->get_type(), render_node_type::geometry); + + // 验证外层 mask 的第二个子节点是 post_effect + EXPECT_EQ(outer_mask_node->get_children()[1]->get_type(), render_node_type::post_effect); + + // 验证外层 mask 的第三个子节点是内层 mask + auto* inner_mask_node = static_cast(outer_mask_node->get_children()[2].get()); + EXPECT_EQ(inner_mask_node->get_type(), render_node_type::mask); + ASSERT_EQ(inner_mask_node->get_child_count(), 2); // geo + post_effect + + // 验证内层 mask 的子节点 + EXPECT_EQ(inner_mask_node->get_children()[0]->get_type(), render_node_type::geometry); + EXPECT_EQ(inner_mask_node->get_children()[1]->get_type(), render_node_type::post_effect); + + // 验证外层 mask 的第四个子节点是 geometry + EXPECT_EQ(outer_mask_node->get_children()[3]->get_type(), render_node_type::geometry); +} + +// ============================================================================ +// 测试 6: 多个独立 mask 测试 +// ============================================================================ + +/// @brief 验证多个独立遮罩不会互相影响 +/// 创建多个独立的 mask 区域,验证它们在树中是独立的兄弟节点 +TEST(RenderTreeIntegrationTest, MultipleSiblingMasks) { + render_command_builder builder; + + // 第一个独立遮罩 + mask_params mask1; + mask1.type = mask_type::rect; + + builder.begin_mask(mask1, aabb2d_t(vec2f_t(0.0f, 0.0f), vec2f_t(100.0f, 100.0f))); + builder.draw_rectangle({10, 10}, {80, 80}, color::red()); + builder.end_mask(); + + // 第二个独立遮罩 + mask_params mask2; + mask2.type = mask_type::circle; + + builder.begin_mask(mask2, aabb2d_t(vec2f_t(150.0f, 0.0f), vec2f_t(250.0f, 100.0f))); + builder.draw_rectangle({160, 10}, {80, 80}, color::green()); + builder.end_mask(); + + // 第三个独立遮罩 + mask_params mask3; + mask3.type = mask_type::rounded_rect; + mask3.corner_radii = rect_corner_radius(15.0f); + + builder.begin_mask(mask3, aabb2d_t(vec2f_t(300.0f, 0.0f), vec2f_t(400.0f, 100.0f))); + builder.draw_rectangle({310, 10}, {80, 80}, color::blue()); + builder.end_mask(); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 3); // 三个独立的 mask + + // 验证所有子节点都是 mask + for (size_t i = 0; i < 3; ++i) { + auto* mask_node_ptr = root->get_children()[i].get(); + EXPECT_EQ(mask_node_ptr->get_type(), render_node_type::mask); + + auto* mask = static_cast(mask_node_ptr); + ASSERT_EQ(mask->get_child_count(), 1); + EXPECT_EQ(mask->get_children()[0]->get_type(), render_node_type::geometry); + } +} + +// ============================================================================ +// 测试 7: 复杂混合场景 +// ============================================================================ + +/// @brief 验证复杂的混合场景 +/// 包含多个独立的 mask、post_effect 以及它们的嵌套组合 +TEST(RenderTreeIntegrationTest, ComplexMixedScenario) { + render_command_builder builder; + + // 根级别的几何 + builder.draw_rectangle({0, 0}, {50, 50}, color::white()); + + // 第一个 mask 区域 + mask_params mask1; + mask1.type = mask_type::rect; + + builder.begin_mask(mask1, aabb2d_t(vec2f_t(100.0f, 0.0f), vec2f_t(200.0f, 100.0f))); + builder.draw_rectangle({110, 10}, {80, 80}, color::red()); + builder.draw_blur({110, 10}, {80, 80}, 5.0f); + builder.end_mask(); + + // 根级别的后效 + builder.draw_vignette({250, 0}, {100, 100}, 0.5f, 0.3f); + + // 第二个 mask 区域(包含嵌套后效和遮罩) + mask_params mask2; + mask2.type = mask_type::circle; + + builder.begin_mask(mask2, aabb2d_t(vec2f_t(400.0f, 0.0f), vec2f_t(600.0f, 200.0f))); + builder.draw_rectangle({420, 20}, {150, 150}, color::green()); + + // 嵌套后效 + builder.draw_chromatic_aberration({420, 20}, {150, 150}, 2.0f); + + // 嵌套遮罩 + mask_params nested_mask; + nested_mask.type = mask_type::rounded_rect; + nested_mask.corner_radii = rect_corner_radius(10.0f); + + builder.begin_mask(nested_mask, aabb2d_t(vec2f_t(450.0f, 50.0f), vec2f_t(550.0f, 150.0f))); + builder.draw_rectangle({460, 60}, {80, 80}, color::from_rgba(255, 255, 0)); // yellow + builder.end_mask(); + builder.end_mask(); + + // 根级别的更多几何 + builder.draw_rectangle({650, 0}, {50, 50}, color::blue()); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 5); // geo + mask + post_effect + mask + geo + + // 验证节点类型顺序 + EXPECT_EQ(root->get_children()[0]->get_type(), render_node_type::geometry); + EXPECT_EQ(root->get_children()[1]->get_type(), render_node_type::mask); + EXPECT_EQ(root->get_children()[2]->get_type(), render_node_type::post_effect); + EXPECT_EQ(root->get_children()[3]->get_type(), render_node_type::mask); + EXPECT_EQ(root->get_children()[4]->get_type(), render_node_type::geometry); + + // 验证第二个 mask 的内部结构 + auto* mask2_node = static_cast(root->get_children()[3].get()); + ASSERT_EQ(mask2_node->get_child_count(), 3); // geo + post_effect + nested_mask + + EXPECT_EQ(mask2_node->get_children()[0]->get_type(), render_node_type::geometry); + EXPECT_EQ(mask2_node->get_children()[1]->get_type(), render_node_type::post_effect); + EXPECT_EQ(mask2_node->get_children()[2]->get_type(), render_node_type::mask); + + // 验证嵌套遮罩 + auto* nested_mask_node = static_cast(mask2_node->get_children()[2].get()); + ASSERT_EQ(nested_mask_node->get_child_count(), 1); + EXPECT_EQ(nested_mask_node->get_children()[0]->get_type(), render_node_type::geometry); +} + +// ============================================================================ +// 测试 8: 空遮罩和空后效 +// ============================================================================ + +/// @brief 验证空遮罩和空后效的处理 +/// 创建不包含任何内容的 mask 和 post_effect,验证它们仍然被正确构建 +TEST(RenderTreeIntegrationTest, EmptyMaskAndPostEffect) { + render_command_builder builder; + + // 空遮罩 + mask_params empty_mask; + empty_mask.type = mask_type::rect; + + builder.begin_mask(empty_mask, aabb2d_t(vec2f_t(0.0f, 0.0f), vec2f_t(100.0f, 100.0f))); + builder.end_mask(); + + // 后效(没有子内容) + builder.draw_blur({150, 0}, {100, 100}, 10.0f); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 2); // empty_mask + post_effect + + // 验证空遮罩 + auto* mask_node_ptr = root->get_children()[0].get(); + EXPECT_EQ(mask_node_ptr->get_type(), render_node_type::mask); + + auto* mask = static_cast(mask_node_ptr); + EXPECT_EQ(mask->get_child_count(), 0); // 空遮罩没有子节点 + + // 验证后效 + auto* effect_node = root->get_children()[1].get(); + EXPECT_EQ(effect_node->get_type(), render_node_type::post_effect); + + auto* post_effect = static_cast(effect_node); + EXPECT_EQ(post_effect->get_child_count(), 0); // 后效没有子节点 +} + +// ============================================================================ +// 测试 9: 多种后效类型组合 +// ============================================================================ + +/// @brief 验证多种不同类型的后效可以正确组合 +TEST(RenderTreeIntegrationTest, MultiplePostEffectTypes) { + render_command_builder builder; + + // 各种不同类型的后效 + builder.draw_blur({0, 0}, {100, 100}, 10.0f); + builder.draw_vignette({110, 0}, {100, 100}, 0.5f, 0.4f); + builder.draw_chromatic_aberration({220, 0}, {100, 100}, 3.0f); + builder.draw_noise({330, 0}, {100, 100}, 0.2f, 1.5f); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 4); // 四种不同的后效 + + // 验证所有子节点都是 post_effect + for (size_t i = 0; i < 4; ++i) { + EXPECT_EQ(root->get_children()[i]->get_type(), render_node_type::post_effect); + } +} + +// ============================================================================ +// 测试 10: Z-Order 排序验证 +// ============================================================================ + +/// @brief 验证 z-order 对节点顺序的影响 +TEST(RenderTreeIntegrationTest, ZOrderSorting) { + render_command_builder builder; + + // 使用不同的 z-order 和 mask 来创建独立的节点 + mask_params params1; + params1.type = mask_type::rect; + + // z-order = 2 的 mask + builder.set_z_order(2); + builder.begin_mask(params1, aabb2d_t(vec2f_t(0.0f, 0.0f), vec2f_t(50.0f, 50.0f))); + builder.draw_rectangle({0, 0}, {50, 50}, color::red()); + builder.end_mask(); + + // z-order = 0 的 mask + mask_params params2; + params2.type = mask_type::circle; + builder.set_z_order(0); + builder.begin_mask(params2, aabb2d_t(vec2f_t(60.0f, 0.0f), vec2f_t(110.0f, 50.0f))); + builder.draw_rectangle({60, 0}, {50, 50}, color::green()); + builder.end_mask(); + + // z-order = 1 的 mask + mask_params params3; + params3.type = mask_type::rounded_rect; + params3.corner_radii = rect_corner_radius(5.0f); + builder.set_z_order(1); + builder.begin_mask(params3, aabb2d_t(vec2f_t(120.0f, 0.0f), vec2f_t(170.0f, 50.0f))); + builder.draw_rectangle({120, 0}, {50, 50}, color::blue()); + builder.end_mask(); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + ASSERT_EQ(root->get_child_count(), 3); // 三个独立的 mask + + // 验证节点类型和 z-order 顺序:mask(0), mask(1), mask(2) + EXPECT_EQ(root->get_children()[0]->get_type(), render_node_type::mask); + EXPECT_EQ(root->get_children()[0]->get_z_order(), 0); + + EXPECT_EQ(root->get_children()[1]->get_type(), render_node_type::mask); + EXPECT_EQ(root->get_children()[1]->get_z_order(), 1); + + EXPECT_EQ(root->get_children()[2]->get_type(), render_node_type::mask); + EXPECT_EQ(root->get_children()[2]->get_z_order(), 2); +} + +// ============================================================================ +// Widget 系统测试 - 需要包含 widget 头文件 +// ============================================================================ + +#include "button.h" +#include "imager.h" +#include "layout/overlay.h" +#include "layout/stack.h" +#include "post_effect.h" +#include "mask/rounded_rect_mask.h" +#include "mask/circle_mask.h" +#include "mask/mask_widget.h" +#include "layout/modifiers/modifiers.h" +#include "layout_state.h" + +// ============================================================================ +// 测试 11: Widget 代码生成 - 基本 button +// ============================================================================ + +/// @brief 验证 button widget 能否正确生成渲染命令 +TEST(RenderTreeIntegrationTest, WidgetCodeGeneration_Button) { + render_command_builder builder; + layout_state state; + + // 设置布局状态 + state.set_root(transform2d_t::Identity(), vec2f_t(800.0f, 600.0f)); + + // 创建 button widget + auto on_click = []() { /* do nothing */ }; + button btn{"Test Button", on_click}; + + // 布局和生成命令 + btn.arrange(state); + btn.build_render_commands(builder); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 - button 应该生成至少一个几何节点 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + EXPECT_GT(root->get_child_count(), 0); +} + +// ============================================================================ +// 测试 12: Widget 代码生成 - imager with rounded_rect_mask +// ============================================================================ + +/// @brief 验证 imager | mask(rounded_rect_mask) 的命令生成 +TEST(RenderTreeIntegrationTest, WidgetCodeGeneration_ImagerWithRoundedMask) { + render_command_builder builder; + layout_state state; + + state.set_root(transform2d_t::Identity(), vec2f_t(800.0f, 600.0f)); + + // 创建:imager | mask(rounded_rect_mask{}.corner_radius(30.f)) + auto widget = imager{} + .texture_id(1) // 假设纹理 ID 为 1 + .set_texture_size(vec2i_t(400, 300)) + | mask(rounded_rect_mask{}.corner_radius(30.f)); + + // 布局和生成命令 + widget.arrange(state); + widget.build_render_commands(builder); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + + // 应该有一个 mask 节点 + ASSERT_GT(root->get_child_count(), 0); + + // 查找 mask 节点 + bool found_mask = false; + for (size_t i = 0; i < root->get_child_count(); ++i) { + if (root->get_children()[i]->get_type() == render_node_type::mask) { + auto* mask_node_ptr = static_cast(root->get_children()[i].get()); + EXPECT_EQ(mask_node_ptr->get_mask_params().type, mask_type::rounded_rect); + EXPECT_FLOAT_EQ(mask_node_ptr->get_mask_params().corner_radii.top_left, 30.0f); + found_mask = true; + break; + } + } + EXPECT_TRUE(found_mask) << "应该找到 rounded_rect mask 节点"; +} + +// ============================================================================ +// 测试 13: Widget 代码生成 - overlay with post_effect +// ============================================================================ + +/// @brief 验证 overlay 中包含 imager 和 post_effect_widget 的命令生成 +TEST(RenderTreeIntegrationTest, WidgetCodeGeneration_OverlayWithPostEffect) { + render_command_builder builder; + layout_state state; + + state.set_root(transform2d_t::Identity(), vec2f_t(800.0f, 600.0f)); + + // 创建:overlay { imager, post_effect_widget } + overlay ovl{ + imager{} + .texture_id(1) + .set_texture_size(vec2i_t(400, 300)), + post_effect_widget{ + blur_effect{20.0f} + } + }; + + // 布局和生成命令 + ovl.arrange(state); + ovl.build_render_commands(builder); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + EXPECT_GT(root->get_child_count(), 0); + + // 应该包含 geometry 和 post_effect 节点 + bool found_geometry = false; + bool found_post_effect = false; + + for (size_t i = 0; i < root->get_child_count(); ++i) { + auto type = root->get_children()[i]->get_type(); + if (type == render_node_type::geometry) found_geometry = true; + if (type == render_node_type::post_effect) found_post_effect = true; + } + + EXPECT_TRUE(found_geometry) << "应该找到 geometry 节点"; + EXPECT_TRUE(found_post_effect) << "应该找到 post_effect 节点"; +} + +// ============================================================================ +// 测试 14: Widget 代码生成 - v_stack 布局 +// ============================================================================ + +/// @brief 验证 v_stack 中包含多个 widget 的命令生成 +TEST(RenderTreeIntegrationTest, WidgetCodeGeneration_VStack) { + render_command_builder builder; + layout_state state; + + state.set_root(transform2d_t::Identity(), vec2f_t(800.0f, 600.0f)); + + auto on_click = []() { /* do nothing */ }; + + // 创建 v_stack + v_stack stack{ + button{"Button 1", on_click}, + button{"Button 2", on_click}, + button{"Button 3", on_click} + }; + + // 布局和生成命令 + stack.arrange(state); + stack.build_render_commands(builder); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + + // v_stack 中的 3 个 button 应该生成多个节点 + EXPECT_GT(root->get_child_count(), 0); +} + +// ============================================================================ +// 测试 15: Widget 代码生成 - 完整场景(模拟 create_widget_test_commands) +// ============================================================================ + +/// @brief 验证完整的 widget 场景能否正确生成渲染命令 +/// 模拟 create_widget_test_commands 中的复杂布局,并验证结构和顺序 +TEST(RenderTreeIntegrationTest, WidgetCodeGeneration_CompleteScenario) { + render_command_builder builder; + layout_state state; + + state.set_root(transform2d_t::Identity(), vec2f_t(800.0f, 600.0f)); + + auto on_click = []() { /* do nothing */ }; + + // 创建完整的 v_stack 布局 + v_stack stack{ + // 1. Button + button{"Hello", on_click}, + + // 2. 第一个 overlay: imager | mask(rounded_rect) | align | padding + post_effect + overlay{ + imager{} + .texture_id(1) + .set_texture_size(vec2i_t(400, 300)) + | mask(rounded_rect_mask{}.corner_radius(30.f)) + | align(alignment::CENTER) + | padding(10.f), + post_effect_widget{ + blur_effect{20.f} + } + } | stretch(), + + // 3. 第二个 overlay: imager | align | padding + post_effect,外层 | mask(circle) + overlay{ + imager{} + .texture_id(1) + .set_texture_size(vec2i_t(400, 300)) + | align(alignment::CENTER) + | padding(10.f), + post_effect_widget{ + blur_effect{20.f} + } + } | stretch() | mask(circle_mask{}) + }; + + // 布局和生成命令 + stack.arrange(state); + stack.build_render_commands(builder); + + // 构建渲染树 + render_tree_builder tree_builder; + auto tree = tree_builder.build(builder.get_commands()); + + // 验证树结构 + auto* root = static_cast(tree.get_root()); + ASSERT_NE(root, nullptr); + + // ======================================================================== + // 验证顶层结构和顺序 + // ======================================================================== + // 预期结构: + // root (group) + // ├─ button 的 geometry 节点(可能多个) + // ├─ 第一个 overlay 的 mask(rounded_rect) 节点 + // └─ 第二个 overlay 的 mask(circle) 节点 + + ASSERT_GT(root->get_child_count(), 0) << "根节点应该有子节点"; + + // 查找并验证 mask 节点 + std::vector mask_nodes; + for (size_t i = 0; i < root->get_child_count(); ++i) { + if (root->get_children()[i]->get_type() == render_node_type::mask) { + mask_nodes.push_back(static_cast(root->get_children()[i].get())); + } + } + + // 应该有至少 2 个 mask 节点 + ASSERT_GE(mask_nodes.size(), 2) << "应该至少有 2 个 mask 节点(rounded_rect 和 circle)"; + + // ======================================================================== + // 验证第一个 mask 节点(rounded_rect_mask) + // ======================================================================== + // 查找 rounded_rect mask + mask_node* rounded_rect_mask_node = nullptr; + for (auto* mask : mask_nodes) { + if (mask->get_mask_params().type == mask_type::rounded_rect) { + rounded_rect_mask_node = mask; + break; + } + } + + ASSERT_NE(rounded_rect_mask_node, nullptr) << "应该找到 rounded_rect mask 节点"; + EXPECT_FLOAT_EQ(rounded_rect_mask_node->get_mask_params().corner_radii.top_left, 30.0f) + << "rounded_rect mask 的圆角半径应该是 30.0f"; + + // 验证 rounded_rect mask 内部结构 + // 预期:imager(geometry),post_effect 在 overlay 层级,不在 mask 内部 + ASSERT_GE(rounded_rect_mask_node->get_child_count(), 1) + << "rounded_rect mask 应该至少有 1 个子节点(geometry)"; + + // 验证子节点包含 geometry + bool found_geometry = false; + + for (size_t i = 0; i < rounded_rect_mask_node->get_child_count(); ++i) { + auto type = rounded_rect_mask_node->get_children()[i]->get_type(); + if (type == render_node_type::geometry) { + found_geometry = true; + } + } + + EXPECT_TRUE(found_geometry) << "rounded_rect mask 内应该有 geometry 节点"; + + // ======================================================================== + // 验证第二个 mask 节点(circle_mask) + // ======================================================================== + // 查找 circle mask + mask_node* circle_mask_node = nullptr; + for (auto* mask : mask_nodes) { + if (mask->get_mask_params().type == mask_type::circle) { + circle_mask_node = mask; + break; + } + } + + ASSERT_NE(circle_mask_node, nullptr) << "应该找到 circle mask 节点"; + + // 验证 circle mask 内部结构 + // 预期:imager(geometry),post_effect 在 overlay 层级,不在 mask 内部 + ASSERT_GE(circle_mask_node->get_child_count(), 1) + << "circle mask 应该至少有 1 个子节点(geometry)"; + + // 验证子节点包含 geometry + found_geometry = false; + + for (size_t i = 0; i < circle_mask_node->get_child_count(); ++i) { + auto type = circle_mask_node->get_children()[i]->get_type(); + if (type == render_node_type::geometry) { + found_geometry = true; + } + } + + EXPECT_TRUE(found_geometry) << "circle mask 内应该有 geometry 节点"; + + // ======================================================================== + // 验证整体节点统计 + // ======================================================================== + int total_mask_count = 0; + int total_geometry_count = 0; + int total_post_effect_count = 0; + + std::function count_nodes = [&](render_node* node) { + if (!node) return; + + switch (node->get_type()) { + case render_node_type::mask: + total_mask_count++; + { + auto* mask_node_ptr = static_cast(node); + for (auto& child : mask_node_ptr->get_children()) { + count_nodes(child.get()); + } + } + break; + case render_node_type::geometry: + total_geometry_count++; + break; + case render_node_type::post_effect: + total_post_effect_count++; + break; + case render_node_type::group: + { + auto* group = static_cast(node); + for (auto& child : group->get_children()) { + count_nodes(child.get()); + } + } + break; + } + }; + + count_nodes(root); + + // 验证总数 + EXPECT_GE(total_mask_count, 2) << "总共应该至少有 2 个 mask 节点"; + EXPECT_GE(total_geometry_count, 3) << "总共应该至少有 3 个 geometry 节点(button + 2个imager)"; + EXPECT_GE(total_post_effect_count, 2) << "总共应该至少有 2 个 post_effect 节点(2个blur)"; +} \ No newline at end of file diff --git a/tests/shaders/minimal/minimal.comp b/tests/shaders/minimal/minimal.comp deleted file mode 100644 index 16d7a2b..0000000 --- a/tests/shaders/minimal/minimal.comp +++ /dev/null @@ -1,8 +0,0 @@ -#version 450 - -layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; - -void main() { - // Minimal compute shader - does nothing - // Just a valid compute shader for testing -} \ No newline at end of file diff --git a/tests/shaders/minimal/minimal.frag b/tests/shaders/minimal/minimal.frag deleted file mode 100644 index 522e56d..0000000 --- a/tests/shaders/minimal/minimal.frag +++ /dev/null @@ -1,8 +0,0 @@ -#version 450 - -layout(location = 0) out vec4 outColor; - -void main() { - // Minimal fragment shader - output a solid red color - outColor = vec4(1.0, 0.0, 0.0, 1.0); -} \ No newline at end of file diff --git a/tests/shaders/minimal/minimal.vert b/tests/shaders/minimal/minimal.vert deleted file mode 100644 index afeaa95..0000000 --- a/tests/shaders/minimal/minimal.vert +++ /dev/null @@ -1,6 +0,0 @@ -#version 450 - -void main() { - // Minimal vertex shader - output a fixed position - gl_Position = vec4(0.0, 0.0, 0.0, 1.0); -} \ No newline at end of file diff --git a/tests/shaders/minimal/runtime_array_test.comp.glsl b/tests/shaders/minimal/runtime_array_test.comp.glsl deleted file mode 100644 index c90d298..0000000 --- a/tests/shaders/minimal/runtime_array_test.comp.glsl +++ /dev/null @@ -1,33 +0,0 @@ -#version 450 - -// 测试运行时数组的着色器 -layout(local_size_x = 256) in; - -// 包含运行时数组的缓冲区 -layout(binding = 0) buffer PositionBuffer { - vec4 positions[]; // 运行时数组 -}; - -layout(binding = 1) buffer VelocityBuffer { - vec3 velocities[]; // 运行时数组 -}; - -layout(binding = 2) uniform ControlParams { - float deltaTime; - uint particleCount; - float damping; -}; - -void main() { - uint idx = gl_GlobalInvocationID.x; - - if (idx >= particleCount) { - return; - } - - // 更新位置 - positions[idx].xyz += velocities[idx] * deltaTime; - - // 应用阻尼 - velocities[idx] *= damping; -} \ No newline at end of file diff --git a/tests/test_refactor_syntax/CMakeLists.txt b/tests/test_refactor_syntax/CMakeLists.txt new file mode 100644 index 0000000..451b51c --- /dev/null +++ b/tests/test_refactor_syntax/CMakeLists.txt @@ -0,0 +1,11 @@ +project(test_refactor_syntax) + +simple_executable() + +target_link_libraries(${PROJECT_NAME} PUBLIC + GTest::gtest + GTest::gtest_main + render_core + common + widget_core +) diff --git a/tests/test_refactor_syntax/test_refactor_syntax.cpp b/tests/test_refactor_syntax/test_refactor_syntax.cpp new file mode 100644 index 0000000..6effe5d --- /dev/null +++ b/tests/test_refactor_syntax/test_refactor_syntax.cpp @@ -0,0 +1,60 @@ +#include "layout/stack.h" +#include "button.h" +#include "layout/overlay.h" +#include "imager.h" +#include "post_effect.h" +#include "mask/rounded_rect_mask.h" +#include "mask/circle_mask.h" +#include "mask/mask_widget.h" +#include "layout/modifiers/modifiers.h" +#include + +using namespace mirage; +using namespace mirage::modifier; // For align, padding, stretch, mask + +void test_syntax() { + auto on_click = []() { std::cout << "Clicked!" << std::endl; }; + uint32_t texture_id_ = 1; + vec2i_t texture_size_{100, 100}; + + v_stack v{ + // Standard widget placement + button{ + "Hello", + on_click + }, + // Using | pipe operator + overlay{ + // Using pipe syntax for alignment and padding + imager{} + .texture_id(texture_id_) + .fit(image_fit::contain) + .set_texture_size(texture_size_) + | mask(rounded_rect_mask{}.corner_radius(30.f)) + | align(alignment::CENTER) + | padding(10.f), + // Standard post-effect widget + post_effect_widget{ + blur_effect{20.f} + }, + } | stretch(), + overlay{ + imager{} + .texture_id(texture_id_) + .fit(image_fit::contain) + .set_texture_size(texture_size_) + | align(alignment::CENTER) + | padding(10.f), + post_effect_widget{ + blur_effect{20.f} + } + } | stretch() | mask(circle_mask{}) + }; + + (void)v; // Suppress unused variable warning +} + +int main() { + test_syntax(); + return 0; +} \ No newline at end of file diff --git a/tests/unit/core/test_context.cpp b/tests/unit/core/test_context.cpp deleted file mode 100644 index 8d22768..0000000 --- a/tests/unit/core/test_context.cpp +++ /dev/null @@ -1,221 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "vulkan/context.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class RenderContextTest : public VulkanTestBase { -protected: - void SetUp() override { - VulkanTestBase::SetUp(); - } - - void TearDown() override { - VulkanTestBase::TearDown(); - } -}; - -// ============================================================================ -// P0 Priority Tests - Context Creation -// ============================================================================ - -TEST_F(RenderContextTest, CreateContextWithDefaultSettings) { - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - render_context::create_info info{}; - info.app_name = "Test App"; - info.enable_validation = false; // Disable for test performance - - auto result = render_context::create(info); - - ASSERT_TRUE(result.has_value()) - << "Failed to create context: " << vk::to_string(result.error()); - - auto& ctx = result.value(); - EXPECT_NE(ctx.get_instance(), nullptr) - << "Instance should not be null"; -} - -TEST_F(RenderContextTest, CreateContextWithValidationLayers) { - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - render_context::create_info info{}; - info.app_name = "Test App with Validation"; - info.enable_validation = true; - - auto result = render_context::create(info); - - // Validation layers may not be available in all environments - if (!result.has_value()) { - GTEST_SKIP() << "Validation layers not available: " - << vk::to_string(result.error()); - } - - auto& ctx = result.value(); - EXPECT_NE(ctx.get_instance(), nullptr); -} - -TEST_F(RenderContextTest, CreateContextWithCustomAppInfo) { - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - render_context::create_info info{}; - info.app_name = "Custom App Name"; - info.app_version = VK_MAKE_VERSION(2, 1, 0); - info.engine_name = "Custom Engine"; - info.engine_version = VK_MAKE_VERSION(3, 2, 1); - info.enable_validation = false; - - auto result = render_context::create(info); - - ASSERT_TRUE(result.has_value()) - << "Failed to create context with custom info: " - << vk::to_string(result.error()); - - EXPECT_NE(result.value().get_instance(), nullptr); -} - -// ============================================================================ -// P0 Priority Tests - Move Semantics -// ============================================================================ - -TEST_F(RenderContextTest, MoveConstructor) { - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - render_context::create_info info{}; - info.enable_validation = false; - - auto result = render_context::create(info); - ASSERT_TRUE(result.has_value()); - - auto ctx1 = std::move(result.value()); - EXPECT_NE(ctx1.get_instance(), nullptr); - - // Move construct ctx2 from ctx1 - render_context ctx2(std::move(ctx1)); - - EXPECT_NE(ctx2.get_instance(), nullptr) - << "Moved-to context should have valid instance"; - EXPECT_EQ(ctx1.get_instance(), nullptr) - << "Moved-from context should have null instance"; -} - -TEST_F(RenderContextTest, MoveAssignment) { - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - render_context::create_info info{}; - info.enable_validation = false; - - auto result1 = render_context::create(info); - auto result2 = render_context::create(info); - - ASSERT_TRUE(result1.has_value()); - ASSERT_TRUE(result2.has_value()); - - auto ctx1 = std::move(result1.value()); - auto ctx2 = std::move(result2.value()); - - auto instance1 = ctx1.get_instance(); - EXPECT_NE(instance1, nullptr); - - // Move assign ctx1 to ctx2 - ctx2 = std::move(ctx1); - - EXPECT_EQ(ctx2.get_instance(), instance1) - << "Move assignment should transfer instance"; - EXPECT_EQ(ctx1.get_instance(), nullptr) - << "Moved-from context should be null"; -} - -// ============================================================================ -// P0 Priority Tests - Instance Properties -// ============================================================================ - -TEST_F(RenderContextTest, GetInstanceReturnsValidHandle) { - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - render_context::create_info info{}; - info.enable_validation = false; - - auto result = render_context::create(info); - ASSERT_TRUE(result.has_value()); - - auto& ctx = result.value(); - auto instance = ctx.get_instance(); - - EXPECT_NE(instance, nullptr) << "Instance handle should be valid"; - - // Verify we can use the instance to enumerate devices - auto devices_result = instance.enumeratePhysicalDevices(); - EXPECT_EQ(devices_result.result, vk::Result::eSuccess) - << "Should be able to enumerate devices with valid instance"; -} - -// ============================================================================ -// P0 Priority Tests - Destruction -// ============================================================================ - -TEST_F(RenderContextTest, DestructionCleansUpResources) { - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - render_context::create_info info{}; - info.enable_validation = false; - - vk::Instance instance_handle; - - { - auto result = render_context::create(info); - ASSERT_TRUE(result.has_value()); - - auto ctx = std::move(result.value()); - instance_handle = ctx.get_instance(); - - EXPECT_NE(instance_handle, nullptr); - // Context destructor called here - } - - // After destruction, attempting to use the instance should fail - // Note: This is a conceptual test - in practice, accessing destroyed - // Vulkan objects leads to undefined behavior. This test just ensures - // the destructor runs without crashing. - - SUCCEED() << "Context destruction completed without crash"; -} - -// ============================================================================ -// P0 Priority Tests - Error Handling -// ============================================================================ - -TEST_F(RenderContextTest, CreateWithMissingExtensionsFails) { - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - render_context::create_info info{}; - info.enable_validation = false; - // Request a non-existent extension - info.required_extensions.push_back("VK_NONEXISTENT_EXTENSION_NAME"); - - auto result = render_context::create(info); - - EXPECT_FALSE(result.has_value()) - << "Should fail when required extension is not available"; - - if (!result.has_value()) { - EXPECT_NE(result.error(), vk::Result::eSuccess) - << "Error code should indicate failure"; - } -} \ No newline at end of file diff --git a/tests/unit/core/test_device_manager.cpp b/tests/unit/core/test_device_manager.cpp deleted file mode 100644 index 3ff2262..0000000 --- a/tests/unit/core/test_device_manager.cpp +++ /dev/null @@ -1,247 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "vulkan/device_manager.h" -#include "vulkan/context.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class DeviceManagerTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - } - - void TearDown() override { - VulkanGPUTest::TearDown(); - } -}; - -// ============================================================================ -// P0 Priority Tests - Device Enumeration -// ============================================================================ - -TEST_F(DeviceManagerTest, EnumerateDevicesReturnsNonEmpty) { - ASSERT_NE(device_mgr_, nullptr) << "Device manager should be initialized"; - - auto devices = device_mgr_->enumerate_devices(); - - EXPECT_FALSE(devices.empty()) - << "Should find at least one physical device"; -} - -TEST_F(DeviceManagerTest, EnumeratedDevicesAreValid) { - ASSERT_NE(device_mgr_, nullptr); - - auto devices = device_mgr_->enumerate_devices(); - ASSERT_FALSE(devices.empty()); - - for (const auto& device : devices) { - EXPECT_NE(device, nullptr) - << "Each enumerated device should have valid handle"; - - // Verify we can query device properties - auto props = device.getProperties(); - EXPECT_GT(props.apiVersion, 0u) - << "Device should have valid API version"; - } -} - -// ============================================================================ -// P0 Priority Tests - Primary Device Selection -// ============================================================================ - -TEST_F(DeviceManagerTest, SelectPrimaryDeviceSucceeds) { - ASSERT_NE(device_mgr_, nullptr); - - auto result = device_mgr_->select_primary_device(); - - ASSERT_TRUE(result.has_value()) - << "Should successfully select a primary device: " - << vk::to_string(result.error()); - - auto device = result.value(); - EXPECT_NE(device, nullptr) << "Selected device should be valid"; -} - -TEST_F(DeviceManagerTest, PrimaryDeviceSupportsGraphics) { - ASSERT_NE(device_mgr_, nullptr); - - auto result = device_mgr_->select_primary_device(); - ASSERT_TRUE(result.has_value()); - - auto device = result.value(); - auto queue_families = device.getQueueFamilyProperties(); - - bool has_graphics = false; - for (const auto& family : queue_families) { - if (family.queueFlags & vk::QueueFlagBits::eGraphics) { - has_graphics = true; - break; - } - } - - EXPECT_TRUE(has_graphics) - << "Primary device must support graphics operations"; -} - -TEST_F(DeviceManagerTest, PrimaryDeviceSupportsCompute) { - ASSERT_NE(device_mgr_, nullptr); - - auto result = device_mgr_->select_primary_device(); - ASSERT_TRUE(result.has_value()); - - auto device = result.value(); - auto queue_families = device.getQueueFamilyProperties(); - - bool has_compute = false; - for (const auto& family : queue_families) { - if (family.queueFlags & vk::QueueFlagBits::eCompute) { - has_compute = true; - break; - } - } - - EXPECT_TRUE(has_compute) - << "Primary device should support compute operations"; -} - -TEST_F(DeviceManagerTest, PrimaryDeviceHasMemory) { - ASSERT_NE(device_mgr_, nullptr); - - auto result = device_mgr_->select_primary_device(); - ASSERT_TRUE(result.has_value()); - - auto device = result.value(); - auto mem_props = device.getMemoryProperties(); - - EXPECT_GT(mem_props.memoryHeapCount, 0u) - << "Device should have at least one memory heap"; - EXPECT_GT(mem_props.memoryTypeCount, 0u) - << "Device should have at least one memory type"; -} - -// ============================================================================ -// P0 Priority Tests - Secondary Device Selection -// ============================================================================ - -TEST_F(DeviceManagerTest, SelectSecondaryDevicesWithPrimaryExcluded) { - ASSERT_NE(device_mgr_, nullptr); - - auto primary_result = device_mgr_->select_primary_device(); - ASSERT_TRUE(primary_result.has_value()); - auto primary = primary_result.value(); - - auto secondary_devices = device_mgr_->select_secondary_devices(primary); - - // Secondary devices list may be empty if only one GPU exists - for (const auto& device : secondary_devices) { - EXPECT_NE(device, primary) - << "Secondary devices should not include primary device"; - } -} - -TEST_F(DeviceManagerTest, SecondaryDevicesSupportCompute) { - ASSERT_NE(device_mgr_, nullptr); - - auto primary_result = device_mgr_->select_primary_device(); - ASSERT_TRUE(primary_result.has_value()); - - auto secondary_devices = device_mgr_->select_secondary_devices(primary_result.value()); - - for (const auto& device : secondary_devices) { - auto queue_families = device.getQueueFamilyProperties(); - - bool has_compute = false; - for (const auto& family : queue_families) { - if (family.queueFlags & vk::QueueFlagBits::eCompute) { - has_compute = true; - break; - } - } - - EXPECT_TRUE(has_compute) - << "Each secondary device must support compute operations"; - } -} - -// ============================================================================ -// P0 Priority Tests - Device Properties -// ============================================================================ - -TEST_F(DeviceManagerTest, DeviceHasValidProperties) { - ASSERT_NE(device_mgr_, nullptr); - - auto result = device_mgr_->select_primary_device(); - ASSERT_TRUE(result.has_value()); - - auto device = result.value(); - auto props = device.getProperties(); - - EXPECT_GT(props.apiVersion, 0u) << "API version should be non-zero"; - EXPECT_GT(props.driverVersion, 0u) << "Driver version should be non-zero"; - EXPECT_NE(props.deviceType, vk::PhysicalDeviceType::eOther) - << "Device type should be specific"; - EXPECT_GT(std::strlen(props.deviceName.data()), 0u) - << "Device should have a name"; -} - -TEST_F(DeviceManagerTest, DeviceHasFeatures) { - ASSERT_NE(device_mgr_, nullptr); - - auto result = device_mgr_->select_primary_device(); - ASSERT_TRUE(result.has_value()); - - auto device = result.value(); - auto features = device.getFeatures(); - - // At least some basic features should be supported - // Not checking specific features as they vary by GPU - SUCCEED() << "Successfully queried device features"; -} - -// ============================================================================ -// P0 Priority Tests - Multiple Instances -// ============================================================================ - -TEST_F(DeviceManagerTest, MultipleDeviceManagerInstances) { - ASSERT_NE(context_, nullptr); - - // Create two device managers from same instance - device_manager mgr1(context_->get_instance()); - device_manager mgr2(context_->get_instance()); - - auto devices1 = mgr1.enumerate_devices(); - auto devices2 = mgr2.enumerate_devices(); - - EXPECT_EQ(devices1.size(), devices2.size()) - << "Both managers should enumerate same number of devices"; -} - -TEST_F(DeviceManagerTest, DeviceManagerFromDifferentContext) { - // Create a new context - render_context::create_info info{}; - info.enable_validation = false; - auto ctx_result = render_context::create(info); - ASSERT_TRUE(ctx_result.has_value()); - - auto new_context = std::move(ctx_result.value()); - device_manager new_mgr(new_context.get_instance()); - - auto devices = new_mgr.enumerate_devices(); - EXPECT_FALSE(devices.empty()) - << "New device manager should also find devices"; -} - -// ============================================================================ -// P0 Priority Tests - Surface Support (if applicable) -// ============================================================================ - -TEST_F(DeviceManagerTest, SelectPrimaryDeviceWithNullSurface) { - ASSERT_NE(device_mgr_, nullptr); - - // Passing nullptr for surface should still work - auto result = device_mgr_->select_primary_device(nullptr); - - ASSERT_TRUE(result.has_value()) - << "Should select device even without surface"; -} \ No newline at end of file diff --git a/tests/unit/core/test_logical_device.cpp b/tests/unit/core/test_logical_device.cpp deleted file mode 100644 index 7e73d6b..0000000 --- a/tests/unit/core/test_logical_device.cpp +++ /dev/null @@ -1,308 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "vulkan/logical_device.h" -#include "vulkan/device_manager.h" -#include "vulkan/context.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class LogicalDeviceTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - } - - void TearDown() override { - VulkanGPUTest::TearDown(); - } -}; - -// ============================================================================ -// P0 Priority Tests - Device Creation -// ============================================================================ - -TEST_F(LogicalDeviceTest, CreateLogicalDeviceFromPhysicalDevice) { - ASSERT_NE(physical_device_, nullptr) << "Physical device should be valid"; - - auto result = logical_device::create(physical_device_); - - ASSERT_TRUE(result.has_value()) - << "Failed to create logical device: " << vk::to_string(result.error()); - - auto dev = std::move(result.value()); - EXPECT_NE(dev.get_handle(), nullptr) << "Device handle should be valid"; -} - -TEST_F(LogicalDeviceTest, CreateLogicalDeviceWithNullSurface) { - ASSERT_NE(physical_device_, nullptr); - - // Creating without surface (nullptr) should still work - auto result = logical_device::create(physical_device_, nullptr); - - ASSERT_TRUE(result.has_value()) - << "Should create device without surface"; - - EXPECT_NE(result.value().get_handle(), nullptr); -} - -TEST_F(LogicalDeviceTest, CreatedDeviceHasQueues) { - ASSERT_NE(device_, nullptr) << "Test fixture device should exist"; - - EXPECT_NE(device_->get_graphics_queue(), nullptr) - << "Graphics queue should be valid"; - EXPECT_NE(device_->get_compute_queue(), nullptr) - << "Compute queue should be valid"; - EXPECT_NE(device_->get_present_queue(), nullptr) - << "Present queue should be valid"; -} - -TEST_F(LogicalDeviceTest, CreatedDeviceHasCommandPool) { - ASSERT_NE(device_, nullptr); - - EXPECT_NE(device_->get_command_pool(), nullptr) - << "Command pool should be valid"; -} - -// ============================================================================ -// P0 Priority Tests - Queue Family Indices -// ============================================================================ - -TEST_F(LogicalDeviceTest, QueueFamilyIndicesStructIsComplete) { - logical_device::queue_family_indices indices; - indices.graphics_family = 0; - indices.compute_family = 0; - indices.present_family = 0; - - EXPECT_TRUE(indices.is_complete()) - << "Indices should be complete when all families set"; -} - -TEST_F(LogicalDeviceTest, QueueFamilyIndicesStructIsIncomplete) { - logical_device::queue_family_indices indices; - indices.graphics_family = 0; - // Leave compute and present unset - - EXPECT_FALSE(indices.is_complete()) - << "Indices should be incomplete when families missing"; -} - -TEST_F(LogicalDeviceTest, DeviceHasValidGraphicsFamilyIndex) { - ASSERT_NE(device_, nullptr); - - auto graphics_family = device_->get_graphics_family_index(); - - // Verify this is a valid queue family by checking it exists - auto queue_families = physical_device_.getQueueFamilyProperties(); - EXPECT_LT(graphics_family, queue_families.size()) - << "Graphics family index should be valid"; - - // Verify this family supports graphics - EXPECT_TRUE(queue_families[graphics_family].queueFlags & vk::QueueFlagBits::eGraphics) - << "Graphics family should support graphics operations"; -} - -TEST_F(LogicalDeviceTest, DeviceHasValidComputeFamilyIndex) { - ASSERT_NE(device_, nullptr); - - auto compute_family = device_->get_compute_family_index(); - - // Verify this is a valid queue family by checking it exists - auto queue_families = physical_device_.getQueueFamilyProperties(); - EXPECT_LT(compute_family, queue_families.size()) - << "Compute family index should be valid"; - - // Verify this family supports compute - EXPECT_TRUE(queue_families[compute_family].queueFlags & vk::QueueFlagBits::eCompute) - << "Compute family should support compute operations"; -} - -// ============================================================================ -// P0 Priority Tests - Move Semantics -// ============================================================================ - -TEST_F(LogicalDeviceTest, MoveConstructor) { - ASSERT_NE(physical_device_, nullptr); - - auto result = logical_device::create(physical_device_); - ASSERT_TRUE(result.has_value()); - - auto dev1 = std::move(result.value()); - auto handle1 = dev1.get_handle(); - EXPECT_NE(handle1, nullptr); - - // Move construct dev2 from dev1 - logical_device dev2(std::move(dev1)); - - EXPECT_EQ(dev2.get_handle(), handle1) - << "Moved-to device should have same handle"; - EXPECT_EQ(dev1.get_handle(), nullptr) - << "Moved-from device should have null handle"; -} - -TEST_F(LogicalDeviceTest, MoveAssignment) { - ASSERT_NE(physical_device_, nullptr); - - auto result1 = logical_device::create(physical_device_); - auto result2 = logical_device::create(physical_device_); - - ASSERT_TRUE(result1.has_value()); - ASSERT_TRUE(result2.has_value()); - - auto dev1 = std::move(result1.value()); - auto dev2 = std::move(result2.value()); - - auto handle1 = dev1.get_handle(); - EXPECT_NE(handle1, nullptr); - - // Move assign dev1 to dev2 - dev2 = std::move(dev1); - - EXPECT_EQ(dev2.get_handle(), handle1) - << "Move assignment should transfer handle"; - EXPECT_EQ(dev1.get_handle(), nullptr) - << "Moved-from device should be null"; -} - -// ============================================================================ -// P0 Priority Tests - Queue Operations -// ============================================================================ - -TEST_F(LogicalDeviceTest, GraphicsQueueCanBeRetrieved) { - ASSERT_NE(device_, nullptr); - - auto queue = device_->get_graphics_queue(); - EXPECT_NE(queue, nullptr) << "Graphics queue should be retrievable"; - - // Verify queue can be used to query properties - // Note: vk::Queue doesn't have many query methods, but we can at least - // verify it's a valid handle by attempting a wait - auto result = queue.waitIdle(); - EXPECT_EQ(result, vk::Result::eSuccess) - << "Should be able to wait on graphics queue"; -} - -TEST_F(LogicalDeviceTest, ComputeQueueCanBeRetrieved) { - ASSERT_NE(device_, nullptr); - - auto queue = device_->get_compute_queue(); - EXPECT_NE(queue, nullptr) << "Compute queue should be retrievable"; - - auto result = queue.waitIdle(); - EXPECT_EQ(result, vk::Result::eSuccess) - << "Should be able to wait on compute queue"; -} - -TEST_F(LogicalDeviceTest, PresentQueueCanBeRetrieved) { - ASSERT_NE(device_, nullptr); - - auto queue = device_->get_present_queue(); - EXPECT_NE(queue, nullptr) << "Present queue should be retrievable"; - - auto result = queue.waitIdle(); - EXPECT_EQ(result, vk::Result::eSuccess) - << "Should be able to wait on present queue"; -} - -// ============================================================================ -// P0 Priority Tests - Command Pool -// ============================================================================ - -TEST_F(LogicalDeviceTest, CommandPoolCanAllocateBuffers) { - ASSERT_NE(device_, nullptr); - - auto pool = device_->get_command_pool(); - auto dev_handle = device_->get_handle(); - - ASSERT_NE(pool, nullptr); - ASSERT_NE(dev_handle, nullptr); - - // Try to allocate a command buffer from the pool - vk::CommandBufferAllocateInfo alloc_info{}; - alloc_info.commandPool = pool; - alloc_info.level = vk::CommandBufferLevel::ePrimary; - alloc_info.commandBufferCount = 1; - - auto result = dev_handle.allocateCommandBuffers(alloc_info); - EXPECT_EQ(result.result, vk::Result::eSuccess) - << "Should be able to allocate command buffers from pool"; - - if (result.result == vk::Result::eSuccess && !result.value.empty()) { - // Clean up - dev_handle.freeCommandBuffers(pool, result.value); - } -} - -// ============================================================================ -// P0 Priority Tests - Device Destruction -// ============================================================================ - -TEST_F(LogicalDeviceTest, DestructionCleansUpResources) { - ASSERT_NE(physical_device_, nullptr); - - vk::Device device_handle; - - { - auto result = logical_device::create(physical_device_); - ASSERT_TRUE(result.has_value()); - - auto dev = std::move(result.value()); - device_handle = dev.get_handle(); - - EXPECT_NE(device_handle, nullptr); - // Device destructor called here - } - - // After destruction, the device handle should be cleaned up - // This test ensures destruction happens without crashes - SUCCEED() << "Device destruction completed without crash"; -} - -// ============================================================================ -// P0 Priority Tests - Multiple Devices -// ============================================================================ - -TEST_F(LogicalDeviceTest, CanCreateMultipleLogicalDevices) { - ASSERT_NE(physical_device_, nullptr); - - auto result1 = logical_device::create(physical_device_); - auto result2 = logical_device::create(physical_device_); - - ASSERT_TRUE(result1.has_value()) << "First device creation failed"; - ASSERT_TRUE(result2.has_value()) << "Second device creation failed"; - - auto dev1 = std::move(result1.value()); - auto dev2 = std::move(result2.value()); - - EXPECT_NE(dev1.get_handle(), dev2.get_handle()) - << "Multiple logical devices should have different handles"; -} - -TEST_F(LogicalDeviceTest, MultipleDevicesCanCoexist) { - ASSERT_NE(physical_device_, nullptr); - - auto result1 = logical_device::create(physical_device_); - auto result2 = logical_device::create(physical_device_); - - ASSERT_TRUE(result1.has_value()); - ASSERT_TRUE(result2.has_value()); - - auto dev1 = std::move(result1.value()); - auto dev2 = std::move(result2.value()); - - // Both devices should be functional - EXPECT_EQ(dev1.get_graphics_queue().waitIdle(), vk::Result::eSuccess); - EXPECT_EQ(dev2.get_graphics_queue().waitIdle(), vk::Result::eSuccess); -} - -// ============================================================================ -// P0 Priority Tests - Error Conditions -// ============================================================================ - -TEST_F(LogicalDeviceTest, CreateFromInvalidPhysicalDeviceFails) { - vk::PhysicalDevice invalid_device = nullptr; - - auto result = logical_device::create(invalid_device); - - EXPECT_FALSE(result.has_value()) - << "Should fail to create device from null physical device"; -} \ No newline at end of file diff --git a/tests/unit/pipeline/test_compute_pipeline.cpp b/tests/unit/pipeline/test_compute_pipeline.cpp deleted file mode 100644 index 7b6efb3..0000000 --- a/tests/unit/pipeline/test_compute_pipeline.cpp +++ /dev/null @@ -1,333 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "utils/shader_loader.h" -#include "vulkan/pipeline/compute_pipeline.h" -#include "vulkan/resource_manager.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class ComputePipelineTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - // Create a descriptor pool for testing - CreateDescriptorPool(); - - // Create resource manager for buffer operations - resource_mgr_ = std::make_unique(*device_, physical_device_, context_->get_instance()); - } - - void TearDown() override { - // Clean up any test buffers - for (auto& buffer : test_buffers_) { - if (resource_mgr_) { - resource_mgr_->destroy_buffer(buffer); - } - } - test_buffers_.clear(); - - resource_mgr_.reset(); - - if (device_ && descriptor_pool_) { - device_->get_handle().destroyDescriptorPool(descriptor_pool_); - descriptor_pool_ = nullptr; - } - - VulkanGPUTest::TearDown(); - } - - void CreateDescriptorPool() { - std::array pool_sizes = {{ - {vk::DescriptorType::eStorageBuffer, 10} - }}; - - vk::DescriptorPoolCreateInfo pool_info{}; - pool_info.maxSets = 10; - pool_info.poolSizeCount = static_cast(pool_sizes.size()); - pool_info.pPoolSizes = pool_sizes.data(); - - auto result = device_->get_handle().createDescriptorPool(pool_info); - ASSERT_EQ(result.result, vk::Result::eSuccess) - << "Failed to create descriptor pool"; - - descriptor_pool_ = result.value; - } - - resource_manager::buffer_resource CreateTestBuffer(vk::DeviceSize size) { - auto result = resource_mgr_->create_buffer( - size, - vk::BufferUsageFlagBits::eStorageBuffer, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - EXPECT_TRUE(result.has_value()) << "Failed to create test buffer"; - - auto buffer = result.value(); - test_buffers_.push_back(buffer); - return buffer; - } - - vk::DescriptorPool descriptor_pool_; - std::unique_ptr resource_mgr_; - std::vector test_buffers_; -}; - -// ============================================================================ -// P0 Priority Tests - Pipeline Creation -// ============================================================================ - -TEST_F(ComputePipelineTest, CreateFromSPV_Success) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - // Create pipeline with no bindings (minimal shader) - auto result = compute_pipeline::create_from_spv(*device_, spirv); - - ASSERT_TRUE(result.has_value()) - << "Failed to create compute pipeline from SPIR-V: " - << vk::to_string(result.error()); - - auto& pipeline = result.value(); - EXPECT_NE(pipeline.get_pipeline(), nullptr) - << "Pipeline handle should be valid"; -} - -TEST_F(ComputePipelineTest, CreateFromSPV_ValidBinding) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - // Create pipeline with a storage buffer binding - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - - ASSERT_TRUE(result.has_value()) - << "Failed to create compute pipeline with binding: " - << vk::to_string(result.error()); - - auto& pipeline = result.value(); - EXPECT_NE(pipeline.get_pipeline(), nullptr); - EXPECT_NE(pipeline.get_descriptor_set_layout(), nullptr) - << "Descriptor set layout should be valid"; -} - -TEST_F(ComputePipelineTest, GetPipeline_Valid) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - auto result = compute_pipeline::create_from_spv(*device_, spirv); - ASSERT_TRUE(result.has_value()); - - auto& pipeline = result.value(); - auto handle = pipeline.get_pipeline(); - - EXPECT_NE(handle, nullptr) - << "get_pipeline should return valid handle"; -} - -TEST_F(ComputePipelineTest, GetLayout_Valid) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - auto result = compute_pipeline::create_from_spv(*device_, spirv); - ASSERT_TRUE(result.has_value()); - - auto& pipeline = result.value(); - auto layout = pipeline.get_layout(); - - EXPECT_NE(layout, nullptr) - << "get_layout should return valid handle"; -} - -TEST_F(ComputePipelineTest, GetDescriptorSetLayout_Valid) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(result.has_value()); - - auto& pipeline = result.value(); - auto ds_layout = pipeline.get_descriptor_set_layout(); - - EXPECT_NE(ds_layout, nullptr) - << "get_descriptor_set_layout should return valid handle"; -} - -// ============================================================================ -// P0 Priority Tests - Descriptor Set Operations -// ============================================================================ - -TEST_F(ComputePipelineTest, AllocateDescriptorSet_Success) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(result.has_value()); - - auto& pipeline = result.value(); - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - - ASSERT_TRUE(ds_result.has_value()) - << "Failed to allocate descriptor set: " - << vk::to_string(ds_result.error()); - - EXPECT_NE(ds_result.value(), nullptr) - << "Descriptor set should be valid"; -} - -TEST_F(ComputePipelineTest, BindBuffer_StorageBuffer) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(result.has_value()); - - auto& pipeline = result.value(); - - // Allocate descriptor set - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - ASSERT_TRUE(ds_result.has_value()); - auto descriptor_set = ds_result.value(); - - // Create a test buffer - auto buffer = CreateTestBuffer(1024); - ASSERT_NE(buffer.buffer, nullptr); - - // Bind buffer to descriptor set - this should not throw - EXPECT_NO_THROW({ - pipeline.bind_buffer(descriptor_set, 0, buffer, vk::DescriptorType::eStorageBuffer); - }) << "bind_buffer should not throw for valid binding"; -} - -// ============================================================================ -// P0 Priority Tests - Move Semantics -// ============================================================================ - -TEST_F(ComputePipelineTest, MoveSemantics) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - auto result = compute_pipeline::create_from_spv(*device_, spirv); - ASSERT_TRUE(result.has_value()); - - // Test move constructor - { - auto pipeline1 = std::move(result.value()); - auto original_pipeline = pipeline1.get_pipeline(); - auto original_layout = pipeline1.get_layout(); - - compute_pipeline pipeline2(std::move(pipeline1)); - - EXPECT_EQ(pipeline2.get_pipeline(), original_pipeline) - << "Moved-to pipeline should have original handle"; - EXPECT_EQ(pipeline2.get_layout(), original_layout) - << "Moved-to pipeline should have original layout"; - EXPECT_EQ(pipeline1.get_pipeline(), nullptr) - << "Moved-from pipeline should be null"; - } - - // Test move assignment - { - auto result1 = compute_pipeline::create_from_spv(*device_, spirv); - auto result2 = compute_pipeline::create_from_spv(*device_, spirv); - ASSERT_TRUE(result1.has_value()); - ASSERT_TRUE(result2.has_value()); - - auto pipeline1 = std::move(result1.value()); - auto pipeline2 = std::move(result2.value()); - - auto original_pipeline = pipeline1.get_pipeline(); - - pipeline2 = std::move(pipeline1); - - EXPECT_EQ(pipeline2.get_pipeline(), original_pipeline) - << "Move assignment should transfer handle"; - EXPECT_EQ(pipeline1.get_pipeline(), nullptr) - << "Moved-from pipeline should be null"; - } -} - -// ============================================================================ -// P0 Priority Tests - Multiple Bindings -// ============================================================================ - -TEST_F(ComputePipelineTest, MultipleBindings) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - // Create pipeline with multiple storage buffer bindings - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute}, - {1, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute}, - {2, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - - ASSERT_TRUE(result.has_value()) - << "Failed to create pipeline with multiple bindings: " - << vk::to_string(result.error()); - - auto& pipeline = result.value(); - - // Allocate descriptor set - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - ASSERT_TRUE(ds_result.has_value()); - auto descriptor_set = ds_result.value(); - - // Create and bind multiple buffers - auto buffer0 = CreateTestBuffer(256); - auto buffer1 = CreateTestBuffer(512); - auto buffer2 = CreateTestBuffer(1024); - - EXPECT_NO_THROW({ - pipeline.bind_buffer(descriptor_set, 0, buffer0, vk::DescriptorType::eStorageBuffer); - pipeline.bind_buffer(descriptor_set, 1, buffer1, vk::DescriptorType::eStorageBuffer); - pipeline.bind_buffer(descriptor_set, 2, buffer2, vk::DescriptorType::eStorageBuffer); - }) << "Should bind multiple buffers without error"; -} - -// ============================================================================ -// P0 Priority Tests - Resource Cleanup -// ============================================================================ - -TEST_F(ComputePipelineTest, ResourceCleanup) { - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - { - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(result.has_value()); - - auto pipeline = std::move(result.value()); - - // Store handles before destruction - auto pipeline_handle = pipeline.get_pipeline(); - auto layout_handle = pipeline.get_layout(); - auto ds_layout_handle = pipeline.get_descriptor_set_layout(); - - EXPECT_NE(pipeline_handle, nullptr); - EXPECT_NE(layout_handle, nullptr); - EXPECT_NE(ds_layout_handle, nullptr); - - // Pipeline destructor will be called here - } - - // After destruction, resources should be cleaned up - // This is a conceptual test - actual verification would require - // Vulkan validation layers - SUCCEED() << "Compute pipeline destruction completed without crash"; -} \ No newline at end of file diff --git a/tests/unit/pipeline/test_graphics_pipeline.cpp b/tests/unit/pipeline/test_graphics_pipeline.cpp deleted file mode 100644 index 845cb67..0000000 --- a/tests/unit/pipeline/test_graphics_pipeline.cpp +++ /dev/null @@ -1,360 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "utils/shader_loader.h" -#include "vulkan/pipeline/graphics_pipeline.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class GraphicsPipelineTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - } - - void TearDown() override { - VulkanGPUTest::TearDown(); - } -}; - -// ============================================================================ -// P0 Priority Tests - Shader Module Loading -// ============================================================================ - -TEST_F(GraphicsPipelineTest, LoadShaderModule_FromMemory) { - const auto& spirv = shader_loader::get_minimal_vertex_shader(); - - auto result = graphics_pipeline::load_shader_module_from_memory(*device_, spirv); - - ASSERT_TRUE(result.has_value()) - << "Failed to load shader from memory: " << vk::to_string(result.error()); - - auto shader_module = result.value(); - EXPECT_NE(shader_module, nullptr) - << "Shader module handle should be valid"; - - // Clean up - device_->get_handle().destroyShaderModule(shader_module); -} - -TEST_F(GraphicsPipelineTest, LoadShaderModule_ValidSPIRV) { - // Test loading vertex shader - { - const auto& spirv = shader_loader::get_minimal_vertex_shader(); - auto result = graphics_pipeline::load_shader_module_from_memory(*device_, spirv); - - ASSERT_TRUE(result.has_value()) - << "Failed to load valid vertex shader SPIR-V"; - - device_->get_handle().destroyShaderModule(result.value()); - } - - // Test loading fragment shader - { - const auto& spirv = shader_loader::get_minimal_fragment_shader(); - auto result = graphics_pipeline::load_shader_module_from_memory(*device_, spirv); - - ASSERT_TRUE(result.has_value()) - << "Failed to load valid fragment shader SPIR-V"; - - device_->get_handle().destroyShaderModule(result.value()); - } -} - -// ============================================================================ -// P0 Priority Tests - Pipeline Creation -// ============================================================================ - -TEST_F(GraphicsPipelineTest, CreatePipeline_Success) { - // Create a simple pipeline using the constructor - vk::PipelineLayoutCreateInfo layout_info{}; - auto layout_result = device_->get_handle().createPipelineLayout(layout_info); - ASSERT_EQ(layout_result.result, vk::Result::eSuccess); - auto pipeline_layout = layout_result.value; - - // Create a dummy pipeline (we'll use a null handle for testing the wrapper) - vk::GraphicsPipelineCreateInfo pipeline_info{}; - - // Create render pass - vk::AttachmentDescription color_attachment{}; - color_attachment.format = vk::Format::eB8G8R8A8Unorm; - color_attachment.samples = vk::SampleCountFlagBits::e1; - color_attachment.loadOp = vk::AttachmentLoadOp::eClear; - color_attachment.storeOp = vk::AttachmentStoreOp::eStore; - color_attachment.stencilLoadOp = vk::AttachmentLoadOp::eDontCare; - color_attachment.stencilStoreOp = vk::AttachmentStoreOp::eDontCare; - color_attachment.initialLayout = vk::ImageLayout::eUndefined; - color_attachment.finalLayout = vk::ImageLayout::ePresentSrcKHR; - - vk::AttachmentReference color_ref{}; - color_ref.attachment = 0; - color_ref.layout = vk::ImageLayout::eColorAttachmentOptimal; - - vk::SubpassDescription subpass{}; - subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &color_ref; - - vk::RenderPassCreateInfo render_pass_info{}; - render_pass_info.attachmentCount = 1; - render_pass_info.pAttachments = &color_attachment; - render_pass_info.subpassCount = 1; - render_pass_info.pSubpasses = &subpass; - - auto rp_result = device_->get_handle().createRenderPass(render_pass_info); - ASSERT_EQ(rp_result.result, vk::Result::eSuccess); - auto render_pass = rp_result.value; - - // Load shaders - auto vert_result = graphics_pipeline::load_shader_module_from_memory( - *device_, shader_loader::get_minimal_vertex_shader()); - auto frag_result = graphics_pipeline::load_shader_module_from_memory( - *device_, shader_loader::get_minimal_fragment_shader()); - - ASSERT_TRUE(vert_result.has_value()); - ASSERT_TRUE(frag_result.has_value()); - - auto vert_shader = vert_result.value(); - auto frag_shader = frag_result.value(); - - // Set up shader stages - vk::PipelineShaderStageCreateInfo vert_stage{}; - vert_stage.stage = vk::ShaderStageFlagBits::eVertex; - vert_stage.module = vert_shader; - vert_stage.pName = "main"; - - vk::PipelineShaderStageCreateInfo frag_stage{}; - frag_stage.stage = vk::ShaderStageFlagBits::eFragment; - frag_stage.module = frag_shader; - frag_stage.pName = "main"; - - std::array stages = {vert_stage, frag_stage}; - - // Vertex input - vk::PipelineVertexInputStateCreateInfo vertex_input{}; - - // Input assembly - vk::PipelineInputAssemblyStateCreateInfo input_assembly{}; - input_assembly.topology = vk::PrimitiveTopology::eTriangleList; - - // Viewport and scissor - vk::Viewport viewport{}; - viewport.x = 0.0f; - viewport.y = 0.0f; - viewport.width = 800.0f; - viewport.height = 600.0f; - viewport.minDepth = 0.0f; - viewport.maxDepth = 1.0f; - - vk::Rect2D scissor{}; - scissor.offset = vk::Offset2D{0, 0}; - scissor.extent = vk::Extent2D{800, 600}; - - vk::PipelineViewportStateCreateInfo viewport_state{}; - viewport_state.viewportCount = 1; - viewport_state.pViewports = &viewport; - viewport_state.scissorCount = 1; - viewport_state.pScissors = &scissor; - - // Rasterization - vk::PipelineRasterizationStateCreateInfo rasterizer{}; - rasterizer.polygonMode = vk::PolygonMode::eFill; - rasterizer.cullMode = vk::CullModeFlagBits::eBack; - rasterizer.frontFace = vk::FrontFace::eCounterClockwise; - rasterizer.lineWidth = 1.0f; - - // Multisampling - vk::PipelineMultisampleStateCreateInfo multisampling{}; - multisampling.rasterizationSamples = vk::SampleCountFlagBits::e1; - - // Color blending - vk::PipelineColorBlendAttachmentState color_blend_attachment{}; - color_blend_attachment.colorWriteMask = - vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | - vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; - - vk::PipelineColorBlendStateCreateInfo color_blending{}; - color_blending.attachmentCount = 1; - color_blending.pAttachments = &color_blend_attachment; - - // Create pipeline - pipeline_info.stageCount = static_cast(stages.size()); - pipeline_info.pStages = stages.data(); - pipeline_info.pVertexInputState = &vertex_input; - pipeline_info.pInputAssemblyState = &input_assembly; - pipeline_info.pViewportState = &viewport_state; - pipeline_info.pRasterizationState = &rasterizer; - pipeline_info.pMultisampleState = &multisampling; - pipeline_info.pColorBlendState = &color_blending; - pipeline_info.layout = pipeline_layout; - pipeline_info.renderPass = render_pass; - pipeline_info.subpass = 0; - - auto pipeline_result = device_->get_handle().createGraphicsPipeline(nullptr, pipeline_info); - ASSERT_EQ(pipeline_result.result, vk::Result::eSuccess) - << "Failed to create graphics pipeline"; - - auto vk_pipeline = pipeline_result.value; - - // Create graphics_pipeline wrapper - graphics_pipeline pipeline(*device_, vk_pipeline, pipeline_layout); - - EXPECT_NE(pipeline.get_pipeline(), nullptr) - << "Pipeline handle should be valid"; - EXPECT_EQ(pipeline.get_layout(), pipeline_layout) - << "Layout should match"; - - // Clean up - device_->get_handle().destroyShaderModule(vert_shader); - device_->get_handle().destroyShaderModule(frag_shader); - device_->get_handle().destroyRenderPass(render_pass); - // pipeline destructor will clean up pipeline and layout -} - -TEST_F(GraphicsPipelineTest, GetPipeline_Valid) { - // Create a minimal test pipeline - vk::PipelineLayoutCreateInfo layout_info{}; - auto layout_result = device_->get_handle().createPipelineLayout(layout_info); - ASSERT_EQ(layout_result.result, vk::Result::eSuccess); - - // Use a null pipeline for this test (just testing the getter) - vk::Pipeline test_pipeline = nullptr; - - graphics_pipeline pipeline(*device_, test_pipeline, layout_result.value); - - auto retrieved = pipeline.get_pipeline(); - EXPECT_EQ(retrieved, test_pipeline) - << "get_pipeline should return the correct handle"; -} - -TEST_F(GraphicsPipelineTest, GetLayout_Valid) { - vk::PipelineLayoutCreateInfo layout_info{}; - auto layout_result = device_->get_handle().createPipelineLayout(layout_info); - ASSERT_EQ(layout_result.result, vk::Result::eSuccess); - auto test_layout = layout_result.value; - - graphics_pipeline pipeline(*device_, nullptr, test_layout); - - auto retrieved = pipeline.get_layout(); - EXPECT_EQ(retrieved, test_layout) - << "get_layout should return the correct handle"; -} - -// ============================================================================ -// P0 Priority Tests - Move Semantics -// ============================================================================ - -TEST_F(GraphicsPipelineTest, MoveSemantics) { - vk::PipelineLayoutCreateInfo layout_info{}; - auto layout_result = device_->get_handle().createPipelineLayout(layout_info); - ASSERT_EQ(layout_result.result, vk::Result::eSuccess); - auto test_layout = layout_result.value; - - vk::Pipeline test_pipeline = nullptr; - - // Test move constructor - { - graphics_pipeline pipeline1(*device_, test_pipeline, test_layout); - auto layout1 = pipeline1.get_layout(); - - graphics_pipeline pipeline2(std::move(pipeline1)); - - EXPECT_EQ(pipeline2.get_layout(), layout1) - << "Moved-to pipeline should have the original layout"; - EXPECT_EQ(pipeline1.get_layout(), nullptr) - << "Moved-from pipeline should have null layout"; - } - - // Test move assignment - { - // Create new layouts for this test block - vk::PipelineLayoutCreateInfo layout_info1{}; - auto layout_result1 = device_->get_handle().createPipelineLayout(layout_info1); - ASSERT_EQ(layout_result1.result, vk::Result::eSuccess); - - vk::PipelineLayoutCreateInfo layout_info2{}; - auto layout_result2 = device_->get_handle().createPipelineLayout(layout_info2); - ASSERT_EQ(layout_result2.result, vk::Result::eSuccess); - - graphics_pipeline pipeline1(*device_, test_pipeline, layout_result1.value); - graphics_pipeline pipeline2(*device_, test_pipeline, layout_result2.value); - - auto layout1 = pipeline1.get_layout(); - - pipeline2 = std::move(pipeline1); - - EXPECT_EQ(pipeline2.get_layout(), layout1) - << "Move assignment should transfer layout"; - EXPECT_EQ(pipeline1.get_layout(), nullptr) - << "Moved-from pipeline should have null layout"; - } -} - -// ============================================================================ -// P0 Priority Tests - Multiple Shaders -// ============================================================================ - -TEST_F(GraphicsPipelineTest, MultipleShaders_Success) { - // Load multiple shader modules - auto vert_result1 = graphics_pipeline::load_shader_module_from_memory( - *device_, shader_loader::get_minimal_vertex_shader()); - auto vert_result2 = graphics_pipeline::load_shader_module_from_memory( - *device_, shader_loader::get_minimal_vertex_shader()); - auto frag_result1 = graphics_pipeline::load_shader_module_from_memory( - *device_, shader_loader::get_minimal_fragment_shader()); - auto frag_result2 = graphics_pipeline::load_shader_module_from_memory( - *device_, shader_loader::get_minimal_fragment_shader()); - - ASSERT_TRUE(vert_result1.has_value()); - ASSERT_TRUE(vert_result2.has_value()); - ASSERT_TRUE(frag_result1.has_value()); - ASSERT_TRUE(frag_result2.has_value()); - - auto vert1 = vert_result1.value(); - auto vert2 = vert_result2.value(); - auto frag1 = frag_result1.value(); - auto frag2 = frag_result2.value(); - - // Verify all modules are valid and unique - EXPECT_NE(vert1, nullptr); - EXPECT_NE(vert2, nullptr); - EXPECT_NE(frag1, nullptr); - EXPECT_NE(frag2, nullptr); - - // Clean up - device_->get_handle().destroyShaderModule(vert1); - device_->get_handle().destroyShaderModule(vert2); - device_->get_handle().destroyShaderModule(frag1); - device_->get_handle().destroyShaderModule(frag2); -} - -// ============================================================================ -// P0 Priority Tests - Resource Cleanup -// ============================================================================ - -TEST_F(GraphicsPipelineTest, ResourceCleanup) { - vk::PipelineLayout test_layout; - vk::Pipeline test_pipeline; - - { - vk::PipelineLayoutCreateInfo layout_info{}; - auto layout_result = device_->get_handle().createPipelineLayout(layout_info); - ASSERT_EQ(layout_result.result, vk::Result::eSuccess); - test_layout = layout_result.value; - - // Create a minimal pipeline for cleanup testing - graphics_pipeline pipeline(*device_, nullptr, test_layout); - - test_pipeline = pipeline.get_pipeline(); - - // Pipeline destructor will be called here - } - - // After destruction, the resources should be cleaned up - // This is a conceptual test - actual verification would require - // Vulkan validation layers or tracking - SUCCEED() << "Pipeline destruction completed without crash"; -} \ No newline at end of file diff --git a/tests/unit/pipeline/test_pass_state.cpp b/tests/unit/pipeline/test_pass_state.cpp deleted file mode 100644 index 0678206..0000000 --- a/tests/unit/pipeline/test_pass_state.cpp +++ /dev/null @@ -1,388 +0,0 @@ -/// @file test_pass_state.cpp -/// @brief 渲染管线状态管理框架的单元测试 -/// @details 测试编译时类型检查和 Typestate 模式的正确性 - -#include - -#include "render/pipeline/pass_state_tags.h" -#include "render/pipeline/render_target_concept.h" -#include "render/pipeline/frame_context_v2.h" -#include "render/pipeline/nested_pass_context.h" -#include "render/pipeline/legacy_adapter.h" - -#include - -using namespace mirage::pass_state; - -// ============================================================================ -// Mock 类型用于 Concept 测试 -// ============================================================================ - -/// Mock 渲染目标 - 满足 RenderTarget concept -struct mock_render_target { - void begin_render(vk::CommandBuffer, bool) {} - void end_render(vk::CommandBuffer, bool) {} - auto image_view() -> vk::ImageView { return vk::ImageView{}; } - auto extent() -> vk::Extent2D { return vk::Extent2D{800, 600}; } - auto format() -> vk::Format { return vk::Format::eR8G8B8A8Unorm; } -}; - -/// 不满足 RenderTarget concept 的类型 -struct not_a_render_target { - void some_method() {} -}; - -/// Mock 批量渲染器 - 满足 BatchRenderer concept -struct mock_batch_renderer { - void begin_batch(vk::CommandBuffer) {} - void flush(vk::CommandBuffer) {} -}; - -// ============================================================================ -// 状态标签测试 -// ============================================================================ - -TEST(PassStateTagsTest, TagsAreEmptyTypes) { - static_assert(std::is_empty_v); - static_assert(std::is_empty_v); - static_assert(std::is_empty_v); - static_assert(std::is_empty_v); - static_assert(std::is_empty_v); - SUCCEED(); -} - -TEST(PassStateTagsTest, TagsAreTriviallyConstructible) { - static_assert(std::is_trivially_constructible_v); - static_assert(std::is_trivially_constructible_v); - static_assert(std::is_trivially_constructible_v); - static_assert(std::is_trivially_constructible_v); - static_assert(std::is_trivially_constructible_v); - SUCCEED(); -} - -TEST(PassStateTagsTest, PassStateTagConceptSatisfaction) { - static_assert(PassStateTag); - static_assert(PassStateTag); - static_assert(PassStateTag); - static_assert(PassStateTag); - static_assert(PassStateTag); - static_assert(!PassStateTag); - static_assert(!PassStateTag); - static_assert(!PassStateTag); - SUCCEED(); -} - -// ============================================================================ -// 嵌套深度标签测试 -// ============================================================================ - -TEST(NestedDepthTagTest, DepthValues) { - static_assert(nested_depth_tag<1>::depth == 1); - static_assert(nested_depth_tag<2>::depth == 2); - static_assert(nested_depth_tag<3>::depth == 3); - static_assert(nested_depth_tag<10>::depth == 10); - SUCCEED(); -} - -TEST(NestedDepthTagTest, SatisfiesPassStateTag) { - static_assert(PassStateTag>); - static_assert(PassStateTag>); - static_assert(PassStateTag>); - SUCCEED(); -} - -TEST(NestedDepthTagTest, AreEmptyTypes) { - static_assert(std::is_empty_v>); - static_assert(std::is_empty_v>); - SUCCEED(); -} - -// ============================================================================ -// 状态转换验证测试 -// ============================================================================ - -TEST(StateTransitionTest, ValidTransitionsFromIdle) { - static_assert(can_transition()); - static_assert(can_transition()); - SUCCEED(); -} - -TEST(StateTransitionTest, ValidTransitionsFromOffscreenPass) { - static_assert(can_transition()); - static_assert(can_transition()); - static_assert(can_transition()); - SUCCEED(); -} - -TEST(StateTransitionTest, ValidTransitionsFromSwapchainPass) { - static_assert(can_transition()); - SUCCEED(); -} - -TEST(StateTransitionTest, ValidTransitionsFromMaskPass) { - static_assert(can_transition()); - static_assert(can_transition()); - SUCCEED(); -} - -TEST(StateTransitionTest, ValidTransitionsFromPostEffectPass) { - static_assert(can_transition()); - static_assert(can_transition()); - SUCCEED(); -} - -TEST(StateTransitionTest, InvalidTransitions) { - static_assert(!can_transition()); - static_assert(!can_transition()); - static_assert(!can_transition()); - static_assert(!can_transition()); - static_assert(!can_transition()); - static_assert(!can_transition()); - static_assert(!can_transition()); - SUCCEED(); -} - -TEST(StateTransitionTest, NestedDepthTransitions) { - static_assert(can_transition>()); - static_assert(can_transition, nested_depth_tag<2>>()); - static_assert(can_transition, nested_depth_tag<1>>()); - static_assert(can_transition, offscreen_pass_tag>()); - SUCCEED(); -} - -// ============================================================================ -// frame_resources 测试 -// ============================================================================ - -TEST(FrameResourcesTest, IsMoveOnly) { - static_assert(std::is_move_constructible_v); - static_assert(std::is_move_assignable_v); - static_assert(!std::is_copy_constructible_v); - static_assert(!std::is_copy_assignable_v); - SUCCEED(); -} - -TEST(FrameResourcesTest, IsNothrowMovable) { - static_assert(std::is_nothrow_move_constructible_v); - static_assert(std::is_nothrow_move_assignable_v); - SUCCEED(); -} - -TEST(FrameResourcesTest, IsDefaultConstructible) { - static_assert(std::is_default_constructible_v); - SUCCEED(); -} - -// ============================================================================ -// frame_context 测试 -// ============================================================================ - -TEST(FrameContextTest, IsMoveOnly) { - using idle_ctx = frame_context; - using offscreen_ctx = frame_context; - using swapchain_ctx = frame_context; - using mask_ctx = frame_context; - - static_assert(std::is_move_constructible_v); - static_assert(std::is_move_assignable_v); - static_assert(!std::is_copy_constructible_v); - static_assert(!std::is_copy_assignable_v); - - static_assert(std::is_move_constructible_v); - static_assert(!std::is_copy_constructible_v); - - static_assert(std::is_move_constructible_v); - static_assert(!std::is_copy_constructible_v); - - static_assert(std::is_move_constructible_v); - static_assert(!std::is_copy_constructible_v); - SUCCEED(); -} - -TEST(FrameContextTest, HasCorrectStateTag) { - static_assert(std::is_same_v::state_tag, idle_tag>); - static_assert(std::is_same_v::state_tag, - offscreen_pass_tag>); - static_assert(std::is_same_v::state_tag, - swapchain_pass_tag>); - SUCCEED(); -} - -// ============================================================================ -// RenderTarget Concept 测试 -// ============================================================================ - -TEST(RenderTargetConceptTest, MockSatisfiesConcept) { - static_assert(RenderTarget); - SUCCEED(); -} - -TEST(RenderTargetConceptTest, NonTargetsRejected) { - static_assert(!RenderTarget); - static_assert(!RenderTarget); - static_assert(!RenderTarget); - SUCCEED(); -} - -TEST(RenderTargetConceptTest, TypeTraitConsistency) { - static_assert(is_render_target_v == RenderTarget); - static_assert(is_render_target_v == RenderTarget); - SUCCEED(); -} - -// ============================================================================ -// BatchRenderer Concept 测试 -// ============================================================================ - -TEST(BatchRendererConceptTest, MockSatisfiesConcept) { - static_assert(BatchRenderer); - SUCCEED(); -} - -TEST(BatchRendererConceptTest, NonRenderersRejected) { - static_assert(!BatchRenderer); - static_assert(!BatchRenderer); - SUCCEED(); -} - -TEST(BatchRendererConceptTest, TypeTraitConsistency) { - static_assert(is_batch_renderer_v == BatchRenderer); - SUCCEED(); -} - -// ============================================================================ -// ActivePassStateTag Concept 测试 -// ============================================================================ - -TEST(ActivePassStateTagConceptTest, ActiveStatesIdentified) { - static_assert(ActivePassStateTag); - static_assert(ActivePassStateTag); - static_assert(ActivePassStateTag); - SUCCEED(); -} - -TEST(ActivePassStateTagConceptTest, NonActiveStatesRejected) { - static_assert(!ActivePassStateTag); - static_assert(!ActivePassStateTag); - SUCCEED(); -} - -TEST(ActivePassStateTagConceptTest, NonTagsRejected) { - static_assert(!ActivePassStateTag); - static_assert(!ActivePassStateTag); - SUCCEED(); -} - -// ============================================================================ -// nested_pass_context 测试 -// ============================================================================ - -TEST(NestedPassContextTest, IsMoveOnly) { - using mask_nested_ctx = nested_pass_context; - static_assert(std::is_move_constructible_v); - static_assert(std::is_move_assignable_v); - static_assert(!std::is_copy_constructible_v); - static_assert(!std::is_copy_assignable_v); - SUCCEED(); -} - -TEST(NestedPassContextTest, DepthIsCorrect) { - using ctx_depth1 = nested_pass_context; - using ctx_depth2 = nested_pass_context; - using ctx_depth3 = nested_pass_context; - static_assert(ctx_depth1::depth == 1); - static_assert(ctx_depth2::depth == 2); - static_assert(ctx_depth3::depth == 3); - SUCCEED(); -} - -TEST(NestedPassContextTest, TypeAliasesCorrect) { - using ctx = nested_pass_context; - static_assert(std::is_same_v); - static_assert(std::is_same_v); - SUCCEED(); -} - -// ============================================================================ -// mask_pass_result 测试 -// ============================================================================ - -TEST(MaskPassResultTest, IsMoveOnly) { - static_assert(std::is_move_constructible_v); - static_assert(std::is_move_assignable_v); - static_assert(!std::is_copy_constructible_v); - static_assert(!std::is_copy_assignable_v); - SUCCEED(); -} - -TEST(MaskPassResultTest, IsNothrowMovable) { - static_assert(std::is_nothrow_move_constructible_v); - static_assert(std::is_nothrow_move_assignable_v); - SUCCEED(); -} - -// ============================================================================ -// render_pass_scope 测试 -// ============================================================================ - -TEST(RenderPassScopeTest, IsNotCopyable) { - using scope_type = render_pass_scope; - static_assert(!std::is_copy_constructible_v); - static_assert(!std::is_copy_assignable_v); - SUCCEED(); -} - -TEST(RenderPassScopeTest, IsNotMovable) { - using scope_type = render_pass_scope; - static_assert(!std::is_move_constructible_v); - static_assert(!std::is_move_assignable_v); - SUCCEED(); -} - -// ============================================================================ -// 运行时状态转换矩阵测试 -// ============================================================================ - -TEST(RuntimeTransitionMatrixTest, IdleTransitions) { - EXPECT_TRUE(can_transition_runtime(state_type::idle, state_type::offscreen_pass)); - EXPECT_TRUE(can_transition_runtime(state_type::idle, state_type::swapchain_pass)); - EXPECT_FALSE(can_transition_runtime(state_type::idle, state_type::mask_pass)); - EXPECT_FALSE(can_transition_runtime(state_type::idle, state_type::post_effect_pass)); -} - -TEST(RuntimeTransitionMatrixTest, OffscreenPassTransitions) { - EXPECT_TRUE(can_transition_runtime(state_type::offscreen_pass, state_type::idle)); - EXPECT_TRUE(can_transition_runtime(state_type::offscreen_pass, state_type::mask_pass)); - EXPECT_TRUE(can_transition_runtime(state_type::offscreen_pass, state_type::post_effect_pass)); - EXPECT_FALSE(can_transition_runtime(state_type::offscreen_pass, state_type::swapchain_pass)); -} - -TEST(RuntimeTransitionMatrixTest, SwapchainPassTransitions) { - EXPECT_TRUE(can_transition_runtime(state_type::swapchain_pass, state_type::idle)); - EXPECT_FALSE(can_transition_runtime(state_type::swapchain_pass, state_type::offscreen_pass)); - EXPECT_FALSE(can_transition_runtime(state_type::swapchain_pass, state_type::mask_pass)); -} - -TEST(RuntimeTransitionMatrixTest, MaskPassTransitions) { - EXPECT_TRUE(can_transition_runtime(state_type::mask_pass, state_type::offscreen_pass)); - EXPECT_TRUE(can_transition_runtime(state_type::mask_pass, state_type::mask_pass)); - EXPECT_FALSE(can_transition_runtime(state_type::mask_pass, state_type::idle)); -} - -TEST(RuntimeTransitionMatrixTest, PostEffectPassTransitions) { - EXPECT_TRUE(can_transition_runtime(state_type::post_effect_pass, state_type::idle)); - EXPECT_TRUE(can_transition_runtime(state_type::post_effect_pass, state_type::offscreen_pass)); - EXPECT_FALSE(can_transition_runtime(state_type::post_effect_pass, state_type::swapchain_pass)); -} - -// ============================================================================ -// state_name 测试 -// ============================================================================ - -TEST(StateNameTest, ReturnsCorrectNames) { - EXPECT_STREQ(state_name(state_type::idle), "idle"); - EXPECT_STREQ(state_name(state_type::offscreen_pass), "offscreen_pass"); - EXPECT_STREQ(state_name(state_type::swapchain_pass), "swapchain_pass"); - EXPECT_STREQ(state_name(state_type::mask_pass), "mask_pass"); - EXPECT_STREQ(state_name(state_type::post_effect_pass), "post_effect_pass"); -} \ No newline at end of file diff --git a/tests/unit/pipeline/test_pipeline_builder.cpp b/tests/unit/pipeline/test_pipeline_builder.cpp deleted file mode 100644 index 10aa26b..0000000 --- a/tests/unit/pipeline/test_pipeline_builder.cpp +++ /dev/null @@ -1,351 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "utils/shader_loader.h" -#include "vulkan/pipeline/pipeline_builder.h" -#include "vulkan/pipeline/graphics_pipeline.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class PipelineBuilderTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - // Create shader modules - auto vert_result = graphics_pipeline::load_shader_module_from_memory( - *device_, shader_loader::get_minimal_vertex_shader()); - auto frag_result = graphics_pipeline::load_shader_module_from_memory( - *device_, shader_loader::get_minimal_fragment_shader()); - - ASSERT_TRUE(vert_result.has_value()) - << "Failed to load vertex shader: " << vk::to_string(vert_result.error()); - ASSERT_TRUE(frag_result.has_value()) - << "Failed to load fragment shader: " << vk::to_string(frag_result.error()); - - vert_shader_ = vert_result.value(); - frag_shader_ = frag_result.value(); - - // Create a simple render pass for testing - render_pass_ = CreateTestRenderPass(); - ASSERT_NE(render_pass_, nullptr) << "Failed to create render pass"; - - // Create a simple pipeline layout - vk::PipelineLayoutCreateInfo layout_info{}; - auto layout_result = device_->get_handle().createPipelineLayout(layout_info); - ASSERT_EQ(layout_result.result, vk::Result::eSuccess) - << "Failed to create pipeline layout"; - pipeline_layout_ = layout_result.value; - } - - void TearDown() override { - if (device_) { - auto vk_device = device_->get_handle(); - // pipeline_layout_ is managed by graphics_pipeline objects - // Only clean up resources not owned by graphics_pipeline - if (render_pass_) { - vk_device.destroyRenderPass(render_pass_); - render_pass_ = nullptr; - } - if (vert_shader_) { - vk_device.destroyShaderModule(vert_shader_); - vert_shader_ = nullptr; - } - if (frag_shader_) { - vk_device.destroyShaderModule(frag_shader_); - frag_shader_ = nullptr; - } - } - VulkanGPUTest::TearDown(); - } - - vk::RenderPass CreateTestRenderPass() { - // Create a simple render pass with a single color attachment - vk::AttachmentDescription color_attachment{}; - color_attachment.format = vk::Format::eB8G8R8A8Unorm; - color_attachment.samples = vk::SampleCountFlagBits::e1; - color_attachment.loadOp = vk::AttachmentLoadOp::eClear; - color_attachment.storeOp = vk::AttachmentStoreOp::eStore; - color_attachment.stencilLoadOp = vk::AttachmentLoadOp::eDontCare; - color_attachment.stencilStoreOp = vk::AttachmentStoreOp::eDontCare; - color_attachment.initialLayout = vk::ImageLayout::eUndefined; - color_attachment.finalLayout = vk::ImageLayout::ePresentSrcKHR; - - vk::AttachmentReference color_attachment_ref{}; - color_attachment_ref.attachment = 0; - color_attachment_ref.layout = vk::ImageLayout::eColorAttachmentOptimal; - - vk::SubpassDescription subpass{}; - subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &color_attachment_ref; - - vk::RenderPassCreateInfo render_pass_info{}; - render_pass_info.attachmentCount = 1; - render_pass_info.pAttachments = &color_attachment; - render_pass_info.subpassCount = 1; - render_pass_info.pSubpasses = &subpass; - - auto result = device_->get_handle().createRenderPass(render_pass_info); - if (result.result != vk::Result::eSuccess) { - return nullptr; - } - return result.value; - } - - vk::ShaderModule vert_shader_; - vk::ShaderModule frag_shader_; - vk::RenderPass render_pass_; - vk::PipelineLayout pipeline_layout_; -}; - -// ============================================================================ -// P0 Priority Tests - Basic Pipeline Building -// ============================================================================ - -TEST_F(PipelineBuilderTest, Build_MinimalPipeline) { - pipeline_builder builder(*device_); - - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(pipeline_layout_, render_pass_); - - ASSERT_TRUE(result.has_value()) - << "Failed to build minimal pipeline: " << vk::to_string(result.error()); - - auto& pipeline = result.value(); - EXPECT_NE(pipeline.get_pipeline(), nullptr) - << "Pipeline handle should be valid"; - EXPECT_NE(pipeline.get_layout(), nullptr) - << "Pipeline layout should be valid"; -} - -TEST_F(PipelineBuilderTest, SetVertexShader_Valid) { - pipeline_builder builder(*device_); - - auto& builder_ref = builder.set_vertex_shader(vert_shader_); - - // Verify the builder returns itself for chaining - EXPECT_EQ(&builder_ref, &builder) - << "set_vertex_shader should return *this for chaining"; - - // Verify we can build with vertex shader set - auto result = builder - .set_fragment_shader(frag_shader_) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(pipeline_layout_, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build successfully with vertex shader"; -} - -TEST_F(PipelineBuilderTest, SetFragmentShader_Valid) { - pipeline_builder builder(*device_); - - auto& builder_ref = builder.set_fragment_shader(frag_shader_); - - // Verify the builder returns itself for chaining - EXPECT_EQ(&builder_ref, &builder) - << "set_fragment_shader should return *this for chaining"; - - // Verify we can build with fragment shader set - auto result = builder - .set_vertex_shader(vert_shader_) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(pipeline_layout_, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build successfully with fragment shader"; -} - -TEST_F(PipelineBuilderTest, SetTopology_Various) { - // Test Triangle topology - { - // Create dedicated pipeline layout for this scope - vk::PipelineLayoutCreateInfo layout_info{}; - auto layout_result = device_->get_handle().createPipelineLayout(layout_info); - ASSERT_EQ(layout_result.result, vk::Result::eSuccess); - auto triangle_layout = layout_result.value; - - pipeline_builder builder(*device_); - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_topology(vk::PrimitiveTopology::eTriangleList) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(triangle_layout, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build with triangle topology"; - } - - // Test Line topology - { - // Create dedicated pipeline layout for this scope - vk::PipelineLayoutCreateInfo layout_info{}; - auto layout_result = device_->get_handle().createPipelineLayout(layout_info); - ASSERT_EQ(layout_result.result, vk::Result::eSuccess); - auto line_layout = layout_result.value; - - pipeline_builder builder(*device_); - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_topology(vk::PrimitiveTopology::eLineList) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(line_layout, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build with line topology"; - } -} - -TEST_F(PipelineBuilderTest, SetPolygonMode_Fill) { - pipeline_builder builder(*device_); - - auto& builder_ref = builder.set_polygon_mode(vk::PolygonMode::eFill); - EXPECT_EQ(&builder_ref, &builder) - << "set_polygon_mode should return *this for chaining"; - - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(pipeline_layout_, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build with fill polygon mode"; -} - -TEST_F(PipelineBuilderTest, SetCullMode_Back) { - pipeline_builder builder(*device_); - - auto& builder_ref = builder.set_cull_mode( - vk::CullModeFlagBits::eBack, - vk::FrontFace::eCounterClockwise); - - EXPECT_EQ(&builder_ref, &builder) - << "set_cull_mode should return *this for chaining"; - - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(pipeline_layout_, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build with back-face culling"; -} - -TEST_F(PipelineBuilderTest, EnableDepthTest) { - pipeline_builder builder(*device_); - - auto& builder_ref = builder.enable_depth_test(true, vk::CompareOp::eLess); - EXPECT_EQ(&builder_ref, &builder) - << "enable_depth_test should return *this for chaining"; - - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(pipeline_layout_, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build with depth test enabled"; -} - -TEST_F(PipelineBuilderTest, EnableBlending) { - pipeline_builder builder(*device_); - - auto& builder_ref = builder.enable_blending(true); - EXPECT_EQ(&builder_ref, &builder) - << "enable_blending should return *this for chaining"; - - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(pipeline_layout_, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build with blending enabled"; -} - -TEST_F(PipelineBuilderTest, SetViewport_Valid) { - pipeline_builder builder(*device_); - - auto& builder_ref = builder.set_viewport( - 100.0f, 50.0f, // x, y - 1920.0f, 1080.0f, // width, height - 0.0f, 1.0f // min/max depth - ); - - EXPECT_EQ(&builder_ref, &builder) - << "set_viewport should return *this for chaining"; - - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_scissor(0, 0, 1920, 1080) - .build(pipeline_layout_, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build with custom viewport"; -} - -TEST_F(PipelineBuilderTest, SetScissor_Valid) { - pipeline_builder builder(*device_); - - auto& builder_ref = builder.set_scissor(10, 20, 1280, 720); - EXPECT_EQ(&builder_ref, &builder) - << "set_scissor should return *this for chaining"; - - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_viewport(0.0f, 0.0f, 1280.0f, 720.0f) - .build(pipeline_layout_, render_pass_); - - EXPECT_TRUE(result.has_value()) - << "Should build with custom scissor"; -} - -TEST_F(PipelineBuilderTest, ChainedCalls) { - pipeline_builder builder(*device_); - - // Test all methods can be chained together - auto result = builder - .set_vertex_shader(vert_shader_) - .set_fragment_shader(frag_shader_) - .set_topology(vk::PrimitiveTopology::eTriangleList) - .set_polygon_mode(vk::PolygonMode::eFill) - .set_cull_mode(vk::CullModeFlagBits::eBack, vk::FrontFace::eCounterClockwise) - .set_multisampling(vk::SampleCountFlagBits::e1) - .enable_depth_test(true, vk::CompareOp::eLess) - .enable_blending(false) - .set_viewport(0.0f, 0.0f, 800.0f, 600.0f) - .set_scissor(0, 0, 800, 600) - .build(pipeline_layout_, render_pass_); - - ASSERT_TRUE(result.has_value()) - << "Should build successfully with all methods chained: " - << vk::to_string(result.error()); - - auto& pipeline = result.value(); - EXPECT_NE(pipeline.get_pipeline(), nullptr) - << "Pipeline should be valid after chained calls"; -} \ No newline at end of file diff --git a/tests/unit/resource/test_command_buffer.cpp b/tests/unit/resource/test_command_buffer.cpp deleted file mode 100644 index 5e9986a..0000000 --- a/tests/unit/resource/test_command_buffer.cpp +++ /dev/null @@ -1,237 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "vulkan/command_buffer.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class CommandBufferTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - // Create command pool for testing - CreateCommandPool(); - } - - void TearDown() override { - if (device_ && command_pool_) { - device_->get_handle().destroyCommandPool(command_pool_); - command_pool_ = nullptr; - } - - VulkanGPUTest::TearDown(); - } - - void CreateCommandPool() { - vk::CommandPoolCreateInfo pool_info{}; - pool_info.queueFamilyIndex = device_->get_compute_family_index(); - pool_info.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer; - - auto result = device_->get_handle().createCommandPool(pool_info); - ASSERT_EQ(result.result, vk::Result::eSuccess) - << "Failed to create command pool"; - - command_pool_ = result.value; - } - - vk::CommandPool command_pool_; -}; - -// ============================================================================ -// P0 Priority Tests - Command Buffer Allocation -// ============================================================================ - -TEST_F(CommandBufferTest, Allocate_Success) { - command_buffer cmd_buffer(*device_, command_pool_); - - auto result = cmd_buffer.allocate(); - - EXPECT_EQ(result, vk::Result::eSuccess) - << "Command buffer allocation should succeed"; - - EXPECT_NE(cmd_buffer.get_handle(), nullptr) - << "Command buffer handle should be valid after allocation"; -} - -// ============================================================================ -// P0 Priority Tests - Recording -// ============================================================================ - -TEST_F(CommandBufferTest, BeginEnd_Recording) { - command_buffer cmd_buffer(*device_, command_pool_); - - auto alloc_result = cmd_buffer.allocate(); - ASSERT_EQ(alloc_result, vk::Result::eSuccess); - - // Begin recording - auto begin_result = cmd_buffer.begin_recording(); - EXPECT_EQ(begin_result, vk::Result::eSuccess) - << "Begin recording should succeed"; - - // End recording - auto end_result = cmd_buffer.end_recording(); - EXPECT_EQ(end_result, vk::Result::eSuccess) - << "End recording should succeed"; -} - -// ============================================================================ -// P0 Priority Tests - Submission -// ============================================================================ - -TEST_F(CommandBufferTest, Submit_WithFence) { - command_buffer cmd_buffer(*device_, command_pool_); - - auto alloc_result = cmd_buffer.allocate(); - ASSERT_EQ(alloc_result, vk::Result::eSuccess); - - auto begin_result = cmd_buffer.begin_recording(); - ASSERT_EQ(begin_result, vk::Result::eSuccess); - - auto end_result = cmd_buffer.end_recording(); - ASSERT_EQ(end_result, vk::Result::eSuccess); - - // Create a fence for synchronization - vk::FenceCreateInfo fence_info{}; - auto fence_result = device_->get_handle().createFence(fence_info); - ASSERT_EQ(fence_result.result, vk::Result::eSuccess); - vk::Fence fence = fence_result.value; - - // Submit with fence - auto submit_result = cmd_buffer.submit(fence); - EXPECT_EQ(submit_result, vk::Result::eSuccess) - << "Submit with fence should succeed"; - - // Wait for fence - auto wait_result = device_->get_handle().waitForFences( - 1, &fence, VK_TRUE, UINT64_MAX - ); - EXPECT_EQ(wait_result, vk::Result::eSuccess) - << "Fence wait should succeed"; - - // Clean up fence - device_->get_handle().destroyFence(fence); -} - -TEST_F(CommandBufferTest, Submit_WithSemaphore) { - command_buffer cmd_buffer(*device_, command_pool_); - - auto alloc_result = cmd_buffer.allocate(); - ASSERT_EQ(alloc_result, vk::Result::eSuccess); - - auto begin_result = cmd_buffer.begin_recording(); - ASSERT_EQ(begin_result, vk::Result::eSuccess); - - auto end_result = cmd_buffer.end_recording(); - ASSERT_EQ(end_result, vk::Result::eSuccess); - - // Create semaphores for synchronization - vk::SemaphoreCreateInfo semaphore_info{}; - - auto wait_sem_result = device_->get_handle().createSemaphore(semaphore_info); - ASSERT_EQ(wait_sem_result.result, vk::Result::eSuccess); - vk::Semaphore wait_semaphore = wait_sem_result.value; - - auto signal_sem_result = device_->get_handle().createSemaphore(semaphore_info); - ASSERT_EQ(signal_sem_result.result, vk::Result::eSuccess); - vk::Semaphore signal_semaphore = signal_sem_result.value; - - // Create fence for wait - vk::FenceCreateInfo fence_info{}; - auto fence_result = device_->get_handle().createFence(fence_info); - ASSERT_EQ(fence_result.result, vk::Result::eSuccess); - vk::Fence fence = fence_result.value; - - // Submit with semaphores - auto submit_result = cmd_buffer.submit(fence, nullptr, signal_semaphore); - EXPECT_EQ(submit_result, vk::Result::eSuccess) - << "Submit with semaphore should succeed"; - - // Wait for completion - device_->get_handle().waitForFences(1, &fence, VK_TRUE, UINT64_MAX); - - // Clean up - device_->get_handle().destroyFence(fence); - device_->get_handle().destroySemaphore(wait_semaphore); - device_->get_handle().destroySemaphore(signal_semaphore); -} - -TEST_F(CommandBufferTest, MultipleSubmit_Success) { - command_buffer cmd_buffer(*device_, command_pool_); - - auto alloc_result = cmd_buffer.allocate(); - ASSERT_EQ(alloc_result, vk::Result::eSuccess); - - const int num_submits = 3; - - for (int i = 0; i < num_submits; ++i) { - // Begin recording - auto begin_result = cmd_buffer.begin_recording(); - ASSERT_EQ(begin_result, vk::Result::eSuccess) - << "Begin recording iteration " << i << " should succeed"; - - // End recording - auto end_result = cmd_buffer.end_recording(); - ASSERT_EQ(end_result, vk::Result::eSuccess) - << "End recording iteration " << i << " should succeed"; - - // Create fence - vk::FenceCreateInfo fence_info{}; - auto fence_result = device_->get_handle().createFence(fence_info); - ASSERT_EQ(fence_result.result, vk::Result::eSuccess); - vk::Fence fence = fence_result.value; - - // Submit - auto submit_result = cmd_buffer.submit(fence); - EXPECT_EQ(submit_result, vk::Result::eSuccess) - << "Submit iteration " << i << " should succeed"; - - // Wait for completion - device_->get_handle().waitForFences(1, &fence, VK_TRUE, UINT64_MAX); - - // Clean up fence - device_->get_handle().destroyFence(fence); - } -} - -// ============================================================================ -// P0 Priority Tests - Handle Access -// ============================================================================ - -TEST_F(CommandBufferTest, GetHandle_Valid) { - command_buffer cmd_buffer(*device_, command_pool_); - - // Before allocation, handle should be null - EXPECT_EQ(cmd_buffer.get_handle(), nullptr) - << "Handle should be null before allocation"; - - auto alloc_result = cmd_buffer.allocate(); - ASSERT_EQ(alloc_result, vk::Result::eSuccess); - - // After allocation, handle should be valid - EXPECT_NE(cmd_buffer.get_handle(), nullptr) - << "Handle should be valid after allocation"; -} - -// ============================================================================ -// P0 Priority Tests - Resource Cleanup -// ============================================================================ - -TEST_F(CommandBufferTest, ResourceCleanup) { - { - command_buffer cmd_buffer(*device_, command_pool_); - - auto alloc_result = cmd_buffer.allocate(); - ASSERT_EQ(alloc_result, vk::Result::eSuccess); - - EXPECT_NE(cmd_buffer.get_handle(), nullptr); - - // Command buffer destructor will be called here - } - - // After destruction, command buffer should be cleaned up - SUCCEED() << "Command buffer destruction completed without crash"; -} \ No newline at end of file diff --git a/tests/unit/resource/test_resource_manager.cpp b/tests/unit/resource/test_resource_manager.cpp deleted file mode 100644 index fc39e0e..0000000 --- a/tests/unit/resource/test_resource_manager.cpp +++ /dev/null @@ -1,263 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "vulkan/resource_manager.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class ResourceManagerTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - resource_mgr_ = std::make_unique(*device_, physical_device_, context_->get_instance()); - } - - void TearDown() override { - // Clean up any test resources - for (auto& buffer : test_buffers_) { - if (resource_mgr_) { - resource_mgr_->destroy_buffer(buffer); - } - } - test_buffers_.clear(); - - for (auto& image : test_images_) { - if (resource_mgr_) { - resource_mgr_->destroy_image(image); - } - } - test_images_.clear(); - - resource_mgr_.reset(); - - VulkanGPUTest::TearDown(); - } - - std::unique_ptr resource_mgr_; - std::vector test_buffers_; - std::vector test_images_; -}; - -// ============================================================================ -// P0 Priority Tests - Buffer Creation -// ============================================================================ - -TEST_F(ResourceManagerTest, CreateBuffer_DeviceLocal) { - const vk::DeviceSize size = 1024; - - auto result = resource_mgr_->create_buffer( - size, - vk::BufferUsageFlagBits::eStorageBuffer, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - ASSERT_TRUE(result.has_value()) - << "Failed to create device local buffer: " - << vk::to_string(result.error()); - - auto& buffer = result.value(); - test_buffers_.push_back(buffer); - - EXPECT_NE(buffer.buffer, nullptr) - << "Buffer handle should be valid"; - EXPECT_NE(buffer.allocation, nullptr) - << "Allocation handle should be valid"; - EXPECT_EQ(buffer.size, size) - << "Buffer size should match requested size"; -} - -TEST_F(ResourceManagerTest, CreateBuffer_HostVisible) { - const vk::DeviceSize size = 2048; - - auto result = resource_mgr_->create_buffer( - size, - vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - ASSERT_TRUE(result.has_value()) - << "Failed to create host visible buffer: " - << vk::to_string(result.error()); - - auto& buffer = result.value(); - test_buffers_.push_back(buffer); - - EXPECT_NE(buffer.buffer, nullptr); - EXPECT_NE(buffer.allocation, nullptr); - EXPECT_EQ(buffer.size, size); - // VMA should have mapped the memory - EXPECT_NE(buffer.mapped_data, nullptr) - << "Host visible buffer should have mapped data"; -} - -TEST_F(ResourceManagerTest, CreateBuffer_InvalidSize) { - // Try to create a buffer with size 0 - auto result = resource_mgr_->create_buffer( - 0, - vk::BufferUsageFlagBits::eStorageBuffer, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - EXPECT_FALSE(result.has_value()) - << "Creating buffer with size 0 should fail"; -} - -// ============================================================================ -// P0 Priority Tests - Image Creation -// ============================================================================ - -TEST_F(ResourceManagerTest, CreateImage_Basic) { - const uint32_t width = 512; - const uint32_t height = 512; - - auto result = resource_mgr_->create_image( - width, - height, - vk::Format::eR8G8B8A8Unorm, - vk::ImageTiling::eOptimal, - vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - ASSERT_TRUE(result.has_value()) - << "Failed to create image: " - << vk::to_string(result.error()); - - auto& image = result.value(); - test_images_.push_back(image); - - EXPECT_NE(image.image, nullptr) - << "Image handle should be valid"; - EXPECT_NE(image.allocation, nullptr) - << "Allocation handle should be valid"; - EXPECT_NE(image.view, nullptr) - << "Image view should be valid"; - EXPECT_EQ(image.format, vk::Format::eR8G8B8A8Unorm) - << "Image format should match"; -} - -TEST_F(ResourceManagerTest, CreateImageView_Success) { - // First create an image - const uint32_t width = 256; - const uint32_t height = 256; - - auto image_result = resource_mgr_->create_image( - width, - height, - vk::Format::eR8G8B8A8Unorm, - vk::ImageTiling::eOptimal, - vk::ImageUsageFlagBits::eSampled, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - ASSERT_TRUE(image_result.has_value()); - auto& image = image_result.value(); - test_images_.push_back(image); - - // Create another image view for the same image - auto view_result = resource_mgr_->create_image_view( - image.image, - vk::Format::eR8G8B8A8Unorm, - vk::ImageAspectFlagBits::eColor - ); - - ASSERT_TRUE(view_result.has_value()) - << "Failed to create image view: " - << vk::to_string(view_result.error()); - - auto view = view_result.value(); - EXPECT_NE(view, nullptr) - << "Image view handle should be valid"; - - // Clean up the additional view - device_->get_handle().destroyImageView(view); -} - -// ============================================================================ -// P0 Priority Tests - Resource Destruction -// ============================================================================ - -TEST_F(ResourceManagerTest, DestroyBuffer_Success) { - // Create a buffer - auto result = resource_mgr_->create_buffer( - 1024, - vk::BufferUsageFlagBits::eStorageBuffer, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - ASSERT_TRUE(result.has_value()); - auto buffer = result.value(); - - // Destroy should not throw - EXPECT_NO_THROW({ - resource_mgr_->destroy_buffer(buffer); - }) << "Destroying buffer should not throw"; -} - -TEST_F(ResourceManagerTest, DestroyImage_Success) { - // Create an image - auto result = resource_mgr_->create_image( - 256, - 256, - vk::Format::eR8G8B8A8Unorm, - vk::ImageTiling::eOptimal, - vk::ImageUsageFlagBits::eSampled, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - ASSERT_TRUE(result.has_value()); - auto image = result.value(); - - // Destroy should not throw - EXPECT_NO_THROW({ - resource_mgr_->destroy_image(image); - }) << "Destroying image should not throw"; -} - -// ============================================================================ -// P0 Priority Tests - Multiple Resources -// ============================================================================ - -TEST_F(ResourceManagerTest, MultipleBuffers_Management) { - const int num_buffers = 5; - std::vector buffers; - - // Create multiple buffers with different sizes - for (int i = 0; i < num_buffers; ++i) { - vk::DeviceSize size = 256 * (i + 1); - - auto result = resource_mgr_->create_buffer( - size, - vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - ASSERT_TRUE(result.has_value()) - << "Failed to create buffer " << i; - - auto& buffer = result.value(); - buffers.push_back(buffer); - - EXPECT_NE(buffer.buffer, nullptr) - << "Buffer " << i << " should be valid"; - EXPECT_EQ(buffer.size, size) - << "Buffer " << i << " size should match"; - } - - // Verify all buffers are unique - for (size_t i = 0; i < buffers.size(); ++i) { - for (size_t j = i + 1; j < buffers.size(); ++j) { - EXPECT_NE(buffers[i].buffer, buffers[j].buffer) - << "Buffers " << i << " and " << j << " should be different"; - } - } - - // Clean up all buffers - for (auto& buffer : buffers) { - resource_mgr_->destroy_buffer(buffer); - } -} \ No newline at end of file diff --git a/tests/unit/resource/test_typed_buffer.cpp b/tests/unit/resource/test_typed_buffer.cpp deleted file mode 100644 index e5d46a5..0000000 --- a/tests/unit/resource/test_typed_buffer.cpp +++ /dev/null @@ -1,238 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "vulkan/typed_buffer.h" -#include "vulkan/resource_manager.h" -#include - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -// Test structures for type safety testing -struct TestVertex { - float x, y, z; - float r, g, b; -}; - -struct TestData { - int id; - float value; - char padding[8]; -}; - -class TypedBufferTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - resource_mgr_ = std::make_unique(*device_, physical_device_, context_->get_instance()); - } - - void TearDown() override { - resource_mgr_.reset(); - VulkanGPUTest::TearDown(); - } - - template - typed_buffer CreateTypedBuffer(size_t count) { - vk::DeviceSize size = count * sizeof(T); - - auto result = resource_mgr_->create_buffer( - size, - vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - EXPECT_TRUE(result.has_value()) << "Failed to create buffer for typed_buffer"; - - return typed_buffer(device_->get_handle(), std::move(result.value()), count); - } - - std::unique_ptr resource_mgr_; -}; - -// ============================================================================ -// P0 Priority Tests - Upload/Download Operations -// ============================================================================ - -TEST_F(TypedBufferTest, Upload_SimpleData) { - auto buffer = CreateTypedBuffer(10); - - std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - - EXPECT_NO_THROW({ - buffer.upload(data); - }) << "Uploading data should not throw"; -} - -TEST_F(TypedBufferTest, Download_SimpleData) { - auto buffer = CreateTypedBuffer(10); - - std::vector upload_data = {10, 20, 30, 40, 50}; - buffer.upload(upload_data); - - std::vector download_data(5); - EXPECT_NO_THROW({ - buffer.download(download_data); - }) << "Downloading data should not throw"; -} - -TEST_F(TypedBufferTest, Upload_Download_RoundTrip) { - auto buffer = CreateTypedBuffer(8); - - std::vector original_data = {1.0f, 2.5f, 3.14f, 4.2f, 5.9f, 6.1f, 7.7f, 8.3f}; - - // Upload - buffer.upload(original_data); - - // Download - std::vector retrieved_data(8); - buffer.download(retrieved_data); - - // Verify data matches - ASSERT_EQ(retrieved_data.size(), original_data.size()); - for (size_t i = 0; i < original_data.size(); ++i) { - EXPECT_FLOAT_EQ(retrieved_data[i], original_data[i]) - << "Data mismatch at index " << i; - } -} - -TEST_F(TypedBufferTest, Upload_ExceedsCapacity_Throws) { - auto buffer = CreateTypedBuffer(5); - - // Try to upload more data than buffer capacity - std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - - EXPECT_THROW({ - buffer.upload(data); - }, std::runtime_error) << "Uploading data exceeding capacity should throw"; -} - -// ============================================================================ -// P0 Priority Tests - Type Safety -// ============================================================================ - -TEST_F(TypedBufferTest, TypeSafety_IntBuffer) { - auto buffer = CreateTypedBuffer(100); - - std::vector data(100); - for (int i = 0; i < 100; ++i) { - data[i] = i * 2; - } - - buffer.upload(data); - - std::vector result(100); - buffer.download(result); - - for (int i = 0; i < 100; ++i) { - EXPECT_EQ(result[i], i * 2) - << "Int buffer data mismatch at index " << i; - } -} - -TEST_F(TypedBufferTest, TypeSafety_FloatBuffer) { - auto buffer = CreateTypedBuffer(50); - - std::vector data(50); - for (size_t i = 0; i < 50; ++i) { - data[i] = static_cast(i) * 1.5f; - } - - buffer.upload(data); - - std::vector result(50); - buffer.download(result); - - for (size_t i = 0; i < 50; ++i) { - EXPECT_FLOAT_EQ(result[i], static_cast(i) * 1.5f) - << "Float buffer data mismatch at index " << i; - } -} - -TEST_F(TypedBufferTest, TypeSafety_StructBuffer) { - auto buffer = CreateTypedBuffer(10); - - std::vector vertices(10); - for (size_t i = 0; i < 10; ++i) { - vertices[i] = { - static_cast(i), - static_cast(i + 1), - static_cast(i + 2), - 1.0f, 0.0f, 0.0f - }; - } - - buffer.upload(vertices); - - std::vector result(10); - buffer.download(result); - - for (size_t i = 0; i < 10; ++i) { - EXPECT_FLOAT_EQ(result[i].x, vertices[i].x) - << "Struct buffer x mismatch at index " << i; - EXPECT_FLOAT_EQ(result[i].y, vertices[i].y) - << "Struct buffer y mismatch at index " << i; - EXPECT_FLOAT_EQ(result[i].z, vertices[i].z) - << "Struct buffer z mismatch at index " << i; - EXPECT_FLOAT_EQ(result[i].r, vertices[i].r) - << "Struct buffer r mismatch at index " << i; - } -} - -// ============================================================================ -// P0 Priority Tests - Move Semantics -// ============================================================================ - -TEST_F(TypedBufferTest, MoveSemantics) { - auto buffer1 = CreateTypedBuffer(10); - - std::vector data = {1, 2, 3, 4, 5}; - buffer1.upload(data); - - // Test move constructor - auto buffer2 = std::move(buffer1); - - // Verify moved buffer still works - std::vector result(5); - EXPECT_NO_THROW({ - buffer2.download(result); - }) << "Moved buffer should be usable"; - - EXPECT_EQ(result, data) - << "Moved buffer should contain original data"; - - // Test move assignment - auto buffer3 = CreateTypedBuffer(20); - buffer3 = std::move(buffer2); - - std::vector result2(5); - EXPECT_NO_THROW({ - buffer3.download(result2); - }) << "Move-assigned buffer should be usable"; - - EXPECT_EQ(result2, data) - << "Move-assigned buffer should contain original data"; -} - -// ============================================================================ -// P0 Priority Tests - Size Queries -// ============================================================================ - -TEST_F(TypedBufferTest, Size_Correct) { - const size_t count = 42; - auto buffer = CreateTypedBuffer(count); - - EXPECT_EQ(buffer.size(), count) - << "size() should return element count"; -} - -TEST_F(TypedBufferTest, SizeBytes_Correct) { - const size_t count = 42; - auto buffer = CreateTypedBuffer(count); - - EXPECT_EQ(buffer.size_bytes(), count * sizeof(TestData)) - << "size_bytes() should return total byte size"; -} \ No newline at end of file diff --git a/tests/unit/scheduler/test_compute_scheduler.cpp b/tests/unit/scheduler/test_compute_scheduler.cpp deleted file mode 100644 index fc5411c..0000000 --- a/tests/unit/scheduler/test_compute_scheduler.cpp +++ /dev/null @@ -1,385 +0,0 @@ -#include "utils/vulkan_test_base.h" -#include "utils/shader_loader.h" -#include "vulkan/compute/compute_scheduler.h" -#include "vulkan/pipeline/compute_pipeline.h" -#include "vulkan/resource_manager.h" - -using namespace mirage::render::vulkan; -using namespace mirage::render::vulkan::test; - -class ComputeSchedulerTest : public VulkanGPUTest { -protected: - void SetUp() override { - VulkanGPUTest::SetUp(); - - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available"; - } - - scheduler_ = std::make_unique(); - resource_mgr_ = std::make_unique(*device_, physical_device_, context_->get_instance()); - - // Create descriptor pool for pipeline testing - CreateDescriptorPool(); - } - - void TearDown() override { - // Clean up test buffers - for (auto& buffer : test_buffers_) { - if (resource_mgr_) { - resource_mgr_->destroy_buffer(buffer); - } - } - test_buffers_.clear(); - - if (device_ && descriptor_pool_) { - device_->get_handle().destroyDescriptorPool(descriptor_pool_); - descriptor_pool_ = nullptr; - } - - resource_mgr_.reset(); - scheduler_.reset(); - - VulkanGPUTest::TearDown(); - } - - void CreateDescriptorPool() { - std::array pool_sizes = {{ - {vk::DescriptorType::eStorageBuffer, 10} - }}; - - vk::DescriptorPoolCreateInfo pool_info{}; - pool_info.maxSets = 10; - pool_info.poolSizeCount = static_cast(pool_sizes.size()); - pool_info.pPoolSizes = pool_sizes.data(); - - auto result = device_->get_handle().createDescriptorPool(pool_info); - ASSERT_EQ(result.result, vk::Result::eSuccess) - << "Failed to create descriptor pool"; - - descriptor_pool_ = result.value; - } - - resource_manager::buffer_resource CreateTestBuffer(vk::DeviceSize size) { - auto result = resource_mgr_->create_buffer( - size, - vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - EXPECT_TRUE(result.has_value()) << "Failed to create test buffer"; - - auto buffer = result.value(); - test_buffers_.push_back(buffer); - return buffer; - } - - std::unique_ptr scheduler_; - std::unique_ptr resource_mgr_; - vk::DescriptorPool descriptor_pool_; - std::vector test_buffers_; -}; - -// ============================================================================ -// P0 Priority Tests - Device Management -// ============================================================================ - -TEST_F(ComputeSchedulerTest, AddPrimaryDevice_Success) { - ASSERT_NE(scheduler_, nullptr); - ASSERT_NE(device_, nullptr); - - // Add primary device should not throw - EXPECT_NO_THROW({ - scheduler_->add_primary_device(device_.get()); - }) << "Adding primary device should succeed"; -} - -TEST_F(ComputeSchedulerTest, AddSecondaryDevice_Success) { - ASSERT_NE(scheduler_, nullptr); - ASSERT_NE(device_, nullptr); - - // First add primary device - scheduler_->add_primary_device(device_.get()); - - // Add secondary device should not throw - EXPECT_NO_THROW({ - scheduler_->add_secondary_device(device_.get()); - }) << "Adding secondary device should succeed"; -} - -// ============================================================================ -// P0 Priority Tests - Device Selection -// ============================================================================ - -TEST_F(ComputeSchedulerTest, GetBestDeviceFor_Graphics) { - scheduler_->add_primary_device(device_.get()); - - auto* best_device = scheduler_->get_best_device_for(task_type::UI_RELATED); - - EXPECT_EQ(best_device, device_.get()) - << "Graphics task should route to primary device"; -} - -TEST_F(ComputeSchedulerTest, GetBestDeviceFor_Compute) { - scheduler_->add_primary_device(device_.get()); - - auto* best_device = scheduler_->get_best_device_for(task_type::GENERAL_COMPUTE); - - EXPECT_NE(best_device, nullptr) - << "Compute task should have a valid device"; -} - -TEST_F(ComputeSchedulerTest, GetBestDeviceFor_UI) { - scheduler_->add_primary_device(device_.get()); - - auto* best_device = scheduler_->get_best_device_for(task_type::UI_RELATED); - - EXPECT_EQ(best_device, device_.get()) - << "UI task should route to primary device"; -} - -// ============================================================================ -// P0 Priority Tests - Task Scheduling -// ============================================================================ - -TEST_F(ComputeSchedulerTest, ScheduleTask_Success) { - scheduler_->add_primary_device(device_.get()); - - // Create a simple compute pipeline - const auto& spirv = shader_loader::get_minimal_compute_shader(); - - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto pipeline_result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(pipeline_result.has_value()) - << "Failed to create test pipeline"; - - auto pipeline = std::move(pipeline_result.value()); - - // Allocate descriptor set and bind a buffer - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - ASSERT_TRUE(ds_result.has_value()); - auto descriptor_set = ds_result.value(); - - auto buffer = CreateTestBuffer(1024); - pipeline.bind_buffer(descriptor_set, 0, buffer, vk::DescriptorType::eStorageBuffer); - - // Schedule a task - compute_launch_info launch_info{}; - launch_info.descriptor_sets = {descriptor_set}; - launch_info.group_count_x = 1; - launch_info.group_count_y = 1; - launch_info.group_count_z = 1; - - task_id id = 0; - EXPECT_NO_THROW({ - id = scheduler_->schedule_task(task_type::GENERAL_COMPUTE, pipeline, launch_info); - }) << "Scheduling task should not throw"; - - EXPECT_GT(id, 0) << "Task ID should be valid (non-zero)"; -} - -TEST_F(ComputeSchedulerTest, WaitForTask_Success) { - scheduler_->add_primary_device(device_.get()); - - // Create pipeline - const auto& spirv = shader_loader::get_minimal_compute_shader(); - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto pipeline_result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(pipeline_result.has_value()); - auto pipeline = std::move(pipeline_result.value()); - - // Setup descriptor set - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - ASSERT_TRUE(ds_result.has_value()); - auto descriptor_set = ds_result.value(); - - auto buffer = CreateTestBuffer(1024); - pipeline.bind_buffer(descriptor_set, 0, buffer, vk::DescriptorType::eStorageBuffer); - - // Schedule task - compute_launch_info launch_info{}; - launch_info.descriptor_sets = {descriptor_set}; - launch_info.group_count_x = 1; - - auto id = scheduler_->schedule_task(task_type::GENERAL_COMPUTE, pipeline, launch_info); - ASSERT_GT(id, 0); - - // Wait for task should not throw - EXPECT_NO_THROW({ - scheduler_->wait_for_task(id); - }) << "Waiting for task should succeed"; -} - -TEST_F(ComputeSchedulerTest, IsTaskComplete_BeforeWait) { - scheduler_->add_primary_device(device_.get()); - - // Create and schedule a task - const auto& spirv = shader_loader::get_minimal_compute_shader(); - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto pipeline_result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(pipeline_result.has_value()); - auto pipeline = std::move(pipeline_result.value()); - - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - ASSERT_TRUE(ds_result.has_value()); - auto descriptor_set = ds_result.value(); - - auto buffer = CreateTestBuffer(1024); - pipeline.bind_buffer(descriptor_set, 0, buffer, vk::DescriptorType::eStorageBuffer); - - compute_launch_info launch_info{}; - launch_info.descriptor_sets = {descriptor_set}; - launch_info.group_count_x = 1; - - auto id = scheduler_->schedule_task(task_type::GENERAL_COMPUTE, pipeline, launch_info); - - // Check if complete immediately (may or may not be, depending on GPU speed) - bool is_complete = scheduler_->is_task_complete(id); - - // This test just verifies the method works without crashing - SUCCEED() << "is_task_complete returned: " << is_complete; -} - -TEST_F(ComputeSchedulerTest, IsTaskComplete_AfterWait) { - scheduler_->add_primary_device(device_.get()); - - // Create and schedule a task - const auto& spirv = shader_loader::get_minimal_compute_shader(); - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto pipeline_result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(pipeline_result.has_value()); - auto pipeline = std::move(pipeline_result.value()); - - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - ASSERT_TRUE(ds_result.has_value()); - auto descriptor_set = ds_result.value(); - - auto buffer = CreateTestBuffer(1024); - pipeline.bind_buffer(descriptor_set, 0, buffer, vk::DescriptorType::eStorageBuffer); - - compute_launch_info launch_info{}; - launch_info.descriptor_sets = {descriptor_set}; - launch_info.group_count_x = 1; - - auto id = scheduler_->schedule_task(task_type::GENERAL_COMPUTE, pipeline, launch_info); - - // Wait for completion - scheduler_->wait_for_task(id); - - // Should be complete after wait - EXPECT_TRUE(scheduler_->is_task_complete(id)) - << "Task should be complete after wait"; -} - -// ============================================================================ -// P0 Priority Tests - Multiple Tasks -// ============================================================================ - -TEST_F(ComputeSchedulerTest, MultipleTasks_Concurrent) { - scheduler_->add_primary_device(device_.get()); - - // Create pipeline - const auto& spirv = shader_loader::get_minimal_compute_shader(); - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto pipeline_result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(pipeline_result.has_value()); - auto pipeline = std::move(pipeline_result.value()); - - // Schedule multiple tasks - std::vector task_ids; - const int num_tasks = 3; - - for (int i = 0; i < num_tasks; ++i) { - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - ASSERT_TRUE(ds_result.has_value()); - auto descriptor_set = ds_result.value(); - - auto buffer = CreateTestBuffer(1024); - pipeline.bind_buffer(descriptor_set, 0, buffer, vk::DescriptorType::eStorageBuffer); - - compute_launch_info launch_info{}; - launch_info.descriptor_sets = {descriptor_set}; - launch_info.group_count_x = 1; - - auto id = scheduler_->schedule_task(task_type::GENERAL_COMPUTE, pipeline, launch_info); - task_ids.push_back(id); - } - - EXPECT_EQ(task_ids.size(), num_tasks) - << "All tasks should be scheduled"; - - // Wait for all tasks - for (auto id : task_ids) { - EXPECT_NO_THROW({ - scheduler_->wait_for_task(id); - }) << "Waiting for task " << id << " should succeed"; - } -} - -TEST_F(ComputeSchedulerTest, TaskRouting_ToCorrectDevice) { - scheduler_->add_primary_device(device_.get()); - - // UI tasks should go to primary device - auto* ui_device = scheduler_->get_best_device_for(task_type::UI_RELATED); - EXPECT_EQ(ui_device, device_.get()) - << "UI tasks should route to primary device"; - - // Compute tasks should have a valid device - auto* compute_device = scheduler_->get_best_device_for(task_type::GENERAL_COMPUTE); - EXPECT_NE(compute_device, nullptr) - << "Compute tasks should have a valid device"; -} - -// ============================================================================ -// P0 Priority Tests - Resource Cleanup -// ============================================================================ - -TEST_F(ComputeSchedulerTest, ResourceCleanup) { - { - auto scheduler = std::make_unique(); - scheduler->add_primary_device(device_.get()); - - // Create and schedule a task - const auto& spirv = shader_loader::get_minimal_compute_shader(); - std::vector bindings = { - {0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute} - }; - - auto pipeline_result = compute_pipeline::create_from_spv(*device_, spirv, bindings); - ASSERT_TRUE(pipeline_result.has_value()); - auto pipeline = std::move(pipeline_result.value()); - - auto ds_result = pipeline.allocate_descriptor_set(descriptor_pool_); - ASSERT_TRUE(ds_result.has_value()); - auto descriptor_set = ds_result.value(); - - auto buffer = CreateTestBuffer(1024); - pipeline.bind_buffer(descriptor_set, 0, buffer, vk::DescriptorType::eStorageBuffer); - - compute_launch_info launch_info{}; - launch_info.descriptor_sets = {descriptor_set}; - launch_info.group_count_x = 1; - - auto id = scheduler->schedule_task(task_type::GENERAL_COMPUTE, pipeline, launch_info); - scheduler->wait_for_task(id); - - // Scheduler destructor will be called here - } - - SUCCEED() << "Scheduler cleanup completed without crash"; -} \ No newline at end of file diff --git a/tests/utils/mock_window.cpp b/tests/utils/mock_window.cpp deleted file mode 100644 index e9bba02..0000000 --- a/tests/utils/mock_window.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "mock_window.h" -#include - -namespace mirage::render::vulkan::test { - -auto mock_window::create(const create_info& info) -> std::expected { - // In headless mode, we don't create a real window - if (info.headless) { - return mock_window(info.width, info.height, true, nullptr); - } - - // For non-headless mode, we would need platform-specific window creation - // For now, we'll just create a mock with nullptr handle - // In a real implementation, this would use GLFW or platform APIs - return mock_window(info.width, info.height, false, nullptr); -} - -mock_window::mock_window(uint32_t width, uint32_t height, bool headless, void* native_handle) - : width_(width), height_(height), headless_(headless), native_handle_(native_handle) { -} - -mock_window::~mock_window() { - // Cleanup would happen here - // In headless mode, nothing to clean up - // In window mode, would destroy platform window -} - -mock_window::mock_window(mock_window&& other) noexcept - : width_(other.width_) - , height_(other.height_) - , headless_(other.headless_) - , native_handle_(other.native_handle_) { - other.width_ = 0; - other.height_ = 0; - other.headless_ = false; - other.native_handle_ = nullptr; -} - -mock_window& mock_window::operator=(mock_window&& other) noexcept { - if (this != &other) { - width_ = other.width_; - height_ = other.height_; - headless_ = other.headless_; - native_handle_ = other.native_handle_; - - other.width_ = 0; - other.height_ = 0; - other.headless_ = false; - other.native_handle_ = nullptr; - } - return *this; -} - -auto mock_window::create_surface(vk::Instance instance) -> expected { - if (headless_) { - // In headless mode, we can't create a real surface - // Tests should skip surface-dependent operations when headless - return std::unexpected(vk::Result::eErrorInitializationFailed); - } - - // For non-headless mode, would create platform-specific surface - // This is a simplified implementation for testing - // Real implementation would use vkCreateWin32SurfaceKHR, vkCreateXlibSurfaceKHR, etc. - - // Return an error for now since we don't have real window implementation - return std::unexpected(vk::Result::eErrorFeatureNotPresent); -} - -} // namespace mirage::render::vulkan::test \ No newline at end of file diff --git a/tests/utils/mock_window.h b/tests/utils/mock_window.h deleted file mode 100644 index 9a47b6e..0000000 --- a/tests/utils/mock_window.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include "vulkan/vulkan_common.h" - -namespace mirage::render::vulkan::test { - -/** - * @brief Mock window for testing Vulkan surface creation - * Supports CI mode where no real window is created - */ -class mock_window { -public: - struct create_info { - uint32_t width = 800; - uint32_t height = 600; - const char* title = "Mock Test Window"; - bool headless = false; // Set to true in CI environments - }; - - /** - * @brief Creates a mock window (or headless surface in CI mode) - */ - static auto create(const create_info& info) -> std::expected; - - mock_window() = default; - ~mock_window(); - - // Non-copyable - mock_window(const mock_window&) = delete; - mock_window& operator=(const mock_window&) = delete; - - // Movable - mock_window(mock_window&&) noexcept; - mock_window& operator=(mock_window&&) noexcept; - - /** - * @brief Creates a Vulkan surface for this window - */ - auto create_surface(vk::Instance instance) -> expected; - - /** - * @brief Gets window dimensions - */ - auto get_width() const -> uint32_t { return width_; } - auto get_height() const -> uint32_t { return height_; } - - /** - * @brief Checks if running in headless mode - */ - bool is_headless() const { return headless_; } - -private: - explicit mock_window(uint32_t width, uint32_t height, bool headless, void* native_handle = nullptr); - - uint32_t width_ = 0; - uint32_t height_ = 0; - bool headless_ = false; - void* native_handle_ = nullptr; // Platform-specific window handle (nullptr in headless mode) -}; - -} // namespace mirage::render::vulkan::test \ No newline at end of file diff --git a/tests/utils/shader_loader.cpp b/tests/utils/shader_loader.cpp deleted file mode 100644 index 471e81f..0000000 --- a/tests/utils/shader_loader.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "shader_loader.h" -#include -#include - -namespace mirage::render::vulkan::test { - -auto shader_loader::load_spirv_from_file(const std::filesystem::path& shader_path) - -> std::expected, std::string> { - - std::filesystem::path full_path = shader_path; - - // If path is relative, prepend TEST_SHADER_DIR - if (shader_path.is_relative()) { -#ifdef TEST_SHADER_DIR - full_path = std::filesystem::path(TEST_SHADER_DIR) / shader_path; -#else - full_path = get_shader_dir() / shader_path; -#endif - } - - // Open file in binary mode - std::ifstream file(full_path, std::ios::binary | std::ios::ate); - if (!file.is_open()) { - return std::unexpected("Failed to open shader file: " + full_path.string()); - } - - // Get file size - auto file_size = file.tellg(); - if (file_size % 4 != 0) { - return std::unexpected("Invalid SPIR-V file size (not multiple of 4): " + full_path.string()); - } - - // Read file contents - std::vector spirv_code(file_size / 4); - file.seekg(0); - file.read(reinterpret_cast(spirv_code.data()), file_size); - - if (!file) { - return std::unexpected("Failed to read shader file: " + full_path.string()); - } - - return spirv_code; -} - -auto shader_loader::get_shader_dir() -> std::filesystem::path { -#ifdef TEST_SHADER_DIR - return std::filesystem::path(TEST_SHADER_DIR); -#else - // Fallback: assume tests/shaders relative to executable - return std::filesystem::current_path() / "tests" / "shaders"; -#endif -} - -auto shader_loader::get_minimal_vertex_shader() -> const std::vector& { - // Minimal vertex shader SPIR-V that outputs gl_Position = vec4(0.0, 0.0, 0.0, 1.0) - // This is a placeholder - will be replaced with actual compiled SPIR-V - static const std::vector spirv = { - 0x07230203, 0x00010000, 0x000d0007, 0x00000021, - 0x00000000, 0x00020011, 0x00000001, 0x0006000b, - 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, - 0x00000000, 0x0003000e, 0x00000000, 0x00000001, - 0x0007000f, 0x00000000, 0x00000004, 0x6e69616d, - 0x00000000, 0x0000000d, 0x00000011, 0x00030003, - 0x00000002, 0x000001c2, 0x00090004, 0x415f4c47, - 0x735f4252, 0x72617065, 0x5f657461, 0x64616873, - 0x6f5f7265, 0x63656a62, 0x00007374, 0x00040005, - 0x00000004, 0x6e69616d, 0x00000000, 0x00060005, - 0x0000000b, 0x505f6c67, 0x65567265, 0x78657472, - 0x00000000, 0x00060006, 0x0000000b, 0x00000000, - 0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, - 0x0000000b, 0x00000001, 0x505f6c67, 0x746e696f, - 0x657a6953, 0x00000000, 0x00070006, 0x0000000b, - 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, - 0x0065636e, 0x00070006, 0x0000000b, 0x00000003, - 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, - 0x00030005, 0x0000000d, 0x00000000, 0x00050048, - 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, - 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, - 0x00000001, 0x00050048, 0x0000000b, 0x00000002, - 0x0000000b, 0x00000003, 0x00050048, 0x0000000b, - 0x00000003, 0x0000000b, 0x00000004, 0x00030047, - 0x0000000b, 0x00000002, 0x00020013, 0x00000002, - 0x00030021, 0x00000003, 0x00000002, 0x00030016, - 0x00000006, 0x00000020, 0x00040017, 0x00000007, - 0x00000006, 0x00000004, 0x00040015, 0x00000008, - 0x00000020, 0x00000000, 0x0004002b, 0x00000008, - 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, - 0x00000006, 0x00000009, 0x0006001e, 0x0000000b, - 0x00000007, 0x00000006, 0x0000000a, 0x0000000a, - 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, - 0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, - 0x00040015, 0x0000000e, 0x00000020, 0x00000001, - 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, - 0x0004002b, 0x00000006, 0x00000013, 0x00000000, - 0x0004002b, 0x00000006, 0x00000014, 0x3f800000, - 0x0007002c, 0x00000007, 0x00000015, 0x00000013, - 0x00000013, 0x00000013, 0x00000014, 0x00040020, - 0x00000016, 0x00000003, 0x00000007, 0x00050036, - 0x00000002, 0x00000004, 0x00000000, 0x00000003, - 0x000200f8, 0x00000005, 0x00050041, 0x00000016, - 0x00000017, 0x0000000d, 0x0000000f, 0x0003003e, - 0x00000017, 0x00000015, 0x000100fd, 0x00010038 - }; - return spirv; -} - -auto shader_loader::get_minimal_fragment_shader() -> const std::vector& { - // Minimal fragment shader SPIR-V that outputs a solid color vec4(1.0, 0.0, 0.0, 1.0) - static const std::vector spirv = { - 0x07230203, 0x00010000, 0x000d0007, 0x0000000d, - 0x00000000, 0x00020011, 0x00000001, 0x0006000b, - 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, - 0x00000000, 0x0003000e, 0x00000000, 0x00000001, - 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, - 0x00000000, 0x00000009, 0x00000000, 0x00030010, - 0x00000004, 0x00000007, 0x00030003, 0x00000002, - 0x000001c2, 0x00090004, 0x415f4c47, 0x735f4252, - 0x72617065, 0x5f657461, 0x64616873, 0x6f5f7265, - 0x63656a62, 0x00007374, 0x00040005, 0x00000004, - 0x6e69616d, 0x00000000, 0x00050005, 0x00000009, - 0x4374756f, 0x726f6c6f, 0x00000000, 0x00040047, - 0x00000009, 0x0000001e, 0x00000000, 0x00020013, - 0x00000002, 0x00030021, 0x00000003, 0x00000002, - 0x00030016, 0x00000006, 0x00000020, 0x00040017, - 0x00000007, 0x00000006, 0x00000004, 0x00040020, - 0x00000008, 0x00000003, 0x00000007, 0x0004003b, - 0x00000008, 0x00000009, 0x00000003, 0x0004002b, - 0x00000006, 0x0000000a, 0x3f800000, 0x0004002b, - 0x00000006, 0x0000000b, 0x00000000, 0x0007002c, - 0x00000007, 0x0000000c, 0x0000000a, 0x0000000b, - 0x0000000b, 0x0000000a, 0x00050036, 0x00000002, - 0x00000004, 0x00000000, 0x00000003, 0x000200f8, - 0x00000005, 0x0003003e, 0x00000009, 0x0000000c, - 0x000100fd, 0x00010038 - }; - return spirv; -} - -auto shader_loader::get_minimal_compute_shader() -> const std::vector& { - // Minimal compute shader SPIR-V with local_size(1, 1, 1) - static const std::vector spirv = { - 0x07230203, 0x00010000, 0x000d0007, 0x00000006, - 0x00000000, 0x00020011, 0x00000001, 0x0006000b, - 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, - 0x00000000, 0x0003000e, 0x00000000, 0x00000001, - 0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, - 0x00000000, 0x00000000, 0x00060010, 0x00000004, - 0x00000011, 0x00000001, 0x00000001, 0x00000001, - 0x00030003, 0x00000002, 0x000001c2, 0x00090004, - 0x415f4c47, 0x735f4252, 0x72617065, 0x5f657461, - 0x64616873, 0x6f5f7265, 0x63656a62, 0x00007374, - 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, - 0x00020013, 0x00000002, 0x00030021, 0x00000003, - 0x00000002, 0x00050036, 0x00000002, 0x00000004, - 0x00000000, 0x00000003, 0x000200f8, 0x00000005, - 0x000100fd, 0x00010038 - }; - return spirv; -} - -} // namespace mirage::render::vulkan::test \ No newline at end of file diff --git a/tests/utils/shader_loader.h b/tests/utils/shader_loader.h deleted file mode 100644 index 79c4193..0000000 --- a/tests/utils/shader_loader.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "vulkan/vulkan_common.h" -#include -#include -#include - -namespace mirage::render::vulkan::test { - -/** - * @brief Shader loading utilities for tests - */ -class shader_loader { -public: - /** - * @brief Loads compiled SPIR-V from file - * @param shader_path Path relative to TEST_SHADER_DIR or absolute path - * @return SPIR-V bytecode or error - */ - static auto load_spirv_from_file(const std::filesystem::path& shader_path) - -> std::expected, std::string>; - - /** - * @brief Gets the test shader directory path - */ - static auto get_shader_dir() -> std::filesystem::path; - - /** - * @brief Returns minimal vertex shader SPIR-V (embedded) - * Outputs gl_Position = vec4(0.0, 0.0, 0.0, 1.0) - */ - static auto get_minimal_vertex_shader() -> const std::vector&; - - /** - * @brief Returns minimal fragment shader SPIR-V (embedded) - * Outputs a solid color - */ - static auto get_minimal_fragment_shader() -> const std::vector&; - - /** - * @brief Returns minimal compute shader SPIR-V (embedded) - * local_size = (1, 1, 1), does nothing - */ - static auto get_minimal_compute_shader() -> const std::vector&; -}; - -} // namespace mirage::render::vulkan::test \ No newline at end of file diff --git a/tests/utils/test_helpers.cpp b/tests/utils/test_helpers.cpp deleted file mode 100644 index 842ea7b..0000000 --- a/tests/utils/test_helpers.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "test_helpers.h" - -namespace mirage::render::vulkan::test { -namespace helpers { - -auto create_test_vertex_shader(vk::Device device, const std::vector& spirv_code) - -> expected { - vk::ShaderModuleCreateInfo create_info{}; - create_info.codeSize = spirv_code.size() * sizeof(uint32_t); - create_info.pCode = spirv_code.data(); - - auto result = device.createShaderModule(create_info); - if (result.result != vk::Result::eSuccess) { - return std::unexpected(result.result); - } - return result.value; -} - -auto create_test_fragment_shader(vk::Device device, const std::vector& spirv_code) - -> expected { - return create_test_vertex_shader(device, spirv_code); -} - -auto create_test_compute_shader(vk::Device device, const std::vector& spirv_code) - -> expected { - return create_test_vertex_shader(device, spirv_code); -} - -void destroy_shader_module(vk::Device device, vk::ShaderModule module) { - if (module) { - device.destroyShaderModule(module); - } -} - -bool validate_device_features(vk::PhysicalDevice device, const vk::PhysicalDeviceFeatures& required) { - auto available = device.getFeatures(); - - // Check a few common features as examples - if (required.geometryShader && !available.geometryShader) return false; - if (required.tessellationShader && !available.tessellationShader) return false; - if (required.multiDrawIndirect && !available.multiDrawIndirect) return false; - - return true; -} - -auto find_memory_type(vk::PhysicalDevice physical_device, uint32_t type_filter, - vk::MemoryPropertyFlags properties) -> std::optional { - auto mem_properties = physical_device.getMemoryProperties(); - - for (uint32_t i = 0; i < mem_properties.memoryTypeCount; i++) { - if ((type_filter & (1 << i)) && - (mem_properties.memoryTypes[i].propertyFlags & properties) == properties) { - return i; - } - } - - return std::nullopt; -} - -} // namespace helpers -} // namespace mirage::render::vulkan::test \ No newline at end of file diff --git a/tests/utils/test_helpers.h b/tests/utils/test_helpers.h deleted file mode 100644 index f4fc108..0000000 --- a/tests/utils/test_helpers.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "vulkan/vulkan_common.h" -#include -#include - -namespace mirage::render::vulkan::test { - -/** - * @brief Helper functions for Vulkan testing - */ -namespace helpers { - -/** - * @brief Creates a minimal vertex shader module for testing - */ -auto create_test_vertex_shader(vk::Device device, const std::vector& spirv_code) - -> expected; - -/** - * @brief Creates a minimal fragment shader module for testing - */ -auto create_test_fragment_shader(vk::Device device, const std::vector& spirv_code) - -> expected; - -/** - * @brief Creates a minimal compute shader module for testing - */ -auto create_test_compute_shader(vk::Device device, const std::vector& spirv_code) - -> expected; - -/** - * @brief Destroys a shader module - */ -void destroy_shader_module(vk::Device device, vk::ShaderModule module); - -/** - * @brief Validates that a physical device supports required features - */ -bool validate_device_features(vk::PhysicalDevice device, const vk::PhysicalDeviceFeatures& required); - -/** - * @brief Gets the memory type index for given requirements - */ -auto find_memory_type(vk::PhysicalDevice physical_device, uint32_t type_filter, - vk::MemoryPropertyFlags properties) -> std::optional; - -} // namespace helpers - -} // namespace mirage::render::vulkan::test \ No newline at end of file diff --git a/tests/utils/vulkan_test_base.cpp b/tests/utils/vulkan_test_base.cpp deleted file mode 100644 index 4147fd2..0000000 --- a/tests/utils/vulkan_test_base.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "vulkan_test_base.h" -#include -#include - -namespace mirage::render::vulkan::test { - -void VulkanTestBase::SetUp() { - // Base class setup -} - -void VulkanTestBase::TearDown() { - // Base class teardown -} - -bool VulkanTestBase::HasGPU() { - static bool checked = false; - static bool has_gpu = false; - - if (!checked) { - try { - // Initialize Dynamic Loader before any Vulkan calls - static vk::detail::DynamicLoader dl; - auto vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); - if (!vkGetInstanceProcAddr) { - has_gpu = false; - checked = true; - return has_gpu; - } - - VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); - - // Try to create a minimal Vulkan instance - vk::ApplicationInfo app_info{}; - app_info.apiVersion = VK_API_VERSION_1_3; - - vk::InstanceCreateInfo create_info{}; - create_info.pApplicationInfo = &app_info; - - auto instance_result = vk::createInstance(create_info); - if (instance_result.result == vk::Result::eSuccess) { - auto instance = instance_result.value; - - // Initialize dispatcher with instance - VULKAN_HPP_DEFAULT_DISPATCHER.init(instance); - - auto devices_result = instance.enumeratePhysicalDevices(); - has_gpu = devices_result.result == vk::Result::eSuccess && !devices_result.value.empty(); - instance.destroy(); - } - } catch (...) { - has_gpu = false; - } - checked = true; - } - - return has_gpu; -} - -bool VulkanTestBase::IsCI() { - // Check common CI environment variables - const char* ci_env = std::getenv("CI"); - const char* github_actions = std::getenv("GITHUB_ACTIONS"); - const char* gitlab_ci = std::getenv("GITLAB_CI"); - const char* jenkins = std::getenv("JENKINS_HOME"); - - return (ci_env != nullptr) || (github_actions != nullptr) || - (gitlab_ci != nullptr) || (jenkins != nullptr); -} - -std::string VulkanTestBase::GetTestName() const { - const ::testing::TestInfo* test_info = ::testing::UnitTest::GetInstance()->current_test_info(); - return std::string(test_info->test_suite_name()) + "." + std::string(test_info->name()); -} - -void VulkanGPUTest::SetUp() { - VulkanTestBase::SetUp(); - - // Skip test if no GPU available - if (!HasGPU()) { - GTEST_SKIP() << "No GPU available, skipping test: " << GetTestName(); - return; - } - - // Create Vulkan context - render_context::create_info ctx_info{}; - ctx_info.app_name = "Mirage Vulkan Tests"; - ctx_info.enable_validation = false; // Disable validation in tests for performance - - auto ctx_result = render_context::create(ctx_info); - if (!ctx_result.has_value()) { - GTEST_SKIP() << "Failed to create Vulkan context: " << vk::to_string(ctx_result.error()); - return; - } - context_ = std::make_unique(std::move(ctx_result.value())); - - // Create device manager - device_mgr_ = std::make_unique(context_->get_instance()); - - // Select primary device - auto device_result = device_mgr_->select_primary_device(); - if (!device_result.has_value()) { - GTEST_SKIP() << "Failed to select primary device: " << vk::to_string(device_result.error()); - return; - } - physical_device_ = device_result.value(); - - // Create logical device - auto logical_result = logical_device::create(physical_device_); - if (!logical_result.has_value()) { - GTEST_SKIP() << "Failed to create logical device: " << vk::to_string(logical_result.error()); - return; - } - device_ = std::make_unique(std::move(logical_result.value())); -} - -void VulkanGPUTest::TearDown() { - // Clean up in reverse order - device_.reset(); - device_mgr_.reset(); - context_.reset(); - - VulkanTestBase::TearDown(); -} - -} // namespace mirage::render::vulkan::test \ No newline at end of file diff --git a/tests/utils/vulkan_test_base.h b/tests/utils/vulkan_test_base.h deleted file mode 100644 index e75019d..0000000 --- a/tests/utils/vulkan_test_base.h +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include -#include "vulkan/vulkan_common.h" -#include "vulkan/context.h" -#include "vulkan/device_manager.h" -#include "vulkan/logical_device.h" -#include -#include - -namespace mirage::render::vulkan::test { - -/** - * @brief Base test fixture for Vulkan tests that don't require GPU. - * Provides basic environment detection utilities. - */ -class VulkanTestBase : public ::testing::Test { -protected: - void SetUp() override; - void TearDown() override; - - /** - * @brief Checks if a GPU is available on the system. - * Used to skip tests in CI environments without GPU. - */ - static bool HasGPU(); - - /** - * @brief Checks if running in a CI environment. - */ - static bool IsCI(); - - /** - * @brief Gets a human-readable name for the current test. - */ - std::string GetTestName() const; -}; - -/** - * @brief Test fixture for tests that require GPU access. - * Automatically creates Vulkan context and device. - */ -class VulkanGPUTest : public VulkanTestBase { -protected: - void SetUp() override; - void TearDown() override; - - // Vulkan components (created in SetUp) - std::unique_ptr context_; - std::unique_ptr device_mgr_; - std::unique_ptr device_; - vk::PhysicalDevice physical_device_; -}; - -} // namespace mirage::render::vulkan::test \ No newline at end of file