mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 14:48:32 +09:00
Complementary Reimagined would not load through Espryt on Mali: Iris got GL_FRAMEBUFFER_UNSUPPORTED building its composite framebuffer, because colortex1 is RGB8_SNORM and colortex2 is RGB16F - three-channel formats that no real ES driver can render to (EXT_render_snorm covers R/RG/RGBA only, and the float extensions exclude the RGB forms). The frontend's probe cache diagnosed this correctly and then had nothing to offer: the NoThreeChannelRenderTarget widening machinery existed but was gated to multisample targets alone. llvmpipe turns out to refuse most of the same attachments - CI retrace stayed green only because a replay never branches on glCheckFramebufferStatus - so this was never a desktop-vs- device split, just an unlit path. The widening now applies to every color-attachable image, renderbuffers included, riding the driver-probe branch so the native format is still tried first and substituted only on refusal. One ThreeChannelWidening table owns the widened (internalformat, format, type) triple per source format - the previous per-case branches disagreed with each other and could emit an unuploadable (RGBA16F, GL_RGB, GL_BYTE) combination or widen into another three-channel format the driver refuses just the same. Uploads repack three-component client data to four with the format's own one in the alpha channel (127 is not 1 for RGB8I - the integer arms carry integer ones); readback drops the synthetic alpha, derived from the actual image being read, not the bound framebuffer, so glGetTexImage through a scratch FBO cannot be confused by an unrelated widened attachment. Stored alpha on a widened attachment is now an invariant 1.0 rather than an accident: the color-mask sync clears the alpha bit per draw buffer (glColorMaski for MRT mixes), and clears route through glClearBufferfv with alpha substituted on widened slots only - scissored clears inherit the discipline for free, integer color buffers keep their explicit integer-clear path, and glGet still answers the application's own mask. GL_DST_ALPHA blending, blits and readback therefore all see 1.0 without further interception. DriverPost grows the rows this bug earned: EXT_color_buffer_float detection (previously unreferenced anywhere) with a FAIL row when absent, the missing EXT_render_snorm row, and a three-channel- attachment row that reports one representative per widening class - graded so a half-float-only driver warns about the 32-bit float gap instead of being declared unsupported. Gates: 606/606 unit at default and with the async kill switch; full retrace, both backends - the complementary fixtures now run with the widening ACTIVE on llvmpipe and pass with a slightly better SSIM than before; ext caselist DirectGLES holds 3914/4867 with zero set drift while 54 cases move from NotSupported to genuinely passing; on the Mali-G77 device, Complementary Reimagined builds its pipeline and renders in-world through Espryt (md5-verified build), BSL still green. A new ThreeChannelAttachmentScenario pins the frontend answer - COMPLETE where it used to say UNSUPPORTED - on the real driver.
271 lines
12 KiB
CMake
271 lines
12 KiB
CMake
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/AsyncCompileScenario.cpp
|
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
|
Scenarios/ThreeChannelAttachmentScenario.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})
|
|
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}"
|
|
)
|