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:
@@ -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>
|
||||
)
|
||||
75
tests/RHI/OpenGL/integration/compare_ppm.py
Normal file
75
tests/RHI/OpenGL/integration/compare_ppm.py
Normal 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)
|
||||
BIN
tests/RHI/OpenGL/integration/minimal/GT.ppm
Normal file
BIN
tests/RHI/OpenGL/integration/minimal/GT.ppm
Normal file
Binary file not shown.
211
tests/RHI/OpenGL/integration/minimal/main.cpp
Normal file
211
tests/RHI/OpenGL/integration/minimal/main.cpp
Normal 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;
|
||||
}
|
||||
124
tests/RHI/OpenGL/integration/run_integration_test.py
Normal file
124
tests/RHI/OpenGL/integration/run_integration_test.py
Normal 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)
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user