mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
Android links this module against the SHIPPING libMobileGL.so on purpose, so the on-device run validates the real artifact - and that library is -fvisibility=hidden. Six symbols the scenarios reached for were therefore undefined and the executable could not be linked at all: MG_Config::Features, SetAsyncShaderCompileSuspended, ShaderCompilePool::Get/GetThreadCount/SetMaxConcurrency and AsyncShaderCompileEnabled. All six are gone rather than exported. CompilerThreadScope now restores the pool with glMaxShaderCompilerThreadsKHR (0xFFFFFFFF), which MaxShaderCompilerThreadsKHR_State defines as exactly the two steps it used to perform by hand, and AsyncAndSyncProgramsRenderIdenticalFrames picks its two modes with the same entry point - a zero count compiles inline, a nonzero one lifts that - so the switching is now itself under test. ExtensionStringMatchesTheConfiguration stops deriving its expectation from AsyncShaderCompileEnabled(), the very function the backends gate the extension string on: it was asserting the implementation against itself and would have passed however wrong both halves were. The expectation is now MOBILEGL_ASYNC_SHADER_COMPILE as the process inherited it, and the case skips where that is unset because the built-in default is a value only the implementation knows. Both-mode coverage in one ctest run is preserved by registration rather than by in-process forcing, and is wider than before. New entries, each APPENDING to the common/Vulkan environment so the EGL-vendor and ICD pinning is not lost - a ctest ENVIRONMENT property replaces the job environment rather than adding to it: DirectGLES./DirectVulkan.AsyncOn. run all of AsyncCompileScenario with MOBILEGL_ASYNC_SHADER_COMPILE=1; .AsyncOff. run the extension case with =0, which asserts the withdrawn side that nothing covered before; .OptimisticShaderStatus. run the Iris-shaped case with MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1 (its own entries because the quirk is not neutral for the rest of the scenario); DirectGLES.NoViewportArrayEmulation. runs the emulation control with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0. Run from a device shell with nothing set, the ambient configuration runs and the rest skip cleanly. 1836 -> 1853 ctest entries, all green.
128 lines
6.5 KiB
C++
128 lines
6.5 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
//
|
|
// The base fixture every scenario derives from. Its only jobs are to bring the
|
|
// headless context up once per process and to decide what "this machine has no
|
|
// usable GPU" means.
|
|
//
|
|
// By default it means a clean GTEST_SKIP() - never a failure, never a hang -
|
|
// because a developer box or a container without a GPU should not fail a run it
|
|
// was never able to perform. But a skip is indistinguishable from a pass in
|
|
// every CI summary, so the `integration-gpu` label on its own is unfalsifiable:
|
|
// a runner whose driver pinning silently broke reports the same green as one
|
|
// that rendered every frame. MOBILEGL_ITEST_REQUIRE_GPU is the caller saying
|
|
// "this machine HAS a GPU and I am relying on these scenarios actually running";
|
|
// with it set, an unusable harness is a FAILURE carrying the pre-flight's reason.
|
|
|
|
#pragma once
|
|
|
|
#include <cctype>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "HeadlessGL.h"
|
|
|
|
namespace MGITest {
|
|
|
|
// How a MOBILEGL_* quirk variable reads in THIS process's environment.
|
|
//
|
|
// A scenario that needs a non-default configuration takes it from here and skips
|
|
// when the process it was launched into is not in that configuration, rather than
|
|
// writing MG_Config::Features itself. Two reasons, and the second one decides it:
|
|
//
|
|
// - the feature table is an internal symbol. On Android this module links against
|
|
// the SHIPPING libMobileGL.so - deliberately, so the on-device run validates the
|
|
// real artifact - and that library is built -fvisibility=hidden, so nothing
|
|
// internal is reachable from here at all.
|
|
// - a quirk poked in-process is already too late for everything latched at
|
|
// initialization: the compile pool and its threads, and the backend's advertised
|
|
// extension list, which is built once from the configuration in force at first
|
|
// use. The process-wide variable is the only spelling that covers the whole
|
|
// configuration instead of the half of it that is still mutable afterwards.
|
|
//
|
|
// The reading rule is MG_ConfigLoader's, character for character (ConfigLoader.cpp,
|
|
// QueryEnvQuirkOverride / IsTruthyValue): unset is Auto - device auto-detection or a
|
|
// built-in default, i.e. a value only the implementation knows - a truthy value is
|
|
// On, and anything else that IS set ("0", "false", "") is Off.
|
|
enum class AmbientQuirk { Auto, On, Off };
|
|
|
|
inline AmbientQuirk AmbientQuirkFromEnvironment(const char* name) {
|
|
const char* value = std::getenv(name);
|
|
if (value == nullptr) return AmbientQuirk::Auto;
|
|
std::string lowered(value);
|
|
for (char& c : lowered) {
|
|
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
}
|
|
if (lowered.empty() || lowered == "0" || lowered == "false") return AmbientQuirk::Off;
|
|
return AmbientQuirk::On;
|
|
}
|
|
|
|
class ScenarioTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
m_ready = false;
|
|
HeadlessGL& gl = HeadlessGL::Get();
|
|
if (!gl.Usable()) {
|
|
if (RequireGpu()) {
|
|
// FAIL() is a FATAL failure but does NOT mark the test skipped,
|
|
// so a derived SetUp that guards on IsSkipped() alone would run
|
|
// straight into GL calls with no current context and SIGSEGV -
|
|
// that exact crash shipped from the first version of this guard.
|
|
// Derived fixtures must gate on Ready() (below), which is false
|
|
// on BOTH the skip path and this failure path.
|
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set, so an unusable harness is a failure, not a skip. "
|
|
<< "Backend " << gl.BackendName() << " could not be brought up: " << gl.SkipReason();
|
|
}
|
|
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
|
}
|
|
if (RequireHardwareGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
|
// Only when hardware was asked for BY NAME. REQUIRE_GPU means "an
|
|
// unusable harness is a failure, not a silent skip" - it is the
|
|
// falsifiability switch, and CI is exactly where it belongs. But CI
|
|
// runners have no GPU, so folding "must not be llvmpipe" into the
|
|
// same switch made the CI lane unpassable by construction: the
|
|
// scenarios pin backend draw logic, which a software rasterizer
|
|
// executes just as faithfully. Landing on llvmpipe/lavapipe there is
|
|
// the intended configuration, not a misconfiguration. A vendor pin
|
|
// that must not silently degrade sets REQUIRE_HARDWARE_GPU.
|
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set but the context landed on a software "
|
|
<< "rasterizer: " << gl.RendererString();
|
|
}
|
|
// A scenario starts from a clean slate but shares the context (and so
|
|
// the renderer's memos) with every other scenario in this process -
|
|
// which is exactly the situation both shipped bugs needed.
|
|
RecordProperty("backend", gl.BackendName());
|
|
RecordProperty("renderer", gl.RendererString());
|
|
m_ready = true;
|
|
}
|
|
|
|
// The ONLY gate a derived SetUp/TearDown may use: `if (!Ready()) return;`.
|
|
// True only when the base SetUp brought the context up and neither skipped
|
|
// nor failed. IsSkipped() alone is WRONG here (see the comment at FAIL()).
|
|
bool Ready() const { return m_ready; }
|
|
|
|
static HeadlessGL& Gl() { return HeadlessGL::Get(); }
|
|
|
|
private:
|
|
static bool LooksLikeSoftwareRasterizer(const std::string& renderer) {
|
|
static const char* kNames[] = {"llvmpipe", "lavapipe", "softpipe", "SwiftShader", "swrast"};
|
|
for (const char* name : kNames) {
|
|
if (renderer.find(name) != std::string::npos) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool m_ready = false;
|
|
};
|
|
|
|
} // namespace MGITest
|