OpenGL: Add test documentation for minimal integration test

- Create TEST_SPEC.md with OpenGL test hierarchy and specifications
- Add section and inline comments to main.cpp explaining:
  - Window size calculation with AdjustWindowRect
  - glFinish before screenshot for GPU sync
  - Target frame count warm-up period
  - Shutdown order (reverse of initialization)
This commit is contained in:
2026-03-20 19:56:48 +08:00
parent 5201638bb1
commit 394bec9db6
2 changed files with 266 additions and 1 deletions

View File

@@ -0,0 +1,251 @@
# OpenGL 测试专项规范
本文档是 XCEngine 测试规范的 OpenGL 专项补充。
**前置阅读**: [tests/TEST_SPEC.md](../TEST_SPEC.md) - 通用测试规范
---
## 1. 概述
### 1.1 OpenGL 测试特点
| 特点 | 说明 |
|------|------|
| 平台依赖 | Windows WGL 实现Win32 原生 API |
| 窗口依赖 | 集成测试需要 GUI 窗口 |
| GPU 状态 | 测试间可能有 GPU 状态污染 |
| 无 GLFW | 已移除 GLFW 依赖,使用 Win32 WGL API |
### 1.2 测试层级
| 层级 | 位置 | 执行方式 | 框架 |
|------|------|----------|------|
| 单元测试 | `tests/RHI/OpenGL/unit/` | CTest | Google Test |
| 集成测试 | `tests/RHI/OpenGL/integration/` | CTest + Python | Python wrapper |
---
## 2. 单元测试规范
### 2.1 Fixture 设计
每个测试独立创建 OpenGL 上下文,避免 GPU 状态污染:
```cpp
class OpenGLTestFixture : public ::testing::Test {
protected:
void SetUp() override; // 创建窗口、初始化 OpenGL 上下文
void TearDown() override; // 清理资源、销毁上下文
HWND GetHWND();
HDC GetHDC();
HGLRC GetGLRC();
void MakeCurrent();
};
```
### 2.2 测试前缀对应
| 类名 | 测试前缀 |
|------|---------|
| OpenGLDevice | Device |
| OpenGLSwapChain | SwapChain |
| OpenGLBuffer | Buffer |
| OpenGLTexture | Texture |
| OpenGLShader | Shader |
| OpenGLCommandList | CommandList |
| OpenGLPipelineState | PipelineState |
| OpenGLRenderTargetView | RTV |
| OpenGLDepthStencilView | DSV |
| OpenGLFence | Fence |
| OpenGLSampler | Sampler |
| OpenGLVertexArray | VertexArray |
---
## 3. 集成测试规范
### 3.1 目录结构
每个集成测试独占一个子文件夹,资源相互隔离:
```
integration/
├── CMakeLists.txt # 构建配置
├── run_integration_test.py # 公共测试运行脚本
├── compare_ppm.py # PPM 图像比对脚本
├── minimal/ # 最小化测试
│ ├── main.cpp
│ ├── GT.ppm
│ ├── CMakeLists.txt
│ └── run.bat
└── ... # 其他测试
```
### 3.2 Python Wrapper
**位置**: `tests/RHI/OpenGL/integration/run_integration_test.py`
**职责**:
1. 启动 OpenGL exe
2. 等待进程完成
3. 检查输出文件 (PPM 截图)
4. 调用 `compare_ppm.py` 比对 Golden Image
5. 返回 0(成功)/1(失败)
### 3.3 CTest 注册格式
```cmake
add_test(NAME OpenGL_Minimal_Integration
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:OpenGL_Minimal>/run_integration_test.py
$<TARGET_FILE:OpenGL_Minimal>
minimal.ppm
${CMAKE_CURRENT_SOURCE_DIR}/minimal/GT.ppm
5
WORKING_DIRECTORY $<TARGET_FILE_DIR:OpenGL_Minimal>
)
```
### 3.4 Golden Image 规范
| 属性 | 值 |
|------|-----|
| 格式 | PPM (P6) |
| 命名 | `GT.ppm``GT_<test_name>.ppm` |
| 阈值 | 默认 5% |
| 存储位置 | `tests/RHI/OpenGL/integration/<test_name>/` |
### 3.5 Golden Image 生成流程
1. 在干净硬件环境运行集成测试
2. 截图保存为 `minimal.ppm`
3. 人工验证截图正确性
4. 提交到版本控制作为 GT
### 3.6 当前集成测试
| 测试名 | Golden Image | 状态 |
|--------|-------------|------|
| OpenGL_Minimal_Integration | `minimal/GT.ppm` | ✅ 通过 |
---
## 4. 测试执行
### 4.1 单元测试
```bash
# 方式 1: 使用统一脚本
python scripts/run_tests.py --unit-only
# 方式 2: 直接使用 CTest
cd build/tests/RHI/OpenGL/unit
ctest -C Debug --output-on-failure
```
### 4.2 集成测试
```bash
# 方式 1: 使用统一脚本
python scripts/run_tests.py --integration
# 方式 2: 直接使用 CTest
cd build/tests/RHI/OpenGL/integration
ctest -C Debug --output-on-failure
```
### 4.3 构建和测试
```bash
# 构建
cmake --build . --target OpenGL_Minimal --config Debug
# 运行测试
python scripts/run_tests.py --build
```
---
## 5. CI 集成
### 5.1 GitHub Actions 配置
```yaml
name: OpenGL Tests
on: [push, pull_request]
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build OpenGL Tests
run: cmake --build build --target OpenGL_Minimal --config Debug
- name: Run Unit Tests
run: cd build/tests/RHI/OpenGL/unit && ctest -C Debug --output-on-failure
- name: Run Integration Tests
run: python scripts/run_tests.py --integration
```
### 5.2 CI 模式
`--ci` 模式会跳过需要 GUI 的集成测试:
```bash
python scripts/run_tests.py --ci # 仅运行单元测试
```
---
## 6. 文件结构
```
tests/RHI/OpenGL/
├── CMakeLists.txt
├── TEST_SPEC.md # 本文档 (OpenGL 专项)
├── unit/
│ ├── CMakeLists.txt
│ ├── fixtures/
│ │ ├── OpenGLTestFixture.h
│ │ └── OpenGLTestFixture.cpp
│ └── test_device.cpp
└── integration/
├── CMakeLists.txt
├── run_integration_test.py # 公共脚本
├── compare_ppm.py # 公共脚本
└── minimal/ # 最小化测试
├── main.cpp
├── GT.ppm
├── CMakeLists.txt
└── run.bat
```
---
## 7. 已知问题
### 7.1 窗口尺寸问题
**问题**: `SetWindowPos` 的尺寸参数在 `WS_OVERLAPPEDWINDOW` 风格下是完整窗口尺寸(包含 chrome、边框而不是客户区尺寸。
**修复**: 使用 `AdjustWindowRect` 计算完整尺寸后再传给 `SetWindowPos`
---
## 8. 规范更新记录
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.0 | 2026-03-20 | 初始版本,移除 GLFW 依赖后的重构版本 |
---
**规范版本**: 1.0
**最后更新**: 2026-03-20
**前置文档**: [tests/TEST_SPEC.md](../TEST_SPEC.md)

View File

@@ -77,12 +77,14 @@ LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
// Initialize logger
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().SetMinimumLevel(LogLevel::Debug);
Log("[INFO] OpenGL Integration Test Starting");
// Register window class
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(WNDCLASSEXW);
wc.style = CS_HREDRAW | CS_VREDRAW;
@@ -95,9 +97,11 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
return -1;
}
// Calculate full window size from client area size
RECT rect = { 0, 0, gWidth, gHeight };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
// Create window
HWND hwnd = CreateWindowExW(
0,
L"XCEngine_OpenGL_Test",
@@ -113,6 +117,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
return -1;
}
// Initialize OpenGL device with existing window
OpenGLDevice device;
if (!device.InitializeWithExistingWindow(hwnd)) {
Log("[ERROR] Failed to initialize OpenGL device");
@@ -122,12 +127,15 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
ShowWindow(hwnd, nShowCmd);
UpdateWindow(hwnd);
// Log OpenGL device info
Log("[INFO] OpenGL Device: %S", device.GetDeviceInfo().renderer.c_str());
Log("[INFO] OpenGL Version: %S", device.GetDeviceInfo().version.c_str());
// Create swap chain for rendering
OpenGLSwapChain swapChain;
swapChain.Initialize(hwnd, gWidth, gHeight);
// Main render loop
MSG msg = {};
int frameCount = 0;
const int targetFrameCount = 30;
@@ -140,17 +148,23 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
TranslateMessage(&msg);
DispatchMessageW(&msg);
} else {
// Set viewport and clear color for each frame
glViewport(0, 0, gWidth, gHeight);
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Present the rendered frame
swapChain.Present(0, 0);
frameCount++;
}
}
// Take screenshot after target frame count is reached
// glFinish ensures all OpenGL commands are completed before reading pixels
Log("[INFO] Reached target frame count %d - taking screenshot!", targetFrameCount);
SaveScreenshotPPM("minimal.ppm", gWidth, gHeight);
// Shutdown in reverse order of initialization
swapChain.Shutdown();
device.Shutdown();
Logger::Get().Shutdown();