cmake_minimum_required(VERSION 3.24) # MobileGL headless GPU integration tests. # # These are not unit tests: each scenario brings up a real EGL context on a # pbuffer, renders real frames through a real backend and asserts on # glReadPixels output. They need a GPU, so the module is OFF by default # (MOBILEGL_BUILD_INTEGRATION_TEST) and every scenario skips cleanly - never # fails, never hangs - on a machine without one. "Cleanly" is not a hope: the # harness runs the whole bring-up in a forked child first, because MobileGL # ABORTS rather than returning an error on an unusable platform (HeadlessGL.cpp). # # A clean skip is also indistinguishable from a pass, so set # MOBILEGL_ITEST_REQUIRE_GPU wherever the machine is supposed to have a GPU. # # Backend selection is latched at initialization from MOBILEGL_BACKEND_TYPE, so # one process is one backend: the same binary is registered twice, once per # backend, under the `integration-gpu` label. message(STATUS "Generating build files for MobileGL Integration Test...") set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..) # Only meaningful where MobileGL_s exists (i.e. not Android). if (NOT TARGET MobileGL_s) message(STATUS "MobileGL_s is not available; skipping the integration test module") return() endif() # MG_Test already pulls googletest in when MOBILEGL_BUILD_TEST is ON. Stand on # our own feet when it is not, so this module can be built by itself. if (NOT TARGET GTest::gtest) include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.17.0 ) set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) endif() add_executable(MobileGLIntegrationTest Main.cpp Harness/HeadlessGL.cpp Scenarios/OrientationScenario.cpp Scenarios/CrossFrameBufferScenario.cpp Scenarios/ResidentIndexScenario.cpp Scenarios/MultiDrawScenario.cpp Scenarios/DrawParametersScenario.cpp Scenarios/AsyncCompileScenario.cpp Scenarios/XfbAfterClipDistanceScenario.cpp Scenarios/ThreeChannelAttachmentScenario.cpp Scenarios/PipelineFailureScenario.cpp Scenarios/AdvertisedLimitsScenario.cpp Scenarios/PixelStoreSweepScenario.cpp Scenarios/FragCoordOriginScenario.cpp Scenarios/ClearThenReadPixelsScenario.cpp Scenarios/DepthStencilReadbackScenario.cpp Scenarios/SsboArrayLengthScenario.cpp Scenarios/UniformInitializerScenario.cpp Scenarios/SwizzleAccessRoutineScenario.cpp Scenarios/ProgramPipelineScenario.cpp Scenarios/ImageLoadStoreSsoScenario.cpp Scenarios/SsboDeclarationFormScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp Scenarios/FragmentOutputArrayIndexScenario.cpp Scenarios/BufferTextureScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE ${MGL_ITEST_ROOT}/include ${MGL_ITEST_ROOT}/MobileGL ) # gtest, not gtest_main: Main.cpp installs the harness banner itself. target_link_libraries(MobileGLIntegrationTest PRIVATE GTest::gtest MobileGL_s ) if (MSVC) # Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl* # as dllimport on Windows, so the in-library GL entry-point definitions only # resolve if the whole static library is part of the link. target_link_options(MobileGLIntegrationTest PRIVATE /WHOLEARCHIVE:MobileGL_s) endif() target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX) # --- ctest wiring -------------------------------------------------------- # A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is # usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under # a GPU test. Pin the vendor/ICD json the same way MG_Benchmark's # run_driver_bench.sh does. # # Leaving these empty is not a neutral default, it is the failure mode: an # unpinned libEGL lands on llvmpipe and the suite goes green having tested a # software rasteriser. So they are DETECTED here rather than defaulted to empty, # and an empty result is a loud warning. # # mgl_itest_find_driver_json( [...]) # Picks the first json a real hardware vendor owns, in preference order, and # never picks a software rasteriser (llvmpipe / lavapipe / swrast) - landing on # one of those silently is the exact accident this pinning exists to prevent. function(mgl_itest_find_driver_json outVar) set(candidates "") foreach(pattern IN LISTS ARGN) file(GLOB matches "${pattern}") list(APPEND candidates ${matches}) endforeach() list(SORT candidates) # Vendors ship an i686 json beside the x86_64 one and it sorts first. Pinning # the wrong word size is worse than not pinning at all - the loader finds no # driver and the whole suite skips - so drop the mismatched ones outright. if (CMAKE_SIZEOF_VOID_P EQUAL 8) list(FILTER candidates EXCLUDE REGEX "i686|i386") else() list(FILTER candidates EXCLUDE REGEX "x86_64|aarch64") endif() set(software "") foreach(vendor IN ITEMS nvidia amdgpu amd radeon intel_hasvk intel broadcom freedreno panfrost) foreach(candidate IN LISTS candidates) get_filename_component(leaf "${candidate}" NAME) string(TOLOWER "${leaf}" leaf) if (leaf MATCHES "${vendor}") set(${outVar} "${candidate}" PARENT_SCOPE) return() endif() endforeach() endforeach() # Nothing recognised as hardware. Report the first non-software entry if there # is one; otherwise report nothing, so the warning below fires. foreach(candidate IN LISTS candidates) get_filename_component(leaf "${candidate}" NAME) string(TOLOWER "${leaf}" leaf) if (NOT leaf MATCHES "lvp|llvmpipe|lavapipe|swrast|softpipe") set(${outVar} "${candidate}" PARENT_SCOPE) return() endif() set(software "${candidate}") endforeach() set(${outVar} "" PARENT_SCOPE) endfunction() set(MGL_ITEST_DETECTED_EGL_VENDOR "") set(MGL_ITEST_DETECTED_VK_ICD "") if (UNIX AND NOT APPLE AND NOT ANDROID) mgl_itest_find_driver_json(MGL_ITEST_DETECTED_EGL_VENDOR "/usr/share/glvnd/egl_vendor.d/*.json" "/etc/glvnd/egl_vendor.d/*.json") mgl_itest_find_driver_json(MGL_ITEST_DETECTED_VK_ICD "/usr/share/vulkan/icd.d/*.json" "/etc/vulkan/icd.d/*.json") endif() set(MOBILEGL_ITEST_EGL_VENDOR "${MGL_ITEST_DETECTED_EGL_VENDOR}" CACHE FILEPATH "glvnd EGL vendor json to pin for the integration tests (empty: leave the loader alone)") set(MOBILEGL_ITEST_VK_ICD "${MGL_ITEST_DETECTED_VK_ICD}" CACHE FILEPATH "Vulkan ICD json to pin for the DirectVulkan integration tests (empty: leave the loader alone)") if (MOBILEGL_ITEST_EGL_VENDOR) message(STATUS "Integration tests: pinning EGL vendor ${MOBILEGL_ITEST_EGL_VENDOR}") else() message(WARNING "Integration tests: no EGL vendor json found or configured (MOBILEGL_ITEST_EGL_VENDOR is empty). " "An unpinned libEGL on a glvnd system resolves to whichever vendor comes first, which is usually " "Mesa/llvmpipe - the scenarios would then go green against a software rasteriser instead of the GPU. " "Set -DMOBILEGL_ITEST_EGL_VENDOR=/usr/share/glvnd/egl_vendor.d/.json.") endif() if (MOBILEGL_ITEST_VK_ICD) message(STATUS "Integration tests: pinning Vulkan ICD ${MOBILEGL_ITEST_VK_ICD}") else() message(WARNING "Integration tests: no Vulkan ICD json found or configured (MOBILEGL_ITEST_VK_ICD is empty). " "DirectVulkan would then load whichever ICD the loader enumerates first, quite possibly lavapipe. " "Set -DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/.json.") endif() # Turns "no usable GPU" from a clean skip into a failure - see ScenarioFixture.h. # Without it the integration-gpu label is unfalsifiable: a run that skipped every # scenario and a run that passed every scenario are the same green in ctest. option(MOBILEGL_ITEST_REQUIRE_GPU "Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF) # No EGL_PLATFORM knob here on purpose. The harness pins EGL_PLATFORM=surfaceless # itself before its first EGL call (HeadlessGL.cpp, EnsureHeadlessPlatform) so a # developer's machine and a CI runner take the SAME path whether or not a window # system happens to be running. This used to inject "x11", which is how the lane # came up green on a workstation with WSLg and died on a runner with no X server. # # A build-system knob would not just be redundant, it would be a trap: `set(... # CACHE ...)` does not rewrite an existing cache, so every build directory # configured before this change would keep injecting EGL_PLATFORM=x11 and go on # binding to a window system - silently, and only on the machines that have one. # Someone reproducing a platform-specific bug sets EGL_PLATFORM in their own # environment, which the harness still honours. set(MGL_ITEST_COMMON_ENV "") if (MOBILEGL_ITEST_EGL_VENDOR) list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}") endif() unset(MOBILEGL_ITEST_EGL_PLATFORM CACHE) # see above: an old cache must not resurrect x11 if (MOBILEGL_ITEST_REQUIRE_GPU) list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1") endif() set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV}) if (MOBILEGL_ITEST_VK_ICD) list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}") endif() # The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests # forwards PROPERTIES as a flat list - so a plain `;`-joined value arrives as # four separate arguments and everything after the first is silently read as # another property name. Escaping the separators keeps the whole thing one list # element until set_tests_properties expands it back. Without this only # MOBILEGL_BACKEND_TYPE reaches the test and the vendor/ICD pinning is lost. function(mgl_itest_join_environment outVar) set(joined "") foreach(entry IN LISTS ARGN) if (joined) string(APPEND joined "\\;${entry}") else() set(joined "${entry}") endif() endforeach() set(${outVar} "${joined}" PARENT_SCOPE) endfunction() mgl_itest_join_environment(MGL_ITEST_GLES_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectGLES" ${MGL_ITEST_COMMON_ENV}) mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV}) mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV}) # TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it. set(MGL_ITEST_TIMEOUT 120) include(GoogleTest) # Discovery runs `--gtest_list_tests`, which does not construct the harness and # so needs no GPU. One registration per backend; TEST_PREFIX keeps the two sets # of ctest names apart. gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectGLES." DISCOVERY_TIMEOUT 30 PROPERTIES LABELS integration-gpu TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_GLES_ENVIRONMENT}" ) gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan." DISCOVERY_TIMEOUT 30 PROPERTIES LABELS integration-gpu TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_VULKAN_ENVIRONMENT}" ) # A third registration, of ONE scenario, with asynchronous shader compilation # pinned on. Not a second code path in the renderer: a second ALLOCATION pattern. # The async pipeline's job objects change which of the freed blocks the capture # phase is handed, and that is what decides whether the destroyed-VAO address is # reached at all - on the ablated (pre-fix) tree async=1 reproduced 3 runs out of # 3 where the ambient default reproduced 2 of 3. Pinning it here means the # high-signal configuration runs whatever the shipped default becomes, instead of # the suite quietly weakening the day that default flips. It must be process-wide # (the ENVIRONMENT property), not an in-process scope: the compile pool and its # threads are stood up at initialization, and their allocations are half the # point. DirectVulkan only - the memo this pins is DirectVulkan's. gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.AsyncCompile." TEST_FILTER "XfbAfterClipDistanceScenario.*" DISCOVERY_TIMEOUT 30 PROPERTIES LABELS integration-gpu TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}" )