mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Test] (MG_IntegrationTest): pin the two shipped memo bugs with rendered pixels
Bothd7976326bugs passed every unit test while corrupting real frames - state-level assertions cannot see them. This module renders and reads back. A headless EGL-pbuffer harness (no window, no GLFW) linking MobileGL_s directly, registered once per backend under the ctest label integration-gpu, behind the default-OFF option MOBILEGL_BUILD_INTEGRATION_TEST. The platform pre-flight runs the ENTIRE bring-up in a forked child first - MobileGL aborts rather than returning errors on an unusable platform, and the child dying on any signal turns into a clean GTEST_SKIP instead of taking the test binary down. MOBILEGL_ITEST_REQUIRE_GPU makes the label falsifiable: with it set, an unusable harness (or a context that lands on a software rasterizer) is a FAILURE - without it, a CI runner whose driver pinning silently broke reports the same green as one that rendered every frame. Configure-time detection pins the EGL vendor and Vulkan ICD jsons, preferring hardware vendors and never selecting llvmpipe/lavapipe. Scenarios assert on glReadPixels with whole-region pixel counts (a 2x2 quadrant pattern whose signature distinguishes all eight square symmetries; every region predicate reports the first offending pixel): - OrientationScenario: default -> FBO -> default, pinning the transform-flags memo key. Keying GetBaseTransformFlagsRaw on the pre-transform alone fails exactly 3 entries. - StreamedArenaScenario: an untouched streamed vertex buffer must survive transient-arena recycling. Re-enabling only the cross-frame vertex revalidation fails exactly this entry. - CrossFrameBufferScenario + ResidentIndexScenario: cross-frame mutation matrix (SubData, map/unmap, persistent+flush, coherent persistent, orphan, CopyBufferSubData; vertex and index) plus six adversarial resident-EBO constructions. Instrumentation showed the cross-frame EBO memo cannot be made to serve wrong bytes from GL level on this stack (89 entries, 81 accepts, zero divergent slices) - these cases are freshness tripwires, documented as such in-file; the EBO half ofd7976326remains unpinned by a failing test. At the buggy commit72ee7c43the suite fails 4 entries (3 orientation + 1 streamed-arena); atd7976326all 52 pass, 5 consecutive runs, zero flakes, and the default build is bit-for-bit unaffected (unit suite unchanged). Adversarially verified twice, including hostile-platform sweeps (26 configurations, all clean skips) and hand-edits of each production hole in isolation.
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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
|
||||
)
|
||||
|
||||
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(<outVar> <description> <glob> [<glob>...])
|
||||
# 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/<vendor>.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/<vendor>.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)
|
||||
|
||||
# DirectGLES asks the system EGL for a pbuffer config, and on Mesa the default
|
||||
# platform is not X11 unless it is said out loud (run_driver_bench.sh sets the
|
||||
# same variable). Wrong platform here is not a soft failure: eglCreatePbuffer
|
||||
# fails and every scenario skips.
|
||||
if (UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
set(MOBILEGL_ITEST_EGL_PLATFORM "x11" CACHE STRING
|
||||
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||
else()
|
||||
set(MOBILEGL_ITEST_EGL_PLATFORM "" CACHE STRING
|
||||
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||
endif()
|
||||
|
||||
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()
|
||||
if (MOBILEGL_ITEST_EGL_PLATFORM)
|
||||
list(APPEND MGL_ITEST_COMMON_ENV "EGL_PLATFORM=${MOBILEGL_ITEST_EGL_PLATFORM}")
|
||||
endif()
|
||||
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})
|
||||
|
||||
# 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}"
|
||||
)
|
||||
Reference in New Issue
Block a user