ScreenLockDetector/docs/FINAL_SOLUTION.md

9.0 KiB
Raw Permalink Blame History

VulkanWidget 渲染问题最终解决方案

问题描述

VulkanWidget 中绘制的波浪线和彩色球存在严重的渲染问题:

  • 图形超出 widget 范围
  • 图形随时间移动,从各个角落慢慢进入
  • 垂直方向严重拉伸变形(椭圆而非正圆)
  • 圆心位置不固定

根本原因

经过深入调试,发现问题的根本原因是:

Uniform Buffer 结构体内存布局不匹配

C++ 端定义src/vulkanrenderer.h

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,之前的错误版本):

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 布局:

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()

// 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 - 管线创建函数

// 在所有管线创建中添加动态状态
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() 中动态设置:

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()

// 创建 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()

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. 动态状态的优势

  • 窗口大小变化时无需重建管线
  • 性能开销极小
  • 代码更灵活

验证方法

测试用例

创建了测试模式来验证坐标系统:

// 在 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 的内存对齐问题

相关资源

维护建议

  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 对齐问题