OpenGL: Add minimal integration test and enable integration test framework

- Add OpenGL_Minimal integration test using Win32 native API + glad
- Copy run_integration_test.py and compare_ppm.py from D3D12
- Create minimal/main.cpp with red clear color (matching D3D12)
- Generate GT.ppm golden template for 1280x720 red window
- Add VertexArray_Bind_MultipleAttributes unit test
- Update integration/CMakeLists.txt to build OpenGL_Minimal target
This commit is contained in:
2026-03-20 18:30:38 +08:00
parent 34c04af6cb
commit 460a2477c3
6 changed files with 497 additions and 1 deletions

View File

@@ -8,4 +8,45 @@ find_package(Python3 REQUIRED)
enable_testing()
message(STATUS "OpenGL integration tests placeholder - to be implemented in Phase 5")
set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/tests/OpenGL/package)
add_executable(OpenGL_Minimal
WIN32
minimal/main.cpp
${PACKAGE_DIR}/src/glad.c
)
target_include_directories(OpenGL_Minimal PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/minimal
${ENGINE_ROOT_DIR}/include
${PACKAGE_DIR}/include
)
target_link_libraries(OpenGL_Minimal PRIVATE
opengl32
${PACKAGE_DIR}/lib/glfw3.lib
XCEngine
)
target_compile_definitions(OpenGL_Minimal PRIVATE
UNICODE
_UNICODE
)
add_custom_command(TARGET OpenGL_Minimal POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/run_integration_test.py
$<TARGET_FILE_DIR:OpenGL_Minimal>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/compare_ppm.py
$<TARGET_FILE_DIR:OpenGL_Minimal>/
)
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>
)

View File

@@ -0,0 +1,75 @@
import sys
import os
def read_ppm(filename):
with open(filename, "rb") as f:
header = f.readline()
if header != b"P6\n":
raise ValueError(f"Not a P6 PPM file: {filename}")
while True:
line = f.readline()
if not line.startswith(b"#"):
break
dims = line.split()
width, height = int(dims[0]), int(dims[1])
line = f.readline()
maxval = int(line.strip())
data = f.read()
return width, height, data
def compare_ppm(file1, file2, threshold):
w1, h1, d1 = read_ppm(file1)
w2, h2, d2 = read_ppm(file2)
if w1 != w2 or h1 != h2:
print(f"ERROR: Size mismatch - {file1}: {w1}x{h1}, {file2}: {w2}x{h2}")
return False
total_pixels = w1 * h1
diff_count = 0
for i in range(len(d1)):
diff = abs(d1[i] - d2[i])
if diff > threshold:
diff_count += 1
diff_percent = (diff_count / (total_pixels * 3)) * 100
print(f"Image 1: {file1} ({w1}x{h1})")
print(f"Image 2: {file2} ({w2}x{h2})")
print(f"Threshold: {threshold}")
print(f"Different pixels: {diff_count} / {total_pixels * 3} ({diff_percent:.2f}%)")
if diff_percent <= 1.0:
print("PASS: Images match!")
return True
else:
print("FAIL: Images differ!")
return False
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: python compare_ppm.py <file1.ppm> <file2.ppm> <threshold>")
sys.exit(1)
file1 = sys.argv[1]
file2 = sys.argv[2]
threshold = int(sys.argv[3])
if not os.path.exists(file1):
print(f"ERROR: File not found: {file1}")
sys.exit(1)
if not os.path.exists(file2):
print(f"ERROR: File not found: {file2}")
sys.exit(1)
result = compare_ppm(file1, file2, threshold)
sys.exit(0 if result else 1)

Binary file not shown.

View File

@@ -0,0 +1,211 @@
#include <windows.h>
#include <glad/glad.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "XCEngine/Debug/Logger.h"
#include "XCEngine/Debug/ConsoleLogSink.h"
#include "XCEngine/Containers/String.h"
#pragma comment(lib, "opengl32.lib")
using namespace XCEngine::Debug;
using namespace XCEngine::Containers;
static const int gWidth = 1280;
static const int gHeight = 720;
static HWND gHWND = nullptr;
static HDC gHDC = nullptr;
static HGLRC gHGLRC = nullptr;
void Log(const char* format, ...) {
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
Logger::Get().Debug(LogCategory::Rendering, String(buffer));
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
bool InitOpenGL() {
gHDC = GetDC(gHWND);
if (!gHDC) {
Log("[ERROR] Failed to get DC");
return false;
}
PIXELFORMATDESCRIPTOR pfd = {};
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 32;
pfd.cDepthBits = 24;
pfd.cStencilBits = 8;
pfd.iLayerType = PFD_MAIN_PLANE;
int pixelFormat = ChoosePixelFormat(gHDC, &pfd);
if (!pixelFormat) {
Log("[ERROR] Failed to choose pixel format");
return false;
}
if (!SetPixelFormat(gHDC, pixelFormat, &pfd)) {
Log("[ERROR] Failed to set pixel format");
return false;
}
gHGLRC = wglCreateContext(gHDC);
if (!gHGLRC) {
Log("[ERROR] Failed to create GL context");
return false;
}
if (!wglMakeCurrent(gHDC, gHGLRC)) {
Log("[ERROR] Failed to make GL context current");
return false;
}
if (!gladLoadGL()) {
Log("[ERROR] Failed to load OpenGL functions");
return false;
}
Log("[INFO] OpenGL initialized: %s", glGetString(GL_RENDERER));
Log("[INFO] OpenGL Version: %s", glGetString(GL_VERSION));
glViewport(0, 0, gWidth, gHeight);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
return true;
}
void ShutdownOpenGL() {
if (gHGLRC) {
wglDeleteContext(gHGLRC);
gHGLRC = nullptr;
}
if (gHDC) {
ReleaseDC(gHWND, gHDC);
gHDC = nullptr;
}
}
void SaveScreenshotPPM(const char* filename) {
unsigned char* pixels = (unsigned char*)malloc(gWidth * gHeight * 3);
if (!pixels) {
Log("[ERROR] Failed to allocate pixel buffer");
return;
}
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, gWidth, gHeight, GL_RGB, GL_UNSIGNED_BYTE, pixels);
FILE* f = fopen(filename, "wb");
if (!f) {
Log("[ERROR] Failed to open file for screenshot: %s", filename);
free(pixels);
return;
}
fprintf(f, "P6\n%d %d\n255\n", gWidth, gHeight);
unsigned char* row = (unsigned char*)malloc(gWidth * 3);
for (int y = gHeight - 1; y >= 0; y--) {
memcpy(row, pixels + y * gWidth * 3, gWidth * 3);
fwrite(row, 1, gWidth * 3, f);
}
free(row);
free(pixels);
fclose(f);
Log("[INFO] Screenshot saved to %s", filename);
}
void RenderFrame() {
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
SwapBuffers(gHDC);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().SetMinimumLevel(LogLevel::Debug);
Log("[INFO] OpenGL Integration Test Starting");
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"OpenGLTest";
if (!RegisterClassEx(&wc)) {
MessageBox(NULL, L"Failed to register window class", L"Error", MB_OK);
return -1;
}
RECT rect = { 0, 0, gWidth, gHeight };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
gHWND = CreateWindowEx(0, L"OpenGLTest", L"OpenGL Integration Test",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
NULL, NULL, hInstance, NULL);
if (!gHWND) {
MessageBox(NULL, L"Failed to create window", L"Error", MB_OK);
return -1;
}
if (!InitOpenGL()) {
MessageBox(NULL, L"Failed to initialize OpenGL", L"Error", MB_OK);
return -1;
}
ShowWindow(gHWND, nShowCmd);
UpdateWindow(gHWND);
MSG msg = {};
int frameCount = 0;
const int targetFrameCount = 30;
while (true) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
RenderFrame();
frameCount++;
if (frameCount >= targetFrameCount) {
Log("[INFO] Reached target frame count %d - taking screenshot!", targetFrameCount);
SaveScreenshotPPM("minimal.ppm");
break;
}
}
}
ShutdownOpenGL();
Logger::Get().Shutdown();
Log("[INFO] OpenGL Integration Test Finished");
return 0;
}

View File

@@ -0,0 +1,124 @@
import sys
import os
import subprocess
import time
import shutil
def run_integration_test(exe_path, output_ppm, gt_ppm, threshold, timeout=120):
"""
Run a D3D12 integration test and compare output with golden template.
Args:
exe_path: Path to the test executable
output_ppm: Filename of the output screenshot
gt_ppm: Path to the golden template PPM file
threshold: Pixel difference threshold for comparison
timeout: Maximum time to wait for test completion (seconds)
Returns:
0 on success, non-zero on failure
"""
exe_dir = os.path.dirname(os.path.abspath(exe_path))
output_path = os.path.join(exe_dir, output_ppm)
print(f"[Integration Test] Starting: {exe_path}")
print(f"[Integration Test] Working directory: {exe_dir}")
print(f"[Integration Test] Expected output: {output_path}")
if not os.path.exists(exe_path):
print(f"[Integration Test] ERROR: Executable not found: {exe_path}")
return 1
if not os.path.exists(gt_ppm):
print(f"[Integration Test] ERROR: Golden template not found: {gt_ppm}")
return 1
if os.path.exists(output_path):
print(f"[Integration Test] Removing old output: {output_path}")
os.remove(output_path)
try:
print(f"[Integration Test] Launching process...")
start_time = time.time()
process = subprocess.Popen(
[exe_path],
cwd=exe_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == "nt" else 0,
)
returncode = None
while time.time() - start_time < timeout:
returncode = process.poll()
if returncode is not None:
break
time.sleep(0.5)
if returncode is None:
print(f"[Integration Test] ERROR: Process timed out after {timeout}s")
process.kill()
return 1
elapsed = time.time() - start_time
print(
f"[Integration Test] Process finished in {elapsed:.1f}s with exit code: {returncode}"
)
if returncode != 0:
print(f"[Integration Test] ERROR: Process returned non-zero exit code")
stdout, stderr = process.communicate(timeout=5)
if stdout:
print(
f"[Integration Test] STDOUT:\n{stdout.decode('utf-8', errors='replace')}"
)
if stderr:
print(
f"[Integration Test] STDERR:\n{stderr.decode('utf-8', errors='replace')}"
)
return 1
except Exception as e:
print(f"[Integration Test] ERROR: Failed to run process: {e}")
return 1
if not os.path.exists(output_path):
print(f"[Integration Test] ERROR: Output file not created: {output_path}")
return 1
print(f"[Integration Test] Running image comparison...")
script_dir = os.path.dirname(os.path.abspath(__file__))
compare_script = os.path.join(script_dir, "compare_ppm.py")
try:
result = subprocess.run(
[sys.executable, compare_script, output_path, gt_ppm, str(threshold)],
cwd=exe_dir,
capture_output=True,
text=True,
)
print(result.stdout)
if result.stderr:
print(f"[Integration Test] Comparison STDERR: {result.stderr}")
return result.returncode
except Exception as e:
print(f"[Integration Test] ERROR: Failed to run comparison: {e}")
return 1
if __name__ == "__main__":
if len(sys.argv) != 5:
print(
"Usage: run_integration_test.py <exe_path> <output_ppm> <gt_ppm> <threshold>"
)
sys.exit(1)
exe_path = sys.argv[1]
output_ppm = sys.argv[2]
gt_ppm = sys.argv[3]
threshold = int(sys.argv[4])
exit_code = run_integration_test(exe_path, output_ppm, gt_ppm, threshold)
sys.exit(exit_code)

View File

@@ -68,3 +68,48 @@ TEST_F(OpenGLTestFixture, VertexArray_Bind_Unbind) {
vao.Shutdown();
}
TEST_F(OpenGLTestFixture, VertexArray_Bind_MultipleAttributes) {
OpenGLVertexArray vao;
vao.Initialize();
OpenGLBuffer buffer1;
float positions[] = { 0.0f, 0.0f, 0.5f, 0.5f };
buffer1.InitializeVertexBuffer(positions, sizeof(positions));
OpenGLBuffer buffer2;
float colors[] = { 1.0f, 0.0f, 0.0f, 1.0f };
buffer2.InitializeVertexBuffer(colors, sizeof(colors));
VertexAttribute attrPos;
attrPos.index = 0;
attrPos.count = 2;
attrPos.type = GL_FLOAT;
attrPos.normalized = false;
attrPos.stride = sizeof(float) * 2;
attrPos.offset = 0;
VertexAttribute attrColor;
attrColor.index = 1;
attrColor.count = 4;
attrColor.type = GL_FLOAT;
attrColor.normalized = false;
attrColor.stride = sizeof(float) * 4;
attrColor.offset = 0;
vao.AddVertexBuffer(buffer1.GetID(), attrPos);
vao.AddVertexBuffer(buffer2.GetID(), attrColor);
vao.Bind();
GLint boundVAO = 0;
glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &boundVAO);
EXPECT_EQ(boundVAO, static_cast<GLint>(vao.GetID()));
vao.Unbind();
glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &boundVAO);
EXPECT_EQ(boundVAO, 0);
buffer1.Shutdown();
buffer2.Shutdown();
vao.Shutdown();
}