21 KiB
21 KiB
实例数据自动生成方案设计文档
版本: 1.0 日期: 2025-01 状态: 设计草案 关联文档: instanced_rendering_api_design.md
目录
1. 背景与目标
1.1 背景
用户反馈:实例化渲染的数据结构不应该手动定义,而应该由着色器绑定代码工具自动生成。
当前问题:
- 手动定义实例数据结构容易出错
- 与着色器中的实际布局可能不匹配
- 增加了维护负担
1.2 设计目标
| 目标 | 描述 | 优先级 |
|---|---|---|
| 自动生成 | 从着色器 SPIR-V 自动提取实例数据布局 | 高 |
| 带宽优化 | 根据实际着色器需要生成最小必要字段 | 高 |
| 一致性 | 与现有 xxx_shader_spec 生成流程保持一致 |
高 |
| 早期错误检测 | 在工具编译阶段检测并报告布局错误 | 高 |
| 类型安全 | 生成代码中添加编译期静态检查 | 中 |
2. 现有工具分析
2.1 代码生成流程图
flowchart TB
subgraph 输入
GLSL[着色器源文件<br>.vert.glsl / .frag.glsl]
end
subgraph CMake编译流程
CMAKE[compile_shaders.cmake]
PATHS[shader_paths.txt]
end
subgraph Python工具链
MAIN[tools/main.py<br>主入口]
COMPILER[tools/compiler.py<br>调用 glslc]
PARSER[tools/spirv_parser.py<br>SPIR-V 解析]
TYPEMAP[tools/type_mapping.py<br>类型映射]
CODEGEN[tools/code_generator.py<br>代码生成]
TEMPLATE[tools/templates/<br>Jinja2 模板]
end
subgraph 输出
SPIRV[SPIR-V 二进制]
HEADER[生成的 C++ 头文件<br>xxx_vert_frag.h]
end
GLSL --> CMAKE
CMAKE --> PATHS
PATHS --> MAIN
MAIN --> COMPILER
COMPILER --> SPIRV
SPIRV --> PARSER
PARSER --> TYPEMAP
PARSER --> CODEGEN
TYPEMAP --> CODEGEN
CODEGEN --> TEMPLATE
TEMPLATE --> HEADER
2.2 SPIR-V 反射机制
tools/spirv_parser.py 实现了完整的 SPIR-V 反射:
| 类型 | 解析函数 | 生成的 TypeInfo |
|---|---|---|
| 标量 | OP_TYPE_INT, OP_TYPE_FLOAT |
ScalarTypeInfo |
| 向量 | OP_TYPE_VECTOR |
VectorTypeInfo |
| 矩阵 | OP_TYPE_MATRIX |
MatrixTypeInfo |
| 数组 | OP_TYPE_ARRAY, OP_TYPE_RUNTIME_ARRAY |
ArrayTypeInfo |
| 结构体 | OP_TYPE_STRUCT |
StructTypeInfo |
| 指针 | OP_TYPE_POINTER |
PointerTypeInfo |
2.3 Push Constants 布局规范
当前 Push Constants 布局(128 字节):
┌─────────────────────────────────────────────────────────────────────────────┐
│ Push Constants 内存布局 (128 bytes) │
├─────────────────────────────────────────────────────────────────────────────┤
│ Header [0-15]: scale (vec2) + translate (vec2) - 16 bytes │ ← 系统填充
│ Custom [16-127]: 用户自定义数据 - 112 bytes │ ← 自动生成
└─────────────────────────────────────────────────────────────────────────────┘
3. 设计方案
3.1 方案概述
从专门标记的 Instance SSBO 生成实例数据结构。
优点:
- 无大小限制
- 明确的语义(通过注释显式标记)
- 支持 Runtime Array
- 着色器作者意图明确
数据分类:
| 数据类别 | 来源 | 存储位置 | 特点 |
|---|---|---|---|
| 共享参数 | UBO | Uniform Buffer | 所有实例共享,如渐变结束颜色 |
| 每实例参数 | 标记的 SSBO | Storage Buffer | 每个实例不同,如位置、颜色 |
| 系统变换 | Push Constants Header | Push Constants [0-15] | scale, translate |
3.2 工作流程
flowchart TB
SHADER[着色器源文件] --> GLSLC[glslc 编译]
GLSLC --> SPIRV[SPIR-V 二进制]
SPIRV --> PARSER[spirv_parser.py]
PARSER --> DETECT{检测 @instance_buffer<br>注释标记}
DETECT -->|找到| VALIDATE[验证布局]
DETECT -->|未找到| SKIP[跳过实例数据生成]
VALIDATE -->|验证通过| GENERATE[生成实例数据结构]
VALIDATE -->|验证失败| ERROR[工具报错退出]
GENERATE --> CODEGEN[code_generator.py]
CODEGEN --> HEADER[生成 C++ 头文件]
HEADER --> STATIC[C++ 编译期静态检查<br>二次验证]
4. 着色器语法设计
4.1 显式标记语法
通过特殊注释 @instance_buffer 显式标记实例缓冲:
// @instance_buffer
layout(std430, set = 0, binding = N) readonly buffer SomeBuffer {
SomeData items[];
};
规则:
- 注释必须紧邻 SSBO 声明(在
layout之前) - 标记的 SSBO 必须包含 Runtime Array
- 一个着色器最多标记一个实例缓冲
4.2 必需字段要求
工具会验证标记的实例数据结构包含 rect 字段:
struct InstanceData {
vec4 rect; // 必需:x, y, width, height(偏移必须为 0)
// ... 其他用户定义字段
};
验证规则:
- 第一个成员必须命名为
rect - 类型必须为
vec4 - 偏移必须为 0
4.3 完整着色器示例
#version 450 core
// ============================================================================
// Push Constants(共享变换)
// ============================================================================
layout(push_constant) uniform PushConstants {
vec2 scale; // NDC 缩放(系统填充)
vec2 translate; // NDC 平移(系统填充)
} pc;
// ============================================================================
// 实例数据 SSBO
// ============================================================================
struct GradientInstanceData {
vec4 rect; // 必需:x, y, width, height
vec4 start_color; // 起始颜色(每实例不同)
vec4 transform; // rotation, anchor_x, anchor_y, scale
vec4 custom_params; // 自定义参数
};
// @instance_buffer
layout(std430, set = 0, binding = 0) readonly buffer InstanceBuffer {
GradientInstanceData instances[];
};
// ============================================================================
// 共享参数 UBO
// ============================================================================
layout(set = 0, binding = 1) uniform GradientSharedParams {
vec4 end_color; // 结束颜色(所有实例共享)
float gradient_type; // 渐变类型
float gradient_angle; // 渐变角度
vec2 _padding;
};
// ============================================================================
// 顶点着色器主函数
// ============================================================================
layout(location = 0) out vec2 v_uv;
layout(location = 1) out vec4 v_start_color;
layout(location = 2) flat out uint v_instance_id;
const vec2 QUAD_POSITIONS[4] = vec2[4](
vec2(0.0, 0.0), vec2(1.0, 0.0),
vec2(0.0, 1.0), vec2(1.0, 1.0)
);
const int QUAD_INDICES[6] = int[6](0, 1, 2, 2, 1, 3);
mat2 rotate2D(float angle) {
float c = cos(angle);
float s = sin(angle);
return mat2(c, -s, s, c);
}
void main() {
GradientInstanceData inst = instances[gl_InstanceIndex];
// 解析变换
float rotation = inst.transform.x;
vec2 anchor = inst.transform.yz;
float inst_scale = max(inst.transform.w, 1.0);
// 计算顶点位置
int quad_vertex = QUAD_INDICES[gl_VertexIndex];
vec2 local_pos = QUAD_POSITIONS[quad_vertex];
// 应用变换
vec2 centered = local_pos - anchor;
centered *= inst_scale;
centered = rotate2D(rotation) * centered;
vec2 transformed = (centered + anchor) * inst.rect.zw + inst.rect.xy;
// 输出
v_uv = local_pos;
v_start_color = inst.start_color;
v_instance_id = gl_InstanceIndex;
vec2 ndc = transformed * pc.scale + pc.translate;
gl_Position = vec4(ndc, 0.0, 1.0);
}
5. 错误处理策略
5.1 双层错误检测
采用 工具编译时报错 + C++ 静态检查 的双层策略:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第一层:Python 工具编译时报错(主要) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 时机:着色器编译阶段 │
│ 优点: │
│ - 错误发现最早 │
│ - 错误信息可以精确指向着色器源码位置 │
│ - 阻止生成错误的代码 │
│ 检查内容: │
│ - @instance_buffer 标记存在性 │
│ - rect 字段存在、类型、偏移 │
│ - 结构体 16 字节对齐 │
│ - Runtime Array 存在 │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第二层:C++ 静态检查(防御性) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 时机:C++ 项目编译阶段 │
│ 优点: │
│ - 二次验证,防止工具 bug │
│ - 验证 C++ 类型与 SPIR-V 布局一致 │
│ - 捕获手动修改生成文件导致的问题 │
│ 检查内容: │
│ - sizeof 和 alignof 验证 │
│ - offsetof 验证关键字段偏移 │
└─────────────────────────────────────────────────────────────────────────────┘
5.2 工具编译时错误示例
# spirv_parser.py 中的验证逻辑
def validate_instance_buffer(buffer: BufferInfo, reflection: SPIRVReflection) -> None:
"""验证实例缓冲布局
Raises:
ToolError: 如果验证失败
"""
struct_type = buffer.struct_type
# 检查 1:必须有成员
if not struct_type.members:
raise ToolError(
f"Instance buffer '{buffer.name}' struct has no members. "
f"Expected at least 'rect' field."
)
# 检查 2:第一个成员必须是 rect
first_member = struct_type.members[0]
if first_member.name != "rect":
raise ToolError(
f"Instance buffer '{buffer.name}': first member must be named 'rect', "
f"got '{first_member.name}'. The rect field is required for positioning."
)
# 检查 3:rect 必须是 vec4
rect_type = first_member.resolved_type
if not is_vec4_float(rect_type, reflection.types):
raise ToolError(
f"Instance buffer '{buffer.name}': 'rect' must be vec4 (16 bytes), "
f"got {spirv_type_to_cpp(rect_type, reflection.types)}."
)
# 检查 4:rect 偏移必须为 0
if first_member.offset != 0:
raise ToolError(
f"Instance buffer '{buffer.name}': 'rect' offset must be 0, "
f"got {first_member.offset}."
)
# 检查 5:结构体大小必须是 16 的倍数
total_size = calculate_std430_size(struct_type, reflection.types)
if total_size % 16 != 0:
raise ToolError(
f"Instance buffer '{buffer.name}': struct size must be multiple of 16, "
f"got {total_size} bytes. Add padding to align."
)
错误输出示例:
ERROR: Instance buffer 'InstanceBuffer': first member must be named 'rect', got 'position'.
The rect field is required for positioning.
In: shaders/widget/gradient_instanced.vert.glsl
At: layout(std430, set = 0, binding = 0) readonly buffer InstanceBuffer
5.3 生成代码中的静态检查
// 生成的 C++ 代码
struct alignas(16) gradient_instance_data {
Eigen::Vector4f rect;
Eigen::Vector4f start_color;
Eigen::Vector4f transform;
Eigen::Vector4f custom_params;
};
// ============ 编译期静态验证 ============
// 验证总大小
static_assert(
sizeof(gradient_instance_data) == 64,
"gradient_instance_data: size mismatch with shader layout (expected 64 bytes)"
);
// 验证 16 字节对齐
static_assert(
sizeof(gradient_instance_data) % 16 == 0,
"gradient_instance_data: must be 16-byte aligned for GPU compatibility"
);
// 验证 rect 字段偏移
static_assert(
offsetof(gradient_instance_data, rect) == 0,
"gradient_instance_data: rect must be at offset 0"
);
// 验证对齐要求
static_assert(
alignof(gradient_instance_data) >= 16,
"gradient_instance_data: alignment must be at least 16"
);
5.4 为什么需要双层检测
| 场景 | 工具层捕获 | C++ 静态检查捕获 |
|---|---|---|
| 着色器缺少 rect 字段 | ✓ | ✗ |
| 着色器 rect 类型错误 | ✓ | ✗ |
| 工具生成错误的 C++ 代码 | ✗ | ✓ |
| 手动修改生成文件 | ✗ | ✓ |
| C++ 编译器 padding 与预期不同 | ✗ | ✓ |
6. 实现步骤清单
Phase 1: SPIR-V 解析扩展
| 步骤 | 任务 | 文件 |
|---|---|---|
| 1.1 | 添加 BUILTIN_INSTANCE_INDEX 常量 |
tools/constants.py |
| 1.2 | 实现源码注释解析逻辑(提取 @instance_buffer) |
tools/compiler.py |
| 1.3 | 实现 detect_instance_buffer() 函数 |
tools/spirv_parser.py |
| 1.4 | 实现 validate_instance_buffer() 函数 |
tools/spirv_parser.py |
| 1.5 | 扩展 SPIRVReflection 添加 instance_buffer 字段 |
tools/types.py |
Phase 2: 代码生成扩展
| 步骤 | 任务 | 文件 |
|---|---|---|
| 2.1 | 创建 generate_instance_data_struct() 函数 |
tools/code_generator.py |
| 2.2 | 创建 instance_data.jinja2 模板(含静态检查) |
tools/templates/instance/ |
| 2.3 | 更新 header.jinja2 集成实例数据 |
tools/templates/base/header.jinja2 |
| 2.4 | 更新 shader_spec 添加 instance_type 和 supports_instancing |
tools/templates/base/header.jinja2 |
Phase 3: 类型映射完善
| 步骤 | 任务 | 文件 |
|---|---|---|
| 3.1 | 确保 rect 字段正确对齐 |
tools/type_mapping.py |
| 3.2 | 添加 16 字节边界对齐填充计算 | tools/type_mapping.py |
Phase 4: 集成测试
| 步骤 | 任务 | 文件 |
|---|---|---|
| 4.1 | 创建测试着色器 test_instanced.vert.glsl |
shaders/widget/ |
| 4.2 | 验证生成的 C++ 代码 | 测试脚本 |
| 4.3 | 测试错误情况(缺少 rect、类型错误等) | 测试脚本 |
7. 与现有 shader_spec 的集成
7.1 扩展后的 shader_spec
// 生成的代码示例
// build/generated/shaders/gradient_instanced_vert_frag.h
namespace mirage::shaders {
// ============ 实例数据结构 ============
// 从 @instance_buffer 标记的 SSBO 自动生成
struct alignas(16) gradient_instance_data {
// 必需字段(工具验证)
Eigen::Vector4f rect; // x, y, width, height
// 用户定义字段(从着色器反射)
Eigen::Vector4f start_color; // 起始颜色
Eigen::Vector4f transform; // rotation, anchor_x, anchor_y, scale
Eigen::Vector4f custom_params;
};
// ============ 编译期静态验证 ============
static_assert(sizeof(gradient_instance_data) == 64);
static_assert(sizeof(gradient_instance_data) % 16 == 0);
static_assert(offsetof(gradient_instance_data, rect) == 0);
// ============ 共享参数结构 ============
// 从 UBO GradientSharedParams 生成
struct alignas(16) GradientSharedParams {
Eigen::Vector4f end_color;
float gradient_type;
float gradient_angle;
Eigen::Vector2f _padding;
};
// ============ 着色器规格 ============
template <typename ParamsT = GradientSharedParams>
struct gradient_instanced_shader_spec {
using params_type = ParamsT;
using instance_type = gradient_instance_data;
/// @brief 是否支持实例化渲染
static constexpr bool supports_instancing = true;
static constexpr std::string_view name = "gradient_instanced";
static constexpr uint32_t shader_id = /* hash */;
[[nodiscard]] static constexpr auto get_vertex_spirv() noexcept
-> std::span<const uint32_t> {
return gradient_instanced_vert_spirv;
}
[[nodiscard]] static constexpr auto get_fragment_spirv() noexcept
-> std::span<const uint32_t> {
return gradient_instanced_frag_spirv;
}
[[nodiscard]] static constexpr auto get_bindings() noexcept
-> std::span<const shader_binding_info> {
return gradient_instanced_bindings;
}
/// @brief 获取实例数据大小
[[nodiscard]] static constexpr size_t get_instance_size() noexcept {
return sizeof(instance_type);
}
};
} // namespace mirage::shaders
7.2 用户使用方式
// 用户代码(完全自动化)
#include "generated/shaders/gradient_instanced_vert_frag.h"
using namespace mirage::shaders;
// 直接使用生成的实例数据类型
std::vector<gradient_instance_data> instances;
instances.push_back({
.rect = {100.0f, 100.0f, 200.0f, 150.0f},
.start_color = {1.0f, 0.0f, 0.0f, 1.0f},
.transform = {0.0f, 0.5f, 0.5f, 1.0f}, // 无旋转,中心锚点
.custom_params = {0.0f, 0.0f, 0.0f, 0.0f}
});
// 框架自动处理合批
8. 总结
8.1 设计要点
- 识别规则:通过
@instance_buffer注释显式标记 - 验证策略:工具编译时报错(主要)+ C++ 静态检查(防御性)
- 必需字段:
rect(vec4) 作为第一个成员,偏移为 0 - 对齐要求:结构体大小必须是 16 字节的倍数
8.2 修改文件清单
| 文件 | 修改类型 | 描述 |
|---|---|---|
tools/constants.py |
新增 | BUILTIN_INSTANCE_INDEX 常量 |
tools/compiler.py |
修改 | 添加注释解析逻辑 |
tools/spirv_parser.py |
修改 | 添加实例缓冲检测和验证 |
tools/types.py |
修改 | 扩展 SPIRVReflection |
tools/code_generator.py |
修改 | 添加实例数据结构生成 |
tools/templates/base/header.jinja2 |
修改 | 集成实例数据到输出 |
tools/templates/instance/instance_data.jinja2 |
新增 | 实例数据结构模板 |
8.3 后续工作
本设计方案完成后,需要与 instanced_rendering_api_design.md 中的渲染器实现配合,实现完整的实例化渲染管线。