1187 lines
31 KiB
Markdown
1187 lines
31 KiB
Markdown
# OpenGL 后端测试设计
|
||
|
||
## 1. 概述
|
||
|
||
本文档描述 XCEngine OpenGL 渲染后端的测试框架设计。OpenGL 后端与 D3D12 后端有显著差异,主要体现在:
|
||
|
||
- **窗口依赖**:OpenGL 需要 GLFW 窗口上下文才能执行任何图形操作
|
||
- **对象模型**:使用 GL 句柄(unsigned int)而非 COM 接口
|
||
- **状态管理**:OpenGL 是立即模式渲染,状态通过 GL 函数设置
|
||
- **同步机制**:使用 GLsync 对象进行 GPU/CPU 同步
|
||
|
||
### 1.1 测试目标
|
||
|
||
- 验证 OpenGL 各组件 API 的正确性
|
||
- 确保组件间的协作正常工作
|
||
- 捕获 GL 错误和状态问题
|
||
- 支持持续集成(CI)自动化测试
|
||
|
||
### 1.2 特殊挑战
|
||
|
||
| 挑战 | 说明 | 解决方案 |
|
||
|------|------|----------|
|
||
| 窗口依赖 | OpenGL 需要 GL 上下文 | 使用无头渲染(headless)或 offscreen 渲染 |
|
||
| CI 环境 | 无显示器无法创建窗口 | 使用 OSMesa 或 EGL offscreen 上下文 |
|
||
| GL 状态污染 | 全局 GL 状态影响测试 | 每个测试独立设置状态 + 状态重置 |
|
||
|
||
### 1.3 与 D3D12 测试对比
|
||
|
||
| 特性 | D3D12 测试 | OpenGL 测试 |
|
||
|------|-------------|-------------|
|
||
| 设备创建 | D3D12CreateDevice | GLFW + glfwMakeContextCurrent |
|
||
| 错误检查 | HRESULT 返回值 | glGetError() |
|
||
| 对象类型 | COM 接口 (IUnknown) | GL 句柄 (unsigned int) |
|
||
| 资源清理 | Release() | glDelete*() |
|
||
| 同步原语 | ID3D12Fence | GLsync |
|
||
| 状态管理 | 显式状态对象 | 全局 GL 状态 |
|
||
|
||
## 2. 测试目录结构
|
||
|
||
```
|
||
tests/RHI/OpenGL/
|
||
├── CMakeLists.txt # 测试构建配置
|
||
├── fixtures/
|
||
│ ├── OpenGLTestFixture.h # 基础测试夹具
|
||
│ └── OpenGLTestFixture.cpp # 夹具实现
|
||
├── test_device.cpp # OpenGLDevice 测试
|
||
├── test_buffer.cpp # OpenGLBuffer 测试
|
||
├── test_texture.cpp # OpenGLTexture 测试
|
||
├── test_shader.cpp # OpenGLShader 测试
|
||
├── test_pipeline_state.cpp # OpenGLPipelineState 测试
|
||
├── test_vertex_array.cpp # OpenGLVertexArray 测试
|
||
├── test_command_list.cpp # OpenGLCommandList 测试
|
||
├── test_sampler.cpp # OpenGLSampler 测试
|
||
├── test_fence.cpp # OpenGLFence 测试
|
||
├── test_render_target_view.cpp # OpenGLRenderTargetView 测试
|
||
├── test_depth_stencil_view.cpp # OpenGLDepthStencilView 测试
|
||
└── test_swap_chain.cpp # OpenGLSwapChain 测试
|
||
```
|
||
|
||
## 3. 测试夹具设计
|
||
|
||
### 3.1 基础夹具
|
||
|
||
```cpp
|
||
// fixtures/OpenGLTestFixture.h
|
||
#pragma once
|
||
|
||
#include <gtest/gtest.h>
|
||
#include <GLFW/glfw3.h>
|
||
|
||
namespace XCEngine {
|
||
namespace RHI {
|
||
|
||
class OpenGLTestFixture : public ::testing::Test {
|
||
protected:
|
||
static void SetUpTestSuite();
|
||
static void TearDownTestSuite();
|
||
|
||
void SetUp() override;
|
||
void TearDown() override;
|
||
|
||
GLFWwindow* GetWindow() { return m_window; }
|
||
void MakeContextCurrent();
|
||
void DoneContextCurrent();
|
||
|
||
void ClearGLErrors();
|
||
bool CheckGLError(const char* file, int line);
|
||
|
||
private:
|
||
static GLFWwindow* m_window;
|
||
static bool m_contextInitialized;
|
||
};
|
||
|
||
} // namespace RHI
|
||
} // namespace XCEngine
|
||
```
|
||
|
||
### 3.2 设计决策
|
||
|
||
**为什么使用 GLFW 窗口?**
|
||
|
||
OpenGL 必须有有效的 GL 上下文才能执行。测试框架使用 GLFW 创建窗口并获取 GL 上下文。在 CI 环境中可能需要使用 OSMesa 或 offscreen 渲染。
|
||
|
||
### 3.3 GL 错误检查辅助
|
||
|
||
```cpp
|
||
// fixtures/OpenGLTestFixture.cpp
|
||
#include "OpenGLTestFixture.h"
|
||
|
||
GLFWwindow* OpenGLTestFixture::m_window = nullptr;
|
||
bool OpenGLTestFixture::m_contextInitialized = false;
|
||
|
||
void OpenGLTestFixture::SetUpTestSuite() {
|
||
if (!glfwInit()) {
|
||
GTEST_SKIP() << "Failed to initialize GLFW";
|
||
return;
|
||
}
|
||
|
||
// 创建隐藏窗口用于测试
|
||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
|
||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
|
||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||
|
||
m_window = glfwCreateWindow(640, 480, "OpenGL Tests", nullptr, nullptr);
|
||
if (!m_window) {
|
||
GTEST_SKIP() << "Failed to create GLFW window";
|
||
glfwTerminate();
|
||
return;
|
||
}
|
||
|
||
glfwMakeContextCurrent(m_window);
|
||
m_contextInitialized = true;
|
||
}
|
||
|
||
void OpenGLTestFixture::TearDownTestSuite() {
|
||
if (m_window) {
|
||
glfwDestroyWindow(m_window);
|
||
m_window = nullptr;
|
||
}
|
||
glfwTerminate();
|
||
m_contextInitialized = false;
|
||
}
|
||
|
||
void OpenGLTestFixture::SetUp() {
|
||
if (!m_window || !m_contextInitialized) {
|
||
GTEST_SKIP() << "OpenGL context not available";
|
||
return;
|
||
}
|
||
|
||
glfwMakeContextCurrent(m_window);
|
||
ClearGLErrors();
|
||
}
|
||
|
||
void OpenGLTestFixture::TearDown() {
|
||
// 确保没有未检查的 GL 错误
|
||
GLenum error = glGetError();
|
||
if (error != GL_NO_ERROR) {
|
||
// 记录但不一定失败,因为某些 GL 状态改变会产生预期内的错误
|
||
}
|
||
|
||
// 重置 GL 状态
|
||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||
glUseProgram(0);
|
||
glBindVertexArray(0);
|
||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||
|
||
glDisable(GL_DEPTH_TEST);
|
||
glDisable(GL_STENCIL_TEST);
|
||
glDisable(GL_BLEND);
|
||
glDisable(GL_CULL_FACE);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
}
|
||
|
||
void OpenGLTestFixture::ClearGLErrors() {
|
||
while (glGetError() != GL_NO_ERROR);
|
||
}
|
||
|
||
bool OpenGLTestFixture::CheckGLError(const char* file, int line) {
|
||
GLenum error = glGetError();
|
||
if (error != GL_NO_ERROR) {
|
||
GTEST_MESSAGE_ << "OpenGL Error: " << error
|
||
<< " (" << GetGLErrorString(error) << ")"
|
||
<< " at " << file << ":" << line;
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
const char* OpenGLTestFixture::GetGLErrorString(GLenum error) {
|
||
switch (error) {
|
||
case GL_NO_ERROR: return "GL_NO_ERROR";
|
||
case GL_INVALID_ENUM: return "GL_INVALID_ENUM";
|
||
case GL_INVALID_VALUE: return "GL_INVALID_VALUE";
|
||
case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
|
||
case GL_INVALID_FRAMEBUFFER_OPERATION: return "GL_INVALID_FRAMEBUFFER_OPERATION";
|
||
case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY";
|
||
default: return "Unknown error";
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.4 GL 检查宏
|
||
|
||
```cpp
|
||
// 在头文件中定义检查宏
|
||
#define GL_CLEAR_ERRORS() \
|
||
do { while (glGetError() != GL_NO_ERROR); } while(0)
|
||
|
||
#define GL_CHECK(call) \
|
||
do { \
|
||
call; \
|
||
ASSERT_TRUE(OpenGLTestFixture::CheckGLError(__FILE__, __LINE__)) \
|
||
<< "GL error after " << #call; \
|
||
} while(0)
|
||
|
||
#define GL_EXPECT_SUCCESS(call) \
|
||
do { \
|
||
call; \
|
||
EXPECT_EQ(glGetError(), GL_NO_ERROR) \
|
||
<< "GL error after " << #call; \
|
||
} while(0)
|
||
```
|
||
|
||
bool OpenGLTestFixture::CheckGLError(const char* file, int line) {
|
||
GLenum error = glGetError();
|
||
if (error != GL_NO_ERROR) {
|
||
GTEST_MESSAGE_ << "OpenGL Error: " << error
|
||
<< " at " << file << ":" << line;
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
#define GL_CHECK() ASSERT_TRUE(CheckGLError(__FILE__, __LINE__))
|
||
```
|
||
|
||
## 4. 组件测试详情
|
||
|
||
### 4.1 OpenGLDevice
|
||
|
||
**文件**: `test_device.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `CreateRenderWindow_ValidParams` | 窗口创建成功 |
|
||
| 初始化 | `InitializeWithExistingWindow` | 现有窗口初始化 |
|
||
| 设备信息 | `GetDeviceInfo_ReturnsValid` | 供应商/渲染器信息 |
|
||
| 窗口操作 | `SwapBuffers_ClearsScreen` | 交换缓冲区 |
|
||
| 窗口操作 | `PollEvents_ProcessesInput` | 事件轮询 |
|
||
| 窗口操作 | `ShouldClose_ReturnsBool` | 关闭状态查询 |
|
||
|
||
**测试代码示例**:
|
||
|
||
```cpp
|
||
#include "fixtures/OpenGLTestFixture.h"
|
||
|
||
TEST_F(OpenGLTestFixture, Device_CreateWindow_Success) {
|
||
OpenGLDevice device;
|
||
|
||
bool result = device.CreateRenderWindow(800, 600, "Test Window", false);
|
||
|
||
ASSERT_TRUE(result);
|
||
ASSERT_NE(device.GetWindow(), nullptr);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Device_GetDeviceInfo) {
|
||
OpenGLDevice device;
|
||
device.CreateRenderWindow(800, 600, "Test", false);
|
||
|
||
const auto& info = device.GetDeviceInfo();
|
||
|
||
EXPECT_FALSE(info.vendor.empty());
|
||
EXPECT_FALSE(info.renderer.empty());
|
||
EXPECT_GE(info.majorVersion, 4);
|
||
EXPECT_GE(info.minorVersion, 0);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Device_PollEvents) {
|
||
OpenGLDevice device;
|
||
device.CreateRenderWindow(800, 600, "Test", false);
|
||
|
||
bool result = device.PollEvents();
|
||
EXPECT_TRUE(result);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Device_SwapBuffers) {
|
||
OpenGLDevice device;
|
||
device.CreateRenderWindow(800, 600, "Test", false);
|
||
|
||
device.SwapBuffers();
|
||
|
||
GL_CHECK();
|
||
}
|
||
```
|
||
|
||
### 4.2 OpenGLBuffer
|
||
|
||
**文件**: `test_buffer.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `Initialize_VertexBuffer` | 顶点缓冲创建 |
|
||
| 初始化 | `Initialize_IndexBuffer` | 索引缓冲创建 |
|
||
| 初始化 | `Initialize_UniformBuffer` | Uniform 缓冲创建 |
|
||
| 初始化 | `Initialize_Dynamic` | 动态缓冲创建 |
|
||
| 绑定 | `Bind_Unbind` | 缓冲绑定/解绑 |
|
||
| 绑定 | `BindBase_TargetIndex` | 缓冲绑定到目标索引 |
|
||
| 数据操作 | `Map_Unmap` | 数据映射操作 |
|
||
| 数据操作 | `SetData_UpdatesContent` | 数据更新 |
|
||
|
||
**测试代码示例**:
|
||
|
||
```cpp
|
||
#include "fixtures/OpenGLTestFixture.h"
|
||
#include "OpenGLBuffer.h"
|
||
|
||
TEST_F(OpenGLTestFixture, Buffer_InitializeVertexBuffer) {
|
||
OpenGLBuffer buffer;
|
||
float vertices[] = { 0.0f, 0.0f, 0.5f, 0.5f };
|
||
|
||
bool result = buffer.InitializeVertexBuffer(vertices, sizeof(vertices));
|
||
|
||
ASSERT_TRUE(result);
|
||
EXPECT_NE(buffer.GetID(), 0u);
|
||
EXPECT_EQ(buffer.GetSize(), sizeof(vertices));
|
||
EXPECT_EQ(buffer.GetType(), OpenGLBufferType::Vertex);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Buffer_InitializeIndexBuffer) {
|
||
OpenGLBuffer buffer;
|
||
unsigned int indices[] = { 0, 1, 2 };
|
||
|
||
bool result = buffer.InitializeIndexBuffer(indices, sizeof(indices));
|
||
|
||
ASSERT_TRUE(result);
|
||
EXPECT_NE(buffer.GetID(), 0u);
|
||
EXPECT_EQ(buffer.GetSize(), sizeof(indices));
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Buffer_InitializeUniformBuffer) {
|
||
OpenGLBuffer buffer;
|
||
float data[16] = {};
|
||
|
||
bool result = buffer.Initialize(
|
||
OpenGLBufferType::Uniform,
|
||
sizeof(data),
|
||
data,
|
||
true
|
||
);
|
||
|
||
ASSERT_TRUE(result);
|
||
EXPECT_TRUE(buffer.IsDynamic());
|
||
EXPECT_EQ(buffer.GetType(), OpenGLBufferType::Uniform);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Buffer_BindUnbind) {
|
||
OpenGLBuffer buffer;
|
||
buffer.InitializeVertexBuffer(nullptr, 16);
|
||
|
||
buffer.Bind();
|
||
GLenum boundBuffer = 0;
|
||
glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &boundBuffer);
|
||
EXPECT_EQ(boundBuffer, static_cast<GLint>(buffer.GetID()));
|
||
|
||
buffer.Unbind();
|
||
glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &boundBuffer);
|
||
EXPECT_EQ(boundBuffer, 0);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Buffer_MapUnmap) {
|
||
OpenGLBuffer buffer;
|
||
buffer.Initialize(OpenGLBufferType::CopyRead, 256, nullptr, true);
|
||
|
||
void* mappedData = buffer.Map();
|
||
ASSERT_NE(mappedData, nullptr);
|
||
|
||
memset(mappedData, 0xAB, 256);
|
||
|
||
buffer.Unmap();
|
||
|
||
GL_CHECK();
|
||
}
|
||
```
|
||
|
||
### 4.3 OpenGLTexture
|
||
|
||
**文件**: `test_texture.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `Initialize_2DTexture` | 2D 纹理创建 |
|
||
| 初始化 | `Initialize_3DTexture` | 3D 纹理创建 |
|
||
| 初始化 | `Initialize_CubeMap` | 立方体纹理创建 |
|
||
| 初始化 | `Initialize_FromFile` | 从文件加载纹理 |
|
||
| 绑定 | `Bind_Unbind` | 纹理绑定/解绑 |
|
||
| 绑定 | `BindImage_ReadWrite` | 图像绑定 |
|
||
| 生成 | `GenerateMipmap` | Mipmap 生成 |
|
||
| 参数 | `SetFiltering` | 过滤参数设置 |
|
||
| 参数 | `SetWrapping` | 环绕参数设置 |
|
||
|
||
### 4.4 OpenGLShader
|
||
|
||
**文件**: `test_shader.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 编译 | `Compile_VertexFragment` | 顶点和片段着色器编译 |
|
||
| 编译 | `Compile_WithGeometry` | 几何着色器编译 |
|
||
| 编译 | `Compile_Compute` | 计算着色器编译 |
|
||
| 编译 | `Compile_InvalidSource` | 无效源码编译失败 |
|
||
| 链接 | `Link_ValidProgram` | 程序链接成功 |
|
||
| 链接 | `Link_MissingStage` | 缺失阶段链接失败 |
|
||
| Uniform | `SetInt_SetFloat` | Uniform 设置 |
|
||
| Uniform | `SetVec3_SetMat4` | 向量/矩阵 Uniform |
|
||
|
||
**测试代码示例**:
|
||
|
||
```cpp
|
||
#include "fixtures/OpenGLTestFixture.h"
|
||
#include "OpenGLShader.h"
|
||
|
||
TEST_F(OpenGLTestFixture, Shader_Compile_VertexFragment) {
|
||
const char* vertexSource = R"(
|
||
#version 460 core
|
||
layout(location = 0) in vec3 aPosition;
|
||
void main() {
|
||
gl_Position = vec4(aPosition, 1.0);
|
||
}
|
||
)";
|
||
|
||
const char* fragmentSource = R"(
|
||
#version 460 core
|
||
out vec4 FragColor;
|
||
void main() {
|
||
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||
}
|
||
)";
|
||
|
||
OpenGLShader shader;
|
||
bool result = shader.Compile(vertexSource, fragmentSource);
|
||
|
||
ASSERT_TRUE(result);
|
||
EXPECT_TRUE(shader.IsValid());
|
||
EXPECT_NE(shader.GetID(), 0u);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Shader_SetUniforms) {
|
||
OpenGLShader shader;
|
||
const char* vs = R"(
|
||
#version 460 core
|
||
void main() { gl_Position = vec4(0.0); }
|
||
)";
|
||
const char* fs = R"(
|
||
#version 460 core
|
||
uniform int uIntValue;
|
||
uniform float uFloatValue;
|
||
uniform vec3 uVec3Value;
|
||
uniform mat4 uMat4Value;
|
||
out vec4 FragColor;
|
||
void main() { FragColor = vec4(1.0); }
|
||
)";
|
||
shader.Compile(vs, fs);
|
||
|
||
shader.Use();
|
||
|
||
shader.SetInt("uIntValue", 42);
|
||
shader.SetFloat("uFloatValue", 3.14f);
|
||
shader.SetVec3("uVec3Value", 1.0f, 2.0f, 3.0f);
|
||
|
||
float mat[16] = {};
|
||
mat[0] = 1.0f; mat[5] = 1.0f; mat[10] = 1.0f; mat[15] = 1.0f;
|
||
shader.SetMat4("uMat4Value", mat);
|
||
|
||
GL_CHECK();
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Shader_Use_Unbind) {
|
||
OpenGLShader shader;
|
||
shader.Compile(
|
||
"void main() { gl_Position = vec4(0.0); }",
|
||
"void main() { }"
|
||
);
|
||
|
||
shader.Use();
|
||
|
||
GLint currentProgram = 0;
|
||
glGetIntegerv(GL_CURRENT_PROGRAM, ¤tProgram);
|
||
EXPECT_EQ(currentProgram, static_cast<GLint>(shader.GetID()));
|
||
|
||
shader.Unbind();
|
||
|
||
glGetIntegerv(GL_CURRENT_PROGRAM, ¤tProgram);
|
||
EXPECT_EQ(currentProgram, 0);
|
||
}
|
||
```
|
||
|
||
### 4.5 OpenGLPipelineState
|
||
|
||
**文件**: `test_pipeline_state.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 深度状态 | `SetDepthStencilState` | 深度模板状态 |
|
||
| 深度状态 | `ApplyDepthStencil` | 应用深度状态 |
|
||
| 混合状态 | `SetBlendState` | 混合状态设置 |
|
||
| 混合状态 | `ApplyBlend` | 应用混合状态 |
|
||
| 光栅化 | `SetRasterizerState` | 光栅化状态 |
|
||
| 视口 | `SetViewport` | 视口设置 |
|
||
| 裁剪 | `SetScissor` | 裁剪矩形 |
|
||
| 清除 | `SetClearColor` | 清除颜色 |
|
||
| 清除 | `Clear_ColorDepthStencil` | 清除颜色/深度/模板 |
|
||
|
||
### 4.6 OpenGLVertexArray
|
||
|
||
**文件**: `test_vertex_array.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `Initialize_CreatesVAO` | VAO 创建 |
|
||
| 属性 | `AddVertexBuffer` | 顶点缓冲添加 |
|
||
| 属性 | `SetIndexBuffer` | 索引缓冲设置 |
|
||
| 绑定 | `Bind_Unbind` | VAO 绑定/解绑 |
|
||
| 属性 | `GetIndexCount` | 索引数量 |
|
||
|
||
### 4.7 OpenGLCommandList
|
||
|
||
**文件**: `test_command_list.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 清除 | `Clear_ColorBuffer` | 清除颜色缓冲 |
|
||
| 清除 | `Clear_DepthStencil` | 清除深度/模板 |
|
||
| 管线 | `SetPipelineState` | 设置管线状态 |
|
||
| 顶点 | `SetVertexBuffer` | 设置顶点缓冲 |
|
||
| 索引 | `SetIndexBuffer` | 设置索引缓冲 |
|
||
| 绘制 | `Draw_Triangles` | 绘制三角形 |
|
||
| 绘制 | `DrawIndexed_Indices` | 索引绘制 |
|
||
| 绘制 | `DrawInstanced` | 实例化绘制 |
|
||
| 计算 | `Dispatch_ComputeShader` | 计算着色器分发 |
|
||
| 同步 | `MemoryBarrier` | 内存屏障 |
|
||
| 纹理 | `BindTexture` | 纹理绑定 |
|
||
|
||
### 4.8 OpenGLSampler
|
||
|
||
**文件**: `test_sampler.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `Initialize_DefaultDesc` | 默认采样器创建 |
|
||
| 初始化 | `Initialize_CustomDesc` | 自定义采样器创建 |
|
||
| 绑定 | `Bind_Unbind` | 采样器绑定/解绑 |
|
||
| 参数 | `GetID_ReturnsValid` | 采样器 ID 有效 |
|
||
|
||
### 4.9 OpenGLFence
|
||
|
||
**文件**: `test_fence.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `Initialize_Unsignaled` | 未 signaled 状态创建 |
|
||
| 初始化 | `Initialize_Signaled` | signaled 状态创建 |
|
||
| 同步 | `Signal_SetsValue` | Signal 设置值 |
|
||
| 同步 | `Wait_Blocks` | Wait 阻塞等待 |
|
||
| 状态 | `IsSignaled_ReturnsState` | signaled 状态查询 |
|
||
| 状态 | `GetStatus_ReturnsCorrect` | 状态查询 |
|
||
|
||
**测试代码示例**:
|
||
|
||
```cpp
|
||
#include "fixtures/OpenGLTestFixture.h"
|
||
#include "OpenGLFence.h"
|
||
|
||
TEST_F(OpenGLTestFixture, Fence_Initialize_Unsignaled) {
|
||
OpenGLFence fence;
|
||
|
||
bool result = fence.Initialize(false);
|
||
|
||
ASSERT_TRUE(result);
|
||
EXPECT_EQ(fence.GetStatus(), FenceStatus::Unsignaled);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Fence_Initialize_Signaled) {
|
||
OpenGLFence fence;
|
||
|
||
bool result = fence.Initialize(true);
|
||
|
||
ASSERT_TRUE(result);
|
||
// 初始 signaled 状态可能立即完成
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Fence_Signal_Wait) {
|
||
OpenGLFence fence;
|
||
fence.Initialize(false);
|
||
|
||
fence.Signal(1);
|
||
|
||
// 等待完成
|
||
fence.Wait(1);
|
||
|
||
EXPECT_TRUE(fence.IsSignaled());
|
||
EXPECT_EQ(fence.GetCompletedValue(), 1u);
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Fence_GetStatus) {
|
||
OpenGLFence fence;
|
||
fence.Initialize(false);
|
||
|
||
FenceStatus status = fence.GetStatus();
|
||
EXPECT_TRUE(status == FenceStatus::Signaled ||
|
||
status == FenceStatus::Unsignaled);
|
||
}
|
||
|
||
TEST_F(OpenGLFence, Fence_MultipleSignals) {
|
||
OpenGLFence fence;
|
||
fence.Initialize(false);
|
||
|
||
fence.Signal(1);
|
||
fence.Wait(1);
|
||
|
||
fence.Signal(2);
|
||
fence.Wait(2);
|
||
|
||
EXPECT_EQ(fence.GetCompletedValue(), 2u);
|
||
}
|
||
```
|
||
|
||
### 4.10 OpenGLRenderTargetView
|
||
|
||
**文件**: `test_render_target_view.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `Initialize_Texture2D` | 2D 纹理 RTV 创建 |
|
||
| 初始化 | `Initialize_Cubemap` | 立方体 RTV 创建 |
|
||
| 绑定 | `Bind_Unbind` | RTV 绑定/解绑 |
|
||
| 清除 | `Clear_Color` | 清除颜色 |
|
||
|
||
### 4.11 OpenGLDepthStencilView
|
||
|
||
**文件**: `test_depth_stencil_view.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `Initialize_Texture2D` | 2D 纹理 DSV 创建 |
|
||
| 初始化 | `Initialize_Cubemap` | 立方体 DSV 创建 |
|
||
| 绑定 | `Bind_Unbind` | DSV 绑定/解绑 |
|
||
| 清除 | `ClearDepthStencil` | 清除深度/模板 |
|
||
|
||
### 4.12 OpenGLSwapChain
|
||
|
||
**文件**: `test_swap_chain.cpp`
|
||
|
||
**可测试 API 点**:
|
||
|
||
| 测试类别 | 测试用例 | 验证内容 |
|
||
|----------|----------|----------|
|
||
| 初始化 | `Initialize_Window` | 交换链初始化 |
|
||
| 初始化 | `Initialize_WithSize` | 指定尺寸初始化 |
|
||
| 显示 | `Present_VSync` | 垂直同步显示 |
|
||
| 显示 | `Present_Immediate` | 立即显示 |
|
||
| 调整 | `Resize_ChangesSize` | 调整大小 |
|
||
| 属性 | `GetWidth_GetHeight` | 宽高查询 |
|
||
|
||
## 5. 测试分类
|
||
|
||
### 5.1 单元测试
|
||
|
||
测试单一 API 的基本功能:
|
||
|
||
```cpp
|
||
TEST(OpenGL_Buffer, Initialize_VertexBuffer_ReturnsValidID) {
|
||
OpenGLBuffer buffer;
|
||
float data[] = { 1.0f, 2.0f, 3.0f };
|
||
|
||
bool result = buffer.InitializeVertexBuffer(data, sizeof(data));
|
||
|
||
ASSERT_TRUE(result);
|
||
ASSERT_NE(buffer.GetID(), 0u);
|
||
}
|
||
```
|
||
|
||
### 5.2 集成测试
|
||
|
||
测试多个组件的协作:
|
||
|
||
```cpp
|
||
TEST(OpenGL_Buffer, UploadToTexture_DataIntegrity) {
|
||
// 创建纹理
|
||
OpenGLTexture texture;
|
||
texture.Initialize2D(64, 64, 4, nullptr, false);
|
||
|
||
// 创建缓冲并上传数据
|
||
OpenGLBuffer buffer;
|
||
buffer.Initialize(OpenGLBufferType::CopyRead, 64 * 64 * 4, nullptr, false);
|
||
|
||
// 使用命令列表复制
|
||
OpenGLCommandList cmdList;
|
||
cmdList.BindTexture(GL_TEXTURE_2D, 0, texture.GetID());
|
||
|
||
GL_CHECK();
|
||
}
|
||
```
|
||
|
||
### 5.3 渲染结果测试
|
||
|
||
渲染到帧缓冲并验证像素:
|
||
|
||
```cpp
|
||
TEST(OpenGL_RenderTarget, ClearColor_VerifyPixelValue) {
|
||
// 创建帧缓冲
|
||
OpenGLRenderTargetView rtv;
|
||
rtv.Initialize(0); // 默认帧缓冲
|
||
|
||
// 清除为特定颜色
|
||
rtv.Clear(0.25f, 0.5f, 0.75f, 1.0f);
|
||
|
||
// 读取像素
|
||
std::vector<unsigned char> pixels(4);
|
||
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||
|
||
// 验证
|
||
EXPECT_NEAR(pixels[0], 64, 2); // R
|
||
EXPECT_NEAR(pixels[1], 128, 2); // G
|
||
EXPECT_NEAR(pixels[2], 192, 2); // B
|
||
}
|
||
```
|
||
|
||
## 6. 构建配置
|
||
|
||
### 6.1 CMakeLists.txt
|
||
|
||
```cmake
|
||
cmake_minimum_required(VERSION 3.15)
|
||
|
||
project(OpenGLTests)
|
||
|
||
set(CMAKE_CXX_STANDARD 17)
|
||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||
|
||
# 查找 OpenGL
|
||
find_package(OpenGL REQUIRED)
|
||
|
||
# 查找 GLFW
|
||
find_package(GLFW3 REQUIRED)
|
||
|
||
# Google Test
|
||
include(FetchContent)
|
||
FetchContent_Declare(
|
||
googletest
|
||
GIT_REPOSITORY https://gitee.com/mirrors/googletest.git
|
||
GIT_TAG v1.14.0
|
||
)
|
||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||
FetchContent_MakeAvailable(googletest)
|
||
|
||
enable_testing()
|
||
|
||
# 测试源文件
|
||
set(TEST_SOURCES
|
||
test_device.cpp
|
||
test_buffer.cpp
|
||
test_texture.cpp
|
||
test_shader.cpp
|
||
test_pipeline_state.cpp
|
||
test_vertex_array.cpp
|
||
test_command_list.cpp
|
||
test_sampler.cpp
|
||
test_fence.cpp
|
||
test_render_target_view.cpp
|
||
test_depth_stencil_view.cpp
|
||
test_swap_chain.cpp
|
||
)
|
||
|
||
# 可执行文件
|
||
add_executable(opengl_tests ${TEST_SOURCES})
|
||
|
||
# 链接库
|
||
target_link_libraries(opengl_tests PRIVATE
|
||
OpenGL::GL
|
||
GLFW3::GLFW
|
||
XCEngine
|
||
GTest::gtest
|
||
GTest::gtest_main
|
||
)
|
||
|
||
# 包含目录
|
||
target_include_directories(opengl_tests PRIVATE
|
||
${CMAKE_SOURCE_DIR}/engine/include
|
||
${CMAKE_CURRENT_SOURCE_DIR}/fixtures
|
||
)
|
||
|
||
# 编译定义
|
||
target_compile_definitions(opengl_tests PRIVATE
|
||
GL_GLEXT_PROTOTYPES
|
||
)
|
||
|
||
# 测试资源路径
|
||
target_compile_definitions(opengl_tests PRIVATE
|
||
TEST_RESOURCES_DIR="${CMAKE_SOURCE_DIR}/tests/RHI/OpenGL/Res"
|
||
)
|
||
|
||
add_test(NAME OpenGLTests COMMAND opengl_tests)
|
||
```
|
||
|
||
### 6.2 tests/CMakeLists.txt 更新
|
||
|
||
在 `tests/CMakeLists.txt` 中添加:
|
||
|
||
```cmake
|
||
# OpenGL Tests
|
||
add_subdirectory(RHI/OpenGL)
|
||
```
|
||
|
||
## 7. CI 集成
|
||
|
||
### 7.1 GitHub Actions (Linux)
|
||
|
||
```yaml
|
||
# .github/workflows/opengl-tests.yml
|
||
name: OpenGL Tests
|
||
|
||
on: [push, pull_request]
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y \
|
||
libgl1-mesa-dev \
|
||
libglfw3-dev \
|
||
libglm-dev
|
||
|
||
- name: Configure
|
||
run: cmake -B build -S . -G "Unix Makefiles"
|
||
|
||
- name: Build
|
||
run: cmake --build build --config Debug
|
||
|
||
- name: Run Tests
|
||
run: ctest --test-dir build -C Debug --output-on-failure
|
||
```
|
||
|
||
### 7.2 GitHub Actions (Windows)
|
||
|
||
```yaml
|
||
# .github/workflows/opengl-tests-windows.yml
|
||
name: OpenGL Tests (Windows)
|
||
|
||
on: [push, pull_request]
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: windows-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Configure
|
||
run: cmake -B build -S . -G "Visual Studio 17 2022"
|
||
|
||
- name: Build
|
||
run: cmake --build build --config Debug
|
||
|
||
- name: Run Tests
|
||
run: ctest --test-dir build -C Debug --output-on-failure
|
||
```
|
||
|
||
### 7.3 测试执行流程
|
||
|
||
```bash
|
||
# 1. 配置项目
|
||
cmake -B build -S . -G "Unix Makefiles"
|
||
|
||
# 2. 编译测试
|
||
cmake --build build --config Debug
|
||
|
||
# 3. 运行测试
|
||
./build/tests/RHI/OpenGL/Debug/opengl_tests.exe
|
||
|
||
# 或者使用 CTest
|
||
ctest --test-dir build -C Debug --output-on-failure
|
||
|
||
# 4. 运行特定测试
|
||
./build/tests/RHI/OpenGL/Debug/opengl_tests.exe --gtest_filter=OpenGL_Buffer.*
|
||
|
||
# 5. 详细输出
|
||
./build/tests/RHI/OpenGL/Debug/opengl_tests.exe --gtest_also_run_disabled_tests --gtest_print_time=1
|
||
```
|
||
|
||
### 7.4 测试输出示例
|
||
|
||
```
|
||
[==========] Running 54 tests from 1 test suite.
|
||
[----------] Global test environment set-up.
|
||
[----------] 54 tests from OpenGLTestFixture
|
||
[ RUN ] OpenGLTestFixture.Buffer_InitializeVertexBuffer
|
||
[ OK ] OpenGLTestFixture.Buffer_InitializeVertexBuffer (12 ms)
|
||
[ RUN ] OpenGLTestFixture.Buffer_InitializeIndexBuffer
|
||
[ OK ] OpenGLTestFixture.Buffer_InitializeIndexBuffer (8 ms)
|
||
...
|
||
[----------] Global test environment tear-down
|
||
[==========] 54 tests from 1 test suite ran.
|
||
[ PASSED ] 54 tests.
|
||
```
|
||
|
||
## 8. 测试覆盖矩阵
|
||
|
||
| 组件 | 单元测试 | 集成测试 | 渲染测试 | 性能测试 |
|
||
|------|:--------:|:--------:|:--------:|:--------:|
|
||
| OpenGLDevice | ✓ | - | - | ✓ |
|
||
| OpenGLBuffer | ✓ | ✓ | - | ✓ |
|
||
| OpenGLTexture | ✓ | ✓ | ✓ | ✓ |
|
||
| OpenGLShader | ✓ | - | - | - |
|
||
| OpenGLPipelineState | ✓ | - | - | - |
|
||
| OpenGLVertexArray | ✓ | ✓ | - | - |
|
||
| OpenGLCommandList | ✓ | ✓ | - | - |
|
||
| OpenGLSampler | ✓ | - | - | - |
|
||
| OpenGLFence | ✓ | ✓ | - | - |
|
||
| OpenGLRenderTargetView | ✓ | - | ✓ | - |
|
||
| OpenGLDepthStencilView | ✓ | - | ✓ | - |
|
||
| OpenGLSwapChain | - | - | - | - |
|
||
|
||
## 9. 测试资源文件
|
||
|
||
### 9.1 资源目录结构
|
||
|
||
```
|
||
tests/RHI/OpenGL/
|
||
├── Res/
|
||
│ ├── Shader/
|
||
│ │ ├── simple_vs.glsl # 简单顶点着色器
|
||
│ │ ├── simple_fs.glsl # 简单片段着色器
|
||
│ │ ├── compute_cs.glsl # 计算着色器
|
||
│ │ └── quad_vs.glsl # Quad 顶点着色器
|
||
│ ├── Texture/
|
||
│ │ ├── test_256x256.png # 测试纹理
|
||
│ │ └── cube_pos_x.jpg # 立方体纹理面
|
||
│ └── Data/
|
||
│ ├── triangle_verts.bin # 三角形顶点数据
|
||
│ └── quad_verts.bin # Quad 顶点数据
|
||
└── ...
|
||
```
|
||
|
||
### 9.2 测试着色器示例
|
||
|
||
```glsl
|
||
// Res/Shader/simple_vs.glsl
|
||
#version 460 core
|
||
layout(location = 0) in vec3 aPosition;
|
||
layout(location = 1) in vec3 aNormal;
|
||
layout(location = 2) in vec2 aTexCoord;
|
||
|
||
layout(std140, binding = 0) uniform Camera {
|
||
mat4 uModelViewProjection;
|
||
mat4 uModel;
|
||
mat3 uNormalMatrix;
|
||
};
|
||
|
||
out vec3 vPosition;
|
||
out vec3 vNormal;
|
||
out vec2 vTexCoord;
|
||
|
||
void main() {
|
||
vPosition = (uModel * vec4(aPosition, 1.0)).xyz;
|
||
vNormal = normalize(uNormalMatrix * aNormal);
|
||
vTexCoord = aTexCoord;
|
||
gl_Position = uModelViewProjection * vec4(aPosition, 1.0);
|
||
}
|
||
```
|
||
|
||
```glsl
|
||
// Res/Shader/simple_fs.glsl
|
||
#version 460 core
|
||
in vec3 vPosition;
|
||
in vec3 vNormal;
|
||
in vec2 vTexCoord;
|
||
|
||
uniform vec3 uLightDirection;
|
||
uniform vec3 uLightColor;
|
||
uniform vec3 uAmbientColor;
|
||
uniform sampler2D uTexture;
|
||
|
||
out vec4 FragColor;
|
||
|
||
void main() {
|
||
vec3 normal = normalize(vNormal);
|
||
float diff = max(dot(normal, uLightDirection), 0.0);
|
||
vec3 diffuse = diff * uLightColor;
|
||
vec3 ambient = uAmbientColor;
|
||
|
||
vec4 texColor = texture(uTexture, vTexCoord);
|
||
FragColor = vec4(ambient + diffuse, 1.0) * texColor;
|
||
}
|
||
```
|
||
|
||
### 9.3 在测试中加载资源
|
||
|
||
```cpp
|
||
TEST_F(OpenGLTestFixture, Shader_CompileFromFile) {
|
||
OpenGLShader shader;
|
||
|
||
std::string vertexPath = std::string(TEST_RESOURCES_DIR) + "/Shader/simple_vs.glsl";
|
||
std::string fragmentPath = std::string(TEST_RESOURCES_DIR) + "/Shader/simple_fs.glsl";
|
||
|
||
// 加载文件内容并编译
|
||
// ...
|
||
}
|
||
|
||
TEST_F(OpenGLTestFixture, Texture_LoadFromFile) {
|
||
OpenGLTexture texture;
|
||
|
||
std::string texturePath = std::string(TEST_RESOURCES_DIR) + "/Texture/test_256x256.png";
|
||
|
||
bool result = texture.LoadFromFile(texturePath.c_str());
|
||
|
||
ASSERT_TRUE(result);
|
||
EXPECT_EQ(texture.GetWidth(), 256);
|
||
EXPECT_EQ(texture.GetHeight(), 256);
|
||
}
|
||
```
|
||
|
||
## 9. 实现优先级
|
||
|
||
### Phase 1: 核心基础设施
|
||
|
||
1. **OpenGLDevice** - 窗口和上下文管理
|
||
2. **OpenGLBuffer** - 基础数据缓冲
|
||
3. **OpenGLFence** - 同步基础
|
||
|
||
### Phase 2: 资源管理
|
||
|
||
4. **OpenGLTexture** - 纹理资源
|
||
5. **OpenGLSampler** - 采样器
|
||
|
||
### Phase 3: 渲染管线
|
||
|
||
6. **OpenGLShader** - 着色器编译
|
||
7. **OpenGLPipelineState** - 管线状态
|
||
8. **OpenGLVertexArray** - 顶点数组
|
||
|
||
### Phase 4: 命令与视图
|
||
|
||
9. **OpenGLCommandList** - 命令录制
|
||
10. **OpenGLRenderTargetView** - 渲染目标
|
||
11. **OpenGLDepthStencilView** - 深度模板
|
||
|
||
### Phase 5: 窗口管理
|
||
|
||
12. **OpenGLSwapChain** - 交换链
|
||
|
||
## 10. 特殊考虑
|
||
|
||
### 10.1 无头渲染
|
||
|
||
在 CI 环境中没有显示器,需要使用 offscreen 渲染:
|
||
|
||
```cpp
|
||
// 使用 OSMesa 创建 offscreen 上下文
|
||
#ifdef __linux__
|
||
glfwWindowHint(GLFW_PLATFORM, GLFW_PLATFORM_OSMESA);
|
||
#endif
|
||
|
||
// 或使用 Mesa 的软件渲染llvmpipe
|
||
// 运行测试时设置环境变量:
|
||
// MESA_GL_VERSION_OVERRIDE=4.6 MESA_GALLIUM_DRIVER=llvmpipe
|
||
```
|
||
|
||
### 10.2 GL 状态隔离
|
||
|
||
OpenGL 状态是全局的,测试需要注意:
|
||
|
||
- 每个测试开始时重置 GL 状态
|
||
- 使用独立的 VAO 隔离顶点数组状态
|
||
- 使用独立的 Program 隔离着色器状态
|
||
- 测试结束后清理所有绑定的对象
|
||
|
||
```cpp
|
||
void OpenGLTestFixture::ResetGLState() {
|
||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||
|
||
glUseProgram(0);
|
||
|
||
glBindVertexArray(0);
|
||
|
||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||
|
||
glActiveTexture(GL_TEXTURE0);
|
||
for (int i = 0; i < 16; ++i) {
|
||
glBindTexture(GL_TEXTURE_2D, 0);
|
||
}
|
||
|
||
glDisable(GL_DEPTH_TEST);
|
||
glDisable(GL_STENCIL_TEST);
|
||
glDisable(GL_BLEND);
|
||
glDisable(GL_CULL_FACE);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
|
||
glDepthFunc(GL_LESS);
|
||
glStencilFunc(GL_ALWAYS, 0, 0xFF, 0xFF);
|
||
glBlendFunc(GL_ONE, GL_ZERO);
|
||
}
|
||
```
|
||
|
||
### 10.3 错误处理
|
||
|
||
```cpp
|
||
// 检查所有 GL 调用后的错误
|
||
#define GL_CALL(call) \
|
||
call; \
|
||
CheckGLError(__FILE__, __LINE__, #call)
|
||
|
||
// 期望成功的调用
|
||
#define GL_EXPECT_SUCCESS(call) \
|
||
do { \
|
||
call; \
|
||
EXPECT_EQ(glGetError(), GL_NO_ERROR) \
|
||
<< "GL error after " << #call; \
|
||
} while(0)
|
||
|
||
// 检查特定错误
|
||
#define GL_EXPECT_ERROR(expectedError, call) \
|
||
do { \
|
||
glGetError(); /* clear existing */ \
|
||
call; \
|
||
EXPECT_EQ(glGetError(), expectedError) \
|
||
<< "Expected " << #expectedError << " after " << #call; \
|
||
} while(0)
|
||
```
|
||
|
||
### 10.4 跨平台考虑
|
||
|
||
| 平台 | 特殊处理 |
|
||
|------|----------|
|
||
| Windows | 使用 WGL 创建 GL 上下文 |
|
||
| Linux | 使用 GLX 或 Mesa 软件渲染 |
|
||
| macOS | 使用 Cocoa/NSOpenGL(需要特殊处理) |
|
||
| CI/无头 | 使用 OSMesa 或 EGL offscreen |
|
||
|
||
## 11. 后续改进
|
||
|
||
- [ ] 实现 Phase 1 核心基础设施测试
|
||
- [ ] 实现 Phase 2 资源管理测试
|
||
- [ ] 实现 Phase 3 渲染管线测试
|
||
- [ ] 实现 Phase 4 命令与视图测试
|
||
- [ ] 实现 Phase 5 窗口管理测试
|
||
- [ ] 添加资源泄漏检测工具
|
||
- [ ] 添加性能基准测试
|
||
- [ ] 配置 CI 自动测试
|
||
- [ ] 支持 macOS 测试
|
||
|
||
---
|
||
|
||
**文档版本**:1.1
|
||
**创建日期**:2026年3月17日
|
||
**最后更新**:2026年3月17日
|