添加测试着色器功能并优化渲染设置

This commit is contained in:
2026-01-19 11:53:23 +08:00
parent 08aff2e960
commit b710062afd
6 changed files with 61 additions and 252 deletions

View File

@@ -34,10 +34,3 @@ add_subdirectory(third_party/yoga/yoga)
add_subdirectory(src)
add_subdirectory(example)
# add_subdirectory(tests)
# 检查编译器是否是 MSVC
if(MSVC)
# 为所有目标添加 /utf-8 编译选项
add_compile_options(/utf-8)
add_compile_options(/source-charset:utf-8)
endif()

View File

@@ -1,242 +0,0 @@
# Vulkan 队列封装设计方案
## 1. 背景与目标
当前 `src/render/vulkan_device` 在初始化时虽然创建了必要的 Vulkan 队列(图形、计算、传输),但并未将这些队列句柄保存或封装暴露给上层使用。目前的实现中,`transfer_task` 等组件需要使用队列时无法方便地获取。
本设计旨在封装 `vulkan_graphics_queue``vulkan_compute_queue``vulkan_transfer_queue`,明确它们的职责,提供统一的提交和同步接口,并集成到 `vulkan_device` 的初始化流程中。
## 2. 类结构设计
### 2.1 类图概览
```mermaid
classDiagram
class object {
<<mirai::object>>
}
class vulkan_queue {
-vk::Queue queue
-u32 family_index
-u32 queue_index
-vk::Device device
+submit(...)
+wait_idle()
+get_handle()
+get_family_index()
}
class vulkan_graphics_queue {
+submit(command_buffers, wait_semaphores, signal_semaphores, fence)
}
class vulkan_compute_queue {
+submit(command_buffers, wait_semaphores, signal_semaphores, fence)
}
class vulkan_transfer_queue {
+submit(command_buffers, wait_semaphores, signal_semaphores, fence)
}
class vulkan_device {
-graphics_queue: shared_ptr~vulkan_graphics_queue~
-compute_queue: shared_ptr~vulkan_compute_queue~
-transfer_queue: shared_ptr~vulkan_transfer_queue~
+get_graphics_queue()
+get_compute_queue()
+get_transfer_queue()
}
object <|-- vulkan_queue
vulkan_queue <|-- vulkan_graphics_queue
vulkan_queue <|-- vulkan_compute_queue
vulkan_queue <|-- vulkan_transfer_queue
vulkan_device *-- vulkan_graphics_queue
vulkan_device *-- vulkan_compute_queue
vulkan_device *-- vulkan_transfer_queue
```
### 2.2 核心类定义
建议在 `src/render/vulkan_queue.h` 中定义。
#### 基类 `vulkan_queue`
作为所有队列的基类,继承自 `mirai::object`
```cpp
namespace mirai {
class vulkan_queue : public object {
MIRAI_OBJECT_TYPE_INFO(vulkan_queue, object)
public:
vulkan_queue(vk::Device device, vk::Queue queue, u32 family_index, u32 queue_index);
// 基础提交接口
void submit(
const std::vector<vk::CommandBuffer>& command_buffers,
const std::vector<vk::Semaphore>& wait_semaphores = {},
const std::vector<vk::PipelineStageFlags>& wait_stages = {},
const std::vector<vk::Semaphore>& signal_semaphores = {},
vk::Fence fence = nullptr
);
// Vulkan 1.3 Synchronization2 提交接口 (建议支持)
void submit2(
const std::vector<vk::CommandBufferSubmitInfo>& command_buffers,
const std::vector<vk::SemaphoreSubmitInfo>& wait_semaphores = {},
const std::vector<vk::SemaphoreSubmitInfo>& signal_semaphores = {},
vk::Fence fence = nullptr
);
void wait_idle();
[[nodiscard]] vk::Queue get_handle() const noexcept { return queue_; }
[[nodiscard]] u32 get_family_index() const noexcept { return family_index_; }
[[nodiscard]] u32 get_queue_index() const noexcept { return queue_index_; }
protected:
vk::Device device_;
vk::Queue queue_;
u32 family_index_;
u32 queue_index_;
};
}
```
#### 子类实现
子类主要用于类型区分,未来可以针对不同类型的队列添加特定的辅助方法(例如图形队列的呈现操作)。
* **`vulkan_graphics_queue`**: 处理图形渲染命令。
* **`vulkan_compute_queue`**: 处理计算着色器命令。
* **`vulkan_transfer_queue`**: 处理缓冲/图像拷贝命令。
```cpp
namespace mirai {
class vulkan_graphics_queue : public vulkan_queue {
MIRAI_OBJECT_TYPE_INFO(vulkan_graphics_queue, vulkan_queue)
public:
using vulkan_queue::vulkan_queue;
// 未来可添加 present 相关接口
};
class vulkan_compute_queue : public vulkan_queue {
MIRAI_OBJECT_TYPE_INFO(vulkan_compute_queue, vulkan_queue)
public:
using vulkan_queue::vulkan_queue;
};
class vulkan_transfer_queue : public vulkan_queue {
MIRAI_OBJECT_TYPE_INFO(vulkan_transfer_queue, vulkan_queue)
public:
using vulkan_queue::vulkan_queue;
};
}
```
## 3. 集成与初始化
### 3.1 修改 `vulkan_device`
`src/render/vulkan_device.h` 中添加成员变量和访问器。
```cpp
class vulkan_device : public object {
// ... 现有代码 ...
public:
// ...
[[nodiscard]] std::shared_ptr<vulkan_graphics_queue> get_graphics_queue() const noexcept { return graphics_queue_; }
[[nodiscard]] std::shared_ptr<vulkan_compute_queue> get_compute_queue() const noexcept { return compute_queue_; }
[[nodiscard]] std::shared_ptr<vulkan_transfer_queue> get_transfer_queue() const noexcept { return transfer_queue_; }
private:
std::shared_ptr<vulkan_graphics_queue> graphics_queue_;
std::shared_ptr<vulkan_compute_queue> compute_queue_;
std::shared_ptr<vulkan_transfer_queue> transfer_queue_;
// ...
};
```
### 3.2 初始化逻辑
`src/render/vulkan_device.cpp` 的构造函数中,在逻辑设备创建成功后,初始化这些队列对象。
**注意:** `find_queue_families` 可能会返回相同的索引给不同类型的队列(例如图形队列通常也支持计算和传输)。如果是相同的 `family_index`,且我们只创建了一个队列 (`queueCount = 1`),那么这些封装对象将共享同一个底层的 `vk::Queue` 句柄。这在 Vulkan 中是完全合法的,但需要注意线程安全(`vkQueueSubmit` 需要外部同步)。
建议的初始化流程:
1. 获取 `unique_queue_families` 并创建逻辑设备(现有逻辑)。
2.`device_` 获取各个 queue handle。
3. 创建 `vulkan_*_queue` 实例。
```cpp
// 在 vulkan_device 构造函数中:
// ... 逻辑设备创建之后 ...
const auto& indices = config.physical_device_info.queue_families; // 假设 config 传入了 indices 或者重新查询
// 获取 Graphics Queue
if (indices.graphics_family.has_value()) {
u32 family_index = indices.graphics_family.value();
vk::Queue queue = device_.getQueue(family_index, 0);
graphics_queue_ = make_obj<vulkan_graphics_queue>(device_, queue, family_index, 0);
}
// 获取 Compute Queue
if (indices.compute_family.has_value()) {
u32 family_index = indices.compute_family.value();
// 注意:如果 compute family 和 graphics family 相同,这里获得的 queue handle 也是相同的
vk::Queue queue = device_.getQueue(family_index, 0);
compute_queue_ = make_obj<vulkan_compute_queue>(device_, queue, family_index, 0);
}
// 获取 Transfer Queue
if (indices.transfer_family.has_value()) {
u32 family_index = indices.transfer_family.value();
vk::Queue queue = device_.getQueue(family_index, 0);
transfer_queue_ = make_obj<vulkan_transfer_queue>(device_, queue, family_index, 0);
}
```
*注意:目前的 `vulkan_device_config` 并没有包含 `queue_family_indices`,需要从 `physical_device` 重新获取或者修改 `config` 结构体传入。考虑到 `find_queue_families` 已经在 `vulkan_device` 构造函数中调用了一次,可以直接复用其结果。*
## 4. 同步与线程安全
* **队列提交同步**`vkQueueSubmit` 是非线程安全的。如果多个 `vulkan_*_queue` 共享同一个底层 `vk::Queue`,则它们在不同线程并发调用 `submit` 时会发生冲突。
* **解决方案**:可以在 `vulkan_queue` 中添加一个 `std::mutex`。但由于多个 `vulkan_queue` 对象可能共享同一个底层句柄,互斥锁需要是针对底层句柄的。
* **方案 A (简化版)**:假设应用层逻辑保证不会多线程并发访问同一个底层队列(例如只在渲染线程提交)。
* **方案 B (共享锁)**:由于我们设计上允许 `graphics``compute` 是不同的对象但可能指向同一硬件队列,最稳妥的方式是在 `vulkan_device` 中维护一个 `mutex` map或者简单地如果 family index 相同,则应当共享锁。
* **方案 C (即时获取)**`vulkan_device` 内部维护 `vk::Queue``std::mutex` 的映射。
鉴于当前项目处于早期阶段,且 Mirai 引擎可能有特定的线程模型,建议**方案 A** 作为起点,并在 `vulkan_queue` 的文档中明确注明:**如果多个逻辑队列映射到同一个硬件队列,多线程提交需要外部同步。** 或者,如果 `vulkan_queue` 是轻量级封装,我们可以在 `vulkan_device` 这一层控制,确保对于同一个 family index 创建的 `vk::Queue`,只创建一个 `vulkan_queue` 实例并在内部复用,但这样 `get_graphics_queue``get_compute_queue` 可能会返回同一个对象,这会破坏类型系统(不能同时是 `graphics_queue``compute_queue`)。
**修正方案**
保持 `vulkan_graphics_queue` 等类型区分。如果底层队列相同,它们就是指向同一资源的各类“视图”。为了线程安全,可以在 `vulkan_device` 中为每个 unique queue family 创建一个 `std::mutex`,并在构造 `vulkan_queue` 时传入这个 mutex 的指针/引用。
```cpp
class vulkan_queue : public object {
// ...
std::shared_ptr<std::mutex> lock_; // 指向该硬件队列的锁
public:
void submit(...) {
std::lock_guard<std::mutex> lock(*lock_);
queue_.submit(...);
}
};
```
`vulkan_device` 初始化时:
1. 建立 `family_index -> shared_ptr<mutex>` 的映射。
2. 创建 `vulkan_*_queue` 时,根据其 `family_index` 传入对应的 mutex。
这样即使 `graphics_queue``compute_queue` 是不同的对象,只要它们对应同一个硬件队列,就会争夺同一个锁,从而保证线程安全。
## 5. 实现计划
1. 创建 `src/render/vulkan_queue.h``src/render/vulkan_queue.cpp`
2. 实现 `vulkan_queue` 及其子类。
3.`vulkan_device.h` 中引入头文件并添加成员。
4. 修改 `vulkan_device.cpp` 的构造函数,实现队列的获取和封装对象的创建,并处理 mutex 共享逻辑。
5. 修改 `find_queue_families` 的调用逻辑,确保能获取到索引以便初始化。

View File

@@ -31,6 +31,7 @@ namespace mirai {
wgpu_context::instance().update();
update_time();
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 240));
}
shutdown();

View File

@@ -0,0 +1,19 @@
// 顶点着色器 (Vertex Shader)
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
// 简单的三角形顶点数据
var pos = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>(0.5, -0.5)
);
return vec4<f32>(pos[in_vertex_index], 0.0, 1.0);
}
// 片段着色器 (Fragment Shader)
@fragment
fn fs_main() -> @location(0) vec4<f32> {
// 输出红色
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

View File

@@ -2,10 +2,41 @@
#include "core/logger.h"
#include <filesystem>
#include <fstream>
namespace mirai {
void test_shader() {
std::filesystem::path shader_path = R"(F:\Projects\mirai\src\render\test_shader_code.wgsl)";
std::ifstream shader_file(shader_path, std::ios::in);
if (!shader_file.is_open()) {
MIRAI_LOG_ERROR("无法打开测试着色器文件: {}", shader_path.string());
return;
}
std::string shader_code((std::istreambuf_iterator<char>(shader_file)), std::istreambuf_iterator<char>());
MIRAI_LOG_INFO("测试着色器代码:\n{}", shader_code);
wgpu::ShaderModuleWGSLDescriptor wgpu_shader_module_descriptor{};
wgpu_shader_module_descriptor.setDefault();
wgpu_shader_module_descriptor.code = shader_code.c_str();
wgpu::ShaderModuleDescriptor shader_module_descriptor{};
shader_module_descriptor.setDefault();
shader_module_descriptor.label = "Test shader module";
shader_module_descriptor.nextInChain = &wgpu_shader_module_descriptor.chain;
wgpu::ShaderModule shader_module = wgpu_context::instance().get_device().createShaderModule(shader_module_descriptor);
if (!shader_module) {
MIRAI_LOG_ERROR("无法创建测试着色器模块");
return;
}
MIRAI_LOG_INFO("测试着色器模块创建成功");
}
bool wgpu_context::setup() {
// We create a descriptor
wgpu::InstanceDescriptor desc{};
desc.setDefault();
// We create the instance using this descriptor
wgpu_instance_ = wgpu::createInstance(desc);
@@ -29,11 +60,13 @@ namespace mirai {
bool wgpu_context::create_default_device(wgpu::Surface compatible_surface) {
{
wgpu::RequestAdapterOptions options{};
options.setDefault();
options.powerPreference = wgpu::PowerPreference::HighPerformance;
options.compatibleSurface = compatible_surface;
wgpu::raii::Adapter adapter = wgpu_instance_.requestAdapter(options);
wgpu::DeviceDescriptor device_desc{};
device_desc.setDefault();
device_desc.label = "Default device";
device_desc.defaultQueue.label = "Default queue";
device_desc.deviceLostCallback = [](WGPUDeviceLostReason reason, char const* message, void* userdata) {
@@ -49,12 +82,13 @@ namespace mirai {
MIRAI_LOG_ERROR("未捕获的错误 {}: {}", static_cast<int>(type), message);
});
test_shader();
return true;
}
wgpu::CommandEncoder wgpu_context::get_command_encoder() {
wgpu::CommandEncoderDescriptor encoder_desc{};
encoder_desc.setDefault();
encoder_desc.label = "Main command encoder";
return wgpu_device_.createCommandEncoder(encoder_desc);
}

View File

@@ -47,16 +47,18 @@ namespace mirai {
}
}
wgpu::raii::CommandEncoder encoder = wgpu_context::instance().get_command_encoder();
wgpu::raii::Queue queue = wgpu_context::instance().get_queue();
auto [texture, texture_view] = acquire_next_swapchain_texture();
wgpu::raii::Queue queue = wgpu_context::instance().get_queue();
auto [texture, texture_view] = acquire_next_swapchain_texture();
wgpu::RenderPassColorAttachment color_attachment{};
color_attachment.setDefault();
color_attachment.view = texture_view;
color_attachment.loadOp = wgpu::LoadOp::Clear;
color_attachment.storeOp = wgpu::StoreOp::Store;
color_attachment.clearValue = {0.95f, 0.35f, 0.49f, 1.0f};
wgpu::RenderPassDescriptor render_pass_descriptor{};
render_pass_descriptor.setDefault();
render_pass_descriptor.colorAttachmentCount = 1;
render_pass_descriptor.colorAttachments = &color_attachment;
@@ -137,6 +139,7 @@ namespace mirai {
void_result_t window::rebuild_swapchain(bool hdr_enabled, vec2i new_extent) {
wgpu::SurfaceConfiguration wgpu_surface_config{};
wgpu_surface_config.setDefault();
wgpu_surface_config.usage = wgpu::TextureUsage::RenderAttachment;
wgpu_surface_config.device = wgpu_context::instance().get_device();
wgpu_surface_config.format = hdr_enabled ? wgpu::TextureFormat::RGB10A2Unorm : wgpu::TextureFormat::BGRA8Unorm;
@@ -157,6 +160,7 @@ namespace mirai {
return {swapchain_texture, nullptr};
}
wgpu::TextureViewDescriptor view_desc{};
view_desc.setDefault();
view_desc.label = "Surface Texture View";
view_desc.format = wgpuTextureGetFormat(swapchain_texture.texture);
view_desc.dimension = wgpu::TextureViewDimension::_2D;