ScreenLockDetector/docs/FINAL_SOLUTION.md

317 lines
9.0 KiB
Markdown
Raw Permalink Normal View History

2025-11-10 17:01:06 +08:00
# VulkanWidget 渲染问题最终解决方案
## 问题描述
VulkanWidget 中绘制的波浪线和彩色球存在严重的渲染问题:
- 图形超出 widget 范围
- 图形随时间移动,从各个角落慢慢进入
- 垂直方向严重拉伸变形(椭圆而非正圆)
- 圆心位置不固定
## 根本原因
经过深入调试,发现问题的**根本原因**是:
### Uniform Buffer 结构体内存布局不匹配
**C++ 端定义**`src/vulkanrenderer.h`
```cpp
struct UniformBufferObject {
float time; // offset 0
float resolution[2]; // offset 4, 8
float rotation; // offset 12
float wavePhase; // offset 16
float padding[2]; // offset 20, 24
};
```
**Shader 端定义**`shaders/geometry.vert`,之前的错误版本):
```glsl
layout(binding = 0) uniform UniformBufferObject {
float time;
vec2 resolution; // GLSL std140 对齐可能不同!
float rotation;
float wavePhase;
} ubo;
```
**问题**
- GLSL 的 `vec2` 在 std140 布局中的对齐方式与 C++ 的 `float[2]` 可能不同
- 导致 shader 读取的 `resolution` 值错误
- 错误的 resolution 导致坐标转换错误,产生变形和位移
## 完整解决方案
### 1. 修复 Shader UBO 布局
**文件**: `shaders/geometry.vert`
`vec2` 拆分为两个独立的 `float`,并明确指定 `std140` 布局:
```glsl
layout(binding = 0, std140) uniform UniformBufferObject {
float time; // offset 0
float resX; // offset 4
float resY; // offset 8
float rotation; // offset 12
float wavePhase; // offset 16
float padding1; // offset 20
float padding2; // offset 24
} ubo;
void main() {
// 使用 resX 和 resY 而不是 resolution.x 和 resolution.y
float ndcX = (inPosition.x / ubo.resX) * 2.0 - 1.0;
float ndcY = (inPosition.y / ubo.resY) * 2.0 - 1.0;
gl_Position = vec4(ndcX, ndcY, 0.0, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
}
```
**关键点**
- 明确使用 `std140` 布局
- 避免使用 `vec2`,改用两个 `float`
- 确保与 C++ 结构体完全对齐
### 2. 更新所有 Uniform Buffers
**文件**: `src/vulkanrenderer.cpp` - `recordCommandBuffer()`
```cpp
// CRITICAL: 每帧更新所有 uniform buffers
// 因为有 MAX_FRAMES_IN_FLIGHT (通常是2) 个 buffers
// 必须保证所有 buffer 都有最新数据
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
updateUniformBuffer(i);
}
```
**原因**
- Vulkan 使用多个 uniform buffers 来支持并行渲染
- 每个 descriptor set 指向不同的 buffer
- 如果只更新当前帧的 buffer其他 buffer 会有过期数据
### 3. 启用动态视口和裁剪矩形
**文件**: `src/vulkanrenderer.cpp` - 管线创建函数
```cpp
// 在所有管线创建中添加动态状态
VkDynamicState dynamicStates[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR
};
VkPipelineDynamicStateCreateInfo dynamicState = {};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;
// 视口状态不设置静态值
viewportState.pViewports = nullptr;
viewportState.pScissors = nullptr;
// 添加到管线创建信息
pipelineInfo.pDynamicState = &dynamicState;
```
`recordCommandBuffer()` 中动态设置:
```cpp
VkViewport viewport = {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast<float>(m_width);
viewport.height = static_cast<float>(m_height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
VkRect2D scissor = {};
scissor.offset = {0, 0};
scissor.extent = {m_width, m_height};
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
```
### 4. 初始化 Uniform Buffer
**文件**: `src/vulkanrenderer.cpp` - `initialize()`
```cpp
// 创建 uniform buffers 后立即初始化
m_ubo.time = 0.0f;
m_ubo.resolution[0] = static_cast<float>(m_width);
m_ubo.resolution[1] = static_cast<float>(m_height);
m_ubo.rotation = 0.0f;
m_ubo.wavePhase = 0.0f;
// 将初始值写入所有 uniform buffers
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (m_uniformBuffersMapped[i] != nullptr) {
memcpy(m_uniformBuffersMapped[i], &m_ubo, sizeof(m_ubo));
}
}
```
### 5. 在 resize 时更新 UBO
**文件**: `src/vulkanrenderer.cpp` - `resize()`
```cpp
m_width = width;
m_height = height;
// 立即更新 UBO resolution
m_ubo.resolution[0] = static_cast<float>(m_width);
m_ubo.resolution[1] = static_cast<float>(m_height);
// 更新所有 uniform buffers
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (i < m_uniformBuffersMapped.size() && m_uniformBuffersMapped[i] != nullptr) {
memcpy(m_uniformBuffersMapped[i], &m_ubo, sizeof(m_ubo));
}
}
```
## 修改文件清单
### 主要修改
1. **shaders/geometry.vert**
- 使用 `std140` 布局
-`vec2 resolution` 改为 `float resX, resY`
- 添加显式的 padding 字段
2. **src/vulkanrenderer.cpp**
- `initialize()`: 初始化所有 uniform buffers
- `resize()`: 更新 UBO 并写入所有 buffers
- `recordCommandBuffer()`: 每帧更新所有 buffers
- `createBackgroundPipeline()`: 添加动态状态
- `createGeometryPipeline()`: 添加动态状态
- `createLinePipeline()`: 添加动态状态
3. **src/vulkanrenderer.h**
- `drawGeometry()`: 添加 frameCount 参数
## 技术要点
### 1. GLSL std140 布局规则
- `float`: 4 字节对齐
- `vec2`: 8 字节对齐(可能与 C++ `float[2]` 不同)
- `vec3`, `vec4`: 16 字节对齐
- 数组每个元素都是 16 字节对齐
**最佳实践**:避免在 UBO 中使用 `vec2`,使用独立的 `float`
### 2. Vulkan 多缓冲
- `MAX_FRAMES_IN_FLIGHT` 通常为 2 或 3
- 每帧可能使用不同的 uniform buffer
- **必须保证所有 buffers 数据一致**
### 3. 动态状态的优势
- 窗口大小变化时无需重建管线
- 性能开销极小
- 代码更灵活
## 验证方法
### 测试用例
创建了测试模式来验证坐标系统:
```cpp
// 在 4 个角落和中心放置测试圆圈
Position 0: screen(75.6, 42.5) -> NDC(-0.8, -0.8) // 左上
Position 1: screen(680.4, 42.5) -> NDC(0.8, -0.8) // 右上
Position 2: screen(75.6, 382.5) -> NDC(-0.8, 0.8) // 左下
Position 3: screen(680.4, 382.5) -> NDC(0.8, 0.8) // 右下
Position 4: screen(378, 212.5) -> NDC(0, 0) // 中心
```
### 预期结果
修复后应该看到:
- ✅ 8个彩色球在窗口正中心旋转
- ✅ 旋转轨道为正圆半径80
- ✅ 球的半径一致15像素
- ✅ 两条波浪线在窗口70%高度处
- ✅ 所有元素完全在窗口内
- ✅ 调整窗口大小时元素保持正确位置
- ✅ 无变形、无移动、无裁剪
## 调试经验
### 诊断步骤
1. **验证 CPU 端数据**
- 打印 m_width, m_height
- 打印 UBO 结构体内容
- 验证 viewport 和 scissor 设置
2. **验证 GPU 端数据**
- 使用硬编码值替代 UBO
- 如果硬编码值工作 → UBO 传输有问题
- 检查结构体对齐
3. **隔离问题**
- 简化场景如测试模式的5个圆圈
- 逐步添加功能,确定引入问题的代码
### 关键发现
问题表现:
- **垂直拉伸** → resolution.y 值错误
- **随时间移动** → 不同帧使用不同的 resolution
- **从角落进入** → 中心点计算使用错误的 resolution
根本原因:
- Shader 读取的 `ubo.resolution` 与 CPU 写入的值不匹配
- 由于 `vec2` 的内存对齐问题
## 相关资源
- [Vulkan Specification - Uniform Buffer Layout](https://www.khronos.org/registry/vulkan/specs/1.3/html/vkspec.html#interfaces-resources-layout)
- [GLSL std140 Layout Rules](https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)#Memory_layout)
- [Vulkan Dynamic State](https://www.khronos.org/registry/vulkan/specs/1.3/html/vkspec.html#pipelines-dynamic-state)
## 维护建议
1. **UBO 结构体设计**
- 避免使用 `vec2`, `vec3`
- 优先使用独立的 `float`
- 明确添加 padding 到 16 字节边界
- 在 C++ 和 GLSL 中保持相同的字段顺序和对齐
2. **调试工具**
- 保留测试模式代码(可通过宏开关)
- 添加 UBO 内容验证函数
- 使用 RenderDoc 等工具检查 GPU 状态
3. **代码审查要点**
- 检查所有 uniform buffer 是否都被更新
- 验证 descriptor set 绑定的 buffer index
- 确认 viewport 和 scissor 与渲染表面匹配
## 总结
这个问题的修复展示了 Vulkan 开发中的几个重要教训:
1. **内存对齐至关重要**CPU 和 GPU 之间的数据传输必须精确匹配
2. **多缓冲需要同步**:所有 in-flight 的资源都必须保持一致
3. **动态状态很有用**:避免频繁重建管线
4. **系统化调试**:从简单场景开始,逐步定位问题
最终解决方案简洁且高效,确保了渲染的正确性和性能。
---
**修复完成日期**: 2024
**问题持续时间**: 多次迭代
**关键突破**: 使用硬编码值测试发现 UBO 传输问题
**最终原因**: GLSL vec2 对齐问题