diff --git a/.github/workflows/retrace.yml b/.github/workflows/retrace.yml index b2c660c2..fb55a48f 100644 --- a/.github/workflows/retrace.yml +++ b/.github/workflows/retrace.yml @@ -42,7 +42,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers + sudo apt-get install -y clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers xvfb - name: Show installed toolchain run: | @@ -64,17 +64,22 @@ jobs: working-directory: ${{env.TEST_ROOT}}/build-retrace run: cmake --build . --target mobilegl_trace_replay - - name: OpenRA retrace + - name: Retrace and validate Espryt (DirectGLES) working-directory: ${{env.TEST_ROOT}}/build-retrace run: ctest -V -R MobileGLTraceReplay.OpenRA.DirectGLES - - name: Upload OpenRA actual + - name: Retrace and validate Magma (DirectVulkan) + working-directory: ${{env.TEST_ROOT}}/build-retrace + run: xvfb-run -a ctest -V -R MobileGLTraceReplay.OpenRA.DirectVulkan + + - name: Upload actual image if: always() uses: actions/upload-artifact@v4 with: name: OpenRA-retrace-actual path: | - build-retrace/tools/trace_replay/openra/output/actual.png - build-retrace/tools/trace_replay/openra/output/result.json - build-retrace/tools/trace_replay/openra/output/retrace.log + build-retrace/tools/trace_replay/openra/actual-images/openra-DirectGLES-actual.png + build-retrace/tools/trace_replay/openra/actual-images/openra-DirectVulkan-actual.png + build-retrace/tools/trace_replay/openra/actual-images/*.json + build-retrace/tools/trace_replay/openra/actual-images/*.log if-no-files-found: warn diff --git a/tools/trace_replay/CMakeLists.txt b/tools/trace_replay/CMakeLists.txt index fa3a5aac..b9b287b4 100644 --- a/tools/trace_replay/CMakeLists.txt +++ b/tools/trace_replay/CMakeLists.txt @@ -257,14 +257,21 @@ target_link_libraries(mobilegl_trace_replay PRIVATE add_dependencies(mobilegl_trace_replay MobileGL) enable_testing() -add_test( - NAME MobileGLTraceReplay.OpenRA.DirectGLES - COMMAND "${CMAKE_COMMAND}" - -DTRACE_REPLAY_EXE=$ - -DMOBILEGL_LIBRARY=$ - -DOPENRA_TRACE_ARCHIVE=${MOBILEGL_TRACE_ROOT}/fixtures/openra.tgz - -DOPENRA_GOLDEN=${MOBILEGL_TRACE_ROOT}/fixtures/openra.0000031249.png - -DOPENRA_OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/openra - -P ${MOBILEGL_TRACE_ROOT}/run_openra_replay.cmake) -set_tests_properties(MobileGLTraceReplay.OpenRA.DirectGLES PROPERTIES - ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330") +function(add_openra_trace_replay_test BACKEND) + add_test( + NAME MobileGLTraceReplay.OpenRA.${BACKEND} + COMMAND "${CMAKE_COMMAND}" + -DTRACE_REPLAY_EXE=$ + -DMOBILEGL_LIBRARY=$ + -DOPENRA_TRACE_ARCHIVE=${MOBILEGL_TRACE_ROOT}/fixtures/openra.tgz + -DOPENRA_GOLDEN=${MOBILEGL_TRACE_ROOT}/fixtures/openra.0000031249.png + -DOPENRA_BACKEND=${BACKEND} + -DOPENRA_OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/openra/${BACKEND} + -DOPENRA_ARTIFACT_DIR=${CMAKE_CURRENT_BINARY_DIR}/openra/actual-images + -P ${MOBILEGL_TRACE_ROOT}/run_openra_replay.cmake) + set_tests_properties(MobileGLTraceReplay.OpenRA.${BACKEND} PROPERTIES + ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330") +endfunction() + +add_openra_trace_replay_test(DirectGLES) +add_openra_trace_replay_test(DirectVulkan) diff --git a/tools/trace_replay/README.md b/tools/trace_replay/README.md index a9845335..5b238817 100644 --- a/tools/trace_replay/README.md +++ b/tools/trace_replay/README.md @@ -16,7 +16,7 @@ cmake --build build-test Run the OpenRA fixture test: ```sh -ctest --test-dir build-test -V -R MobileGLTraceReplay.OpenRA.DirectGLES +ctest --test-dir build-test -V -R MobileGLTraceReplay.OpenRA ``` Run the CLI directly: @@ -39,6 +39,5 @@ build-test/tools/trace_replay/mobilegl_trace_replay \ --fuzz-percent 20 ``` -The Linux runner uses an EGL pbuffer shim and loads MobileGL by path with `dlopen`. The current CI test covers the -OpenRA core-profile trace through the `DirectGLES` backend. `DirectVulkan` is still available as a CLI argument, but it -requires a native window/swapchain path and is not part of the headless CI test yet. +The Linux runner loads MobileGL by path with `dlopen`. It uses an EGL pbuffer for `DirectGLES` and an X11 window +surface for `DirectVulkan` so Magma can create a Vulkan swapchain under `xvfb-run` in CI. diff --git a/tools/trace_replay/apitrace_glws_egl.cpp b/tools/trace_replay/apitrace_glws_egl.cpp index 169d0421..d2d18d6e 100644 --- a/tools/trace_replay/apitrace_glws_egl.cpp +++ b/tools/trace_replay/apitrace_glws_egl.cpp @@ -16,6 +16,7 @@ using PfnEglBindApi = EGLBoolean (*)(EGLenum); using PfnEglChooseConfig = EGLBoolean (*)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *); using PfnEglCreateContext = EGLContext (*)(EGLDisplay, EGLConfig, EGLContext, const EGLint *); using PfnEglCreatePbufferSurface = EGLSurface (*)(EGLDisplay, EGLConfig, const EGLint *); +using PfnEglCreateWindowSurface = EGLSurface (*)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *); using PfnEglDestroyContext = EGLBoolean (*)(EGLDisplay, EGLContext); using PfnEglDestroySurface = EGLBoolean (*)(EGLDisplay, EGLSurface); using PfnEglGetConfigAttrib = EGLBoolean (*)(EGLDisplay, EGLConfig, EGLint, EGLint *); @@ -26,12 +27,14 @@ using PfnEglMakeCurrent = EGLBoolean (*)(EGLDisplay, EGLSurface, EGLSurface, EGL using PfnEglQueryString = const char *(*)(EGLDisplay, EGLint); using PfnEglSwapBuffers = EGLBoolean (*)(EGLDisplay, EGLSurface); using PfnEglTerminate = EGLBoolean (*)(EGLDisplay); +using PfnGlGetString = const unsigned char *(*)(unsigned int); struct EglFns { PfnEglBindApi bindApi = nullptr; PfnEglChooseConfig chooseConfig = nullptr; PfnEglCreateContext createContext = nullptr; PfnEglCreatePbufferSurface createPbufferSurface = nullptr; + PfnEglCreateWindowSurface createWindowSurface = nullptr; PfnEglDestroyContext destroyContext = nullptr; PfnEglDestroySurface destroySurface = nullptr; PfnEglGetConfigAttrib getConfigAttrib = nullptr; @@ -44,13 +47,39 @@ struct EglFns { PfnEglTerminate terminate = nullptr; }; +struct X11Fns { + void *lib = nullptr; + void *display = nullptr; + using XOpenDisplay = void *(*)(const char *); + using XCloseDisplay = int (*)(void *); + using XDefaultScreen = int (*)(void *); + using XDefaultRootWindow = unsigned long (*)(void *); + using XBlackPixel = unsigned long (*)(void *, int); + using XCreateSimpleWindow = unsigned long (*)(void *, unsigned long, int, int, unsigned int, unsigned int, + unsigned int, unsigned long, unsigned long); + using XDestroyWindow = int (*)(void *, unsigned long); + using XMapWindow = int (*)(void *, unsigned long); + using XFlush = int (*)(void *); + XOpenDisplay openDisplay = nullptr; + XCloseDisplay closeDisplay = nullptr; + XDefaultScreen defaultScreen = nullptr; + XDefaultRootWindow defaultRootWindow = nullptr; + XBlackPixel blackPixel = nullptr; + XCreateSimpleWindow createSimpleWindow = nullptr; + XDestroyWindow destroyWindow = nullptr; + XMapWindow mapWindow = nullptr; + XFlush flush = nullptr; +}; + EglFns gEgl; +X11Fns gX11; EGLDisplay gDisplay = EGL_NO_DISPLAY; void *gMobileGl = nullptr; const glws::Drawable *gCurrentDrawable = nullptr; EGLContext gCurrentContext = EGL_NO_CONTEXT; int gRequestedWidth = 0; int gRequestedHeight = 0; +bool gPrintedGlIdentity = false; int ResolveWidth(int width) { if (gRequestedWidth > 0) { @@ -106,6 +135,7 @@ bool LoadEgl() { Load(gEgl.chooseConfig, "eglChooseConfig") && Load(gEgl.createContext, "eglCreateContext") && Load(gEgl.createPbufferSurface, "eglCreatePbufferSurface") && + Load(gEgl.createWindowSurface, "eglCreateWindowSurface") && Load(gEgl.destroyContext, "eglDestroyContext") && Load(gEgl.destroySurface, "eglDestroySurface") && Load(gEgl.getConfigAttrib, "eglGetConfigAttrib") && @@ -118,6 +148,108 @@ bool LoadEgl() { Load(gEgl.terminate, "eglTerminate"); } +bool IsDirectVulkanBackend() { + const char *backend = std::getenv("MOBILEGL_BACKEND_TYPE"); + return backend != nullptr && std::strcmp(backend, "DirectVulkan") == 0; +} + +template +bool LoadX11(T &slot, const char *name) { + slot = reinterpret_cast(dlsym(gX11.lib, name)); + if (slot == nullptr) { + std::cerr << "error: failed to resolve " << name << " from X11\n"; + return false; + } + return true; +} + +bool EnsureX11() { + if (gX11.display != nullptr) { + return true; + } + if (gX11.lib == nullptr) { + gX11.lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW); + if (gX11.lib == nullptr) { + gX11.lib = dlopen("libX11.so", RTLD_LOCAL | RTLD_NOW); + } + if (gX11.lib == nullptr) { + std::cerr << "error: failed to open libX11 for DirectVulkan trace replay\n"; + return false; + } + if (!LoadX11(gX11.openDisplay, "XOpenDisplay") || + !LoadX11(gX11.closeDisplay, "XCloseDisplay") || + !LoadX11(gX11.defaultScreen, "XDefaultScreen") || + !LoadX11(gX11.defaultRootWindow, "XDefaultRootWindow") || + !LoadX11(gX11.blackPixel, "XBlackPixel") || + !LoadX11(gX11.createSimpleWindow, "XCreateSimpleWindow") || + !LoadX11(gX11.destroyWindow, "XDestroyWindow") || + !LoadX11(gX11.mapWindow, "XMapWindow") || + !LoadX11(gX11.flush, "XFlush")) { + return false; + } + } + gX11.display = gX11.openDisplay(std::getenv("DISPLAY")); + if (gX11.display == nullptr) { + std::cerr << "error: XOpenDisplay failed for DirectVulkan trace replay\n"; + return false; + } + return true; +} + +unsigned long CreateX11Window(int width, int height) { + if (!EnsureX11()) { + return 0; + } + const int screen = gX11.defaultScreen(gX11.display); + const unsigned long root = gX11.defaultRootWindow(gX11.display); + const unsigned long black = gX11.blackPixel(gX11.display, screen); + const unsigned long window = gX11.createSimpleWindow(gX11.display, root, 0, 0, + static_cast(std::max(width, 1)), + static_cast(std::max(height, 1)), + 0, black, black); + if (window != 0) { + gX11.mapWindow(gX11.display, window); + gX11.flush(gX11.display); + } + return window; +} + +void DestroyX11Window(unsigned long window) { + if (window != 0 && gX11.display != nullptr && gX11.destroyWindow != nullptr) { + gX11.destroyWindow(gX11.display, window); + gX11.flush(gX11.display); + } +} + +void PrintGlIdentityOnce() { + if (gPrintedGlIdentity) { + return; + } + auto glGetString = reinterpret_cast(Lookup("glGetString")); + if (glGetString == nullptr) { + std::cerr << "MOBILEGL_TRACE_GL_INFO unavailable: glGetString was not resolved\n"; + gPrintedGlIdentity = true; + return; + } + constexpr unsigned int GL_VENDOR_ENUM = 0x1F00; + constexpr unsigned int GL_RENDERER_ENUM = 0x1F01; + constexpr unsigned int GL_VERSION_ENUM = 0x1F02; + constexpr unsigned int GL_SHADING_LANGUAGE_VERSION_ENUM = 0x8B8C; + auto stringOrUnknown = [glGetString](unsigned int name) { + const auto *value = glGetString(name); + return value == nullptr ? reinterpret_cast("") : value; + }; + std::cerr << "MOBILEGL_TRACE_GL_VENDOR: " + << reinterpret_cast(stringOrUnknown(GL_VENDOR_ENUM)) << "\n" + << "MOBILEGL_TRACE_GL_RENDERER: " + << reinterpret_cast(stringOrUnknown(GL_RENDERER_ENUM)) << "\n" + << "MOBILEGL_TRACE_GL_VERSION: " + << reinterpret_cast(stringOrUnknown(GL_VERSION_ENUM)) << "\n" + << "MOBILEGL_TRACE_GL_SHADING_LANGUAGE_VERSION: " + << reinterpret_cast(stringOrUnknown(GL_SHADING_LANGUAGE_VERSION_ENUM)) << "\n"; + gPrintedGlIdentity = true; +} + class EglVisual final : public glws::Visual { public: EGLConfig config = nullptr; @@ -129,6 +261,7 @@ public: class EglDrawable final : public glws::Drawable { public: EGLSurface surface = EGL_NO_SURFACE; + unsigned long x11Window = 0; EglDrawable(const EglVisual *visual, int width, int height, bool pbuffer) : Drawable(visual, width, height, pbuffer) { @@ -176,14 +309,26 @@ public: private: void createSurface() { - const EGLint attribs[] = { - EGL_WIDTH, ResolveWidth(width), - EGL_HEIGHT, ResolveHeight(height), - EGL_NONE, - }; - surface = gEgl.createPbufferSurface(gDisplay, static_cast(visual)->config, attribs); + const int resolvedWidth = ResolveWidth(width); + const int resolvedHeight = ResolveHeight(height); + if (IsDirectVulkanBackend()) { + x11Window = CreateX11Window(resolvedWidth, resolvedHeight); + if (x11Window == 0) { + surface = EGL_NO_SURFACE; + return; + } + surface = gEgl.createWindowSurface(gDisplay, static_cast(visual)->config, + reinterpret_cast(x11Window), nullptr); + } else { + const EGLint attribs[] = { + EGL_WIDTH, resolvedWidth, + EGL_HEIGHT, resolvedHeight, + EGL_NONE, + }; + surface = gEgl.createPbufferSurface(gDisplay, static_cast(visual)->config, attribs); + } if (surface == EGL_NO_SURFACE) { - std::cerr << "error: EGL pbuffer creation failed: 0x" << std::hex + std::cerr << "error: EGL surface creation failed: 0x" << std::hex << gEgl.getError() << std::dec << "\n"; } } @@ -193,6 +338,8 @@ private: gEgl.destroySurface(gDisplay, surface); surface = EGL_NO_SURFACE; } + DestroyX11Window(x11Window); + x11Window = 0; } }; @@ -337,6 +484,7 @@ bool makeCurrentInternal(Drawable *drawable, Drawable *readable, Context *contex } gCurrentDrawable = drawable; gCurrentContext = eglContext; + PrintGlIdentityOnce(); return true; } diff --git a/tools/trace_replay/run_openra_replay.cmake b/tools/trace_replay/run_openra_replay.cmake index eb88903b..56466565 100644 --- a/tools/trace_replay/run_openra_replay.cmake +++ b/tools/trace_replay/run_openra_replay.cmake @@ -1,4 +1,4 @@ -foreach(required TRACE_REPLAY_EXE MOBILEGL_LIBRARY OPENRA_TRACE_ARCHIVE OPENRA_GOLDEN OPENRA_OUTPUT_DIR) +foreach(required TRACE_REPLAY_EXE MOBILEGL_LIBRARY OPENRA_TRACE_ARCHIVE OPENRA_GOLDEN OPENRA_OUTPUT_DIR OPENRA_BACKEND) if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "") message(FATAL_ERROR "${required} is required") endif() @@ -32,7 +32,7 @@ execute_process( --trace "${openra_trace}" --golden "${OPENRA_GOLDEN}" --output "${OPENRA_OUTPUT_DIR}/output" - --backend DirectGLES + --backend "${OPENRA_BACKEND}" --mobilegl-library "${MOBILEGL_LIBRARY}" --target-call 31249 --width 640 @@ -58,6 +58,28 @@ else() message(FATAL_ERROR "trace replay did not write ${result_json}") endif() +set(retrace_log "${OPENRA_OUTPUT_DIR}/output/retrace.log") +if(EXISTS "${retrace_log}") + file(STRINGS "${retrace_log}" gl_identity_lines REGEX "MOBILEGL_TRACE_GL_") + foreach(line IN LISTS gl_identity_lines) + message(STATUS "${line}") + endforeach() +endif() + +if(DEFINED OPENRA_ARTIFACT_DIR AND NOT "${OPENRA_ARTIFACT_DIR}" STREQUAL "") + file(MAKE_DIRECTORY "${OPENRA_ARTIFACT_DIR}") + set(actual_png "${OPENRA_OUTPUT_DIR}/output/actual.png") + if(EXISTS "${actual_png}") + file(COPY_FILE "${actual_png}" "${OPENRA_ARTIFACT_DIR}/openra-${OPENRA_BACKEND}-actual.png") + endif() + if(EXISTS "${result_json}") + file(COPY_FILE "${result_json}" "${OPENRA_ARTIFACT_DIR}/openra-${OPENRA_BACKEND}-result.json") + endif() + if(EXISTS "${retrace_log}") + file(COPY_FILE "${retrace_log}" "${OPENRA_ARTIFACT_DIR}/openra-${OPENRA_BACKEND}-retrace.log") + endif() +endif() + if(NOT replay_result EQUAL 0) - message(FATAL_ERROR "OpenRA trace replay failed with status ${replay_result}") + message(FATAL_ERROR "OpenRA ${OPENRA_BACKEND} trace replay failed with status ${replay_result}") endif()