Merge magma-validate: Magma SPIR-V validation declines and surfaceless EGL.

This commit is contained in:
2026-10-02 23:38:19 -04:00
17 changed files with 778 additions and 25 deletions
+5
View File
@@ -130,6 +130,11 @@ namespace MobileGL::MG_Config {
Bool AdvertiseFp64 = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV: hand a shader module that fails SPIR-V validation to
// the driver anyway. Off by default: Magma declines such a program (no shader module, its
// draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU
// into device loss. For diagnosing a driver that copes with a specific module only.
Bool MagmaAllowInvalidSpirv = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
+1
View File
@@ -348,6 +348,7 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
@@ -391,6 +391,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
// The assert compiles out of shipping builds, and a program declined for an invalid
// module (VkProgramObject::invalidSpirvDeclined) has no stages at all: a caller that
// forgot to refuse it must get a skipped draw, not a stageCount=0 pipeline.
if (payload.stages == nullptr || payload.stages->empty()) {
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with no shader stages "
"(programHash=0x%llx); its draws are skipped",
static_cast<unsigned long long>(payload.programHash));
return VK_NULL_HANDLE;
}
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
@@ -12,7 +12,9 @@
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <Config.h>
#include <algorithm>
#include <atomic>
#include <bit>
#include <cmath>
#include <cstring>
@@ -2273,8 +2275,84 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
}
// ScreenModuleForDriver's memory. Keyed by content, never by program: the same bytes get
// the same answer whichever program, variant or session carries them.
struct DriverModuleVerdictCache {
struct Entry {
Uint64 wordCount = 0;
Bool valid = true;
String reason;
};
// Generous for any one application's distinct stage modules; past it the whole table
// is dropped rather than tracked per entry - a re-validation is the only cost.
static constexpr SizeT kMaxEntries = 8192;
std::mutex mutex;
UnorderedMap<Uint64, Entry> entries;
std::atomic<Uint64> validatorRuns{0};
};
DriverModuleVerdictCache& GetDriverModuleVerdictCache() {
// Leaked on purpose: screened from session threads that may outlive static destruction.
static auto* cache = new DriverModuleVerdictCache();
return *cache;
}
} // namespace
ProgramFactory::DriverModuleVerdict ProgramFactory::ScreenModuleForDriver(const Vector<Uint>& spirv,
Bool allowInvalid, String* outReason) {
if (outReason != nullptr) outReason->clear();
if (spirv.empty()) return DriverModuleVerdict::Valid;
auto& cache = GetDriverModuleVerdictCache();
const Uint64 key = XXH64(spirv.data(), spirv.size() * sizeof(Uint), 0);
Bool valid = true;
String reason;
Bool known = false;
{
const std::lock_guard<std::mutex> lock(cache.mutex);
const auto it = cache.entries.find(key);
if (it != cache.entries.end() && it->second.wordCount == spirv.size()) {
known = true;
valid = it->second.valid;
reason = it->second.reason;
}
}
if (!known) {
// Outside the lock: two threads screening the same new module both validate, and
// agree; nothing waits on another session's spirv-val.
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
cache.validatorRuns.fetch_add(1, std::memory_order_relaxed);
spv_context context = spvContextCreate(GetSpirvTargetEnv(spirv));
spv_validator_options options = spvValidatorOptionsCreate();
spvValidatorOptionsSetFriendlyNames(options, true);
spv_const_binary_t binary = {spirv.data(), spirv.size()};
spv_diagnostic diagnostic = nullptr;
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
valid = result == SPV_SUCCESS;
if (!valid) {
reason = diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<no diagnostic>";
// The process-wide counter the validating test lanes assert on.
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
}
spvDiagnosticDestroy(diagnostic);
spvValidatorOptionsDestroy(options);
spvContextDestroy(context);
const std::lock_guard<std::mutex> lock(cache.mutex);
if (cache.entries.size() >= DriverModuleVerdictCache::kMaxEntries) cache.entries.clear();
cache.entries[key] = DriverModuleVerdictCache::Entry{spirv.size(), valid, reason};
}
if (valid) return DriverModuleVerdict::Valid;
if (outReason != nullptr) *outReason = Move(reason);
return allowInvalid ? DriverModuleVerdict::AdmittedInvalid : DriverModuleVerdict::DeclinedInvalid;
}
Uint64 ProgramFactory::DriverModuleValidatorRuns() {
return GetDriverModuleVerdictCache().validatorRuns.load(std::memory_order_relaxed);
}
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
// position math between pipelines cannot desynchronize it; the blended depth-write
@@ -3814,27 +3892,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
// Last look at the exact bytes the driver receives, in EVERY build rather than only in
// DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed, and for every stage BEFORE any
// module is created. By here the descriptor bindings have been remapped and the layout
// about to be reflected describes the remapped module, so there is no module left that
// is both valid and consistent with it to fall back to (the recovery that exists lives
// one step earlier, at the clip/XFB fixups). What is left is to keep the bytes away from
// the driver: a driver is not a validating entry point, and an invalid module it accepts
// can fault the GPU into device loss (a frontend translation once put a sampler inside
// the default uniform block, and Adreno page-faulted reading its "descriptor" out of
// uniform-buffer bytes). One invalid stage declines the whole program: no shader module
// of it is created, and every draw or dispatch of it is refused by name
// (VkProgramObject::invalidSpirvDeclined). MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV hands the
// bytes over anyway, for diagnosing a driver that copes.
for (SizeT i = 0; i < moduleCount; ++i) {
String reason;
const DriverModuleVerdict verdict = ScreenModuleForDriver(
moduleSpirvs[i], MG_Config::Features.MagmaAllowInvalidSpirv, &reason);
if (verdict == DriverModuleVerdict::DeclinedInvalid) {
MGLOG_E_ONCE("ProgramFactory: declining program %u - its stage %d module fails SPIR-V validation (%s); "
"no shader module is created and its draws are refused. "
"MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV=1 hands such modules to the driver anyway.",
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
entry.invalidSpirvDeclined = true;
break;
}
if (verdict == DriverModuleVerdict::AdmittedInvalid) {
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d "
"because MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV is set (%s)",
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
}
}
for (SizeT i = 0; i < moduleCount && !entry.invalidSpirvDeclined; ++i) {
auto& moduleSpv = moduleSpirvs[i];
if (moduleSpv.empty()) continue;
// Last look at the exact bytes the driver receives, in EVERY build rather than only
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
// by here the descriptor bindings have been remapped and the layout about to be
// reflected describes the remapped module, so there is no module left that is both
// valid and consistent with it to fall back to. The recovery lives one step earlier,
// at the clip/XFB fixups (see the revert there) - which is where a transform can
// introduce a reference to a delisted interface variable, the failure this whole
// guard exists for. Anything that reaches this line names itself in the log of a
// shipping build instead of dying anonymously inside the driver.
SpirvValidationFailure finalFailure{};
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
"a backend transform after the clip/XFB fixups broke it (%s)",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
finalFailure.message.c_str());
}
// Does the stage the driver will treat as the last pre-rasterization one actually
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
@@ -229,6 +229,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the cache entry, so every later draw of the same program variant reads the same
// answer instead of re-deciding it.
Bool pointSizeCapabilityUnsupported = false;
// One of the program's final modules fails SPIR-V validation, so none of its modules
// was created and every draw or dispatch of it is refused by name (the ProgramInvalidSpirv /
// ComputeProgramInvalidSpirv wire declines) - see ProgramFactory::ScreenModuleForDriver.
// Sticky like the flag above: it lives on the cache entry, so the one log line is honest.
Bool invalidSpirvDeclined = false;
Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are
@@ -289,6 +294,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
// The three refusal flags travel like every other field: a robin-hood insert swaps
// entries, and a flag left behind would refuse (or admit) the wrong program.
invalidSpirvDeclined = other.invalidSpirvDeclined;
xfbCaptureDeclined = other.xfbCaptureDeclined;
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -310,6 +320,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.declinedDescriptors = false;
other.invalidSpirvDeclined = false;
other.xfbCaptureDeclined = false;
other.pointSizeCapabilityUnsupported = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
@@ -353,6 +366,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
// The three refusal flags travel like every other field: a robin-hood insert swaps
// entries, and a flag left behind would refuse (or admit) the wrong program.
invalidSpirvDeclined = other.invalidSpirvDeclined;
xfbCaptureDeclined = other.xfbCaptureDeclined;
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -374,6 +392,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.declinedDescriptors = false;
other.invalidSpirvDeclined = false;
other.xfbCaptureDeclined = false;
other.pointSizeCapabilityUnsupported = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
@@ -504,6 +525,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// VkRenderPassManager::OnPresent's sweep.
void OnFrameBoundary();
// What the last look at a module's final bytes - the exact words vkCreateShaderModule
// would receive - decided.
enum class DriverModuleVerdict : Uint8 {
Valid, // validates: handed to the driver
DeclinedInvalid, // fails validation: never handed over, the program's work is declined
AdmittedInvalid, // fails validation, handed over anyway (MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV)
};
// Validates `spirv` for the Vulkan environment its version word names and turns the
// result into a verdict. A driver is not a validating entry point: Adreno page-faulted
// into device loss on a module that carried a sampler inside a uniform block, Mali
// SIGSEGVs inside pipeline creation. So an invalid module is declined unless
// `allowInvalid` (the escape hatch) says to hand it over regardless.
//
// The validator's answer is remembered per module content (64-bit hash plus word count),
// process-wide: a program variant, an evicted-then-rebuilt entry and two programs that
// share a stage all re-screen the same bytes, and only the first pays for spirv-val.
// `outReason` receives the validator's message for an invalid module.
static DriverModuleVerdict ScreenModuleForDriver(const Vector<Uint>& spirv, Bool allowInvalid,
String* outReason = nullptr);
// How many times ScreenModuleForDriver actually ran the validator (cache misses), for
// the tests that pin the cache.
static Uint64 DriverModuleValidatorRuns();
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
@@ -6835,7 +6835,7 @@ void main() {
// A refused program cannot reach here today - the full path refuses before it ever
// records a snapshot - but declining the fast path costs one compare and means the
// refusal does not depend on that ordering staying true.
if (programObj.pointSizeCapabilityUnsupported) {
if (programObj.pointSizeCapabilityUnsupported || programObj.invalidSpirvDeclined) {
return false;
}
@@ -7318,7 +7318,7 @@ void main() {
// The build already said why, once, naming the program and the stage. Refusing here -
// before any pipeline is built from it - is what makes that message a decline rather
// than a note attached to invalid usage the driver still receives.
if (programObj.pointSizeCapabilityUnsupported) {
if (programObj.pointSizeCapabilityUnsupported || programObj.invalidSpirvDeclined) {
return false;
}
// For the snapshot's memoised entry pointer: if anything below inserts into the
@@ -7778,6 +7778,8 @@ void main() {
}
ProgramFactory::CompileOptionFlags transformFlags = 0;
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
// No shader module of a program that failed validation exists; the build said why, once.
if (programObj.invalidSpirvDeclined) return;
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
@@ -7840,6 +7842,8 @@ void main() {
}
ProgramFactory::CompileOptionFlags transformFlags = 0;
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
// No shader module of a program that failed validation exists; the build said why, once.
if (programObj.invalidSpirvDeclined) return;
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
@@ -126,3 +126,11 @@ MGL_WIRE_DECLINE(MipmapDepthStencilAspect)
// is missing, or the three group-count words lie outside it or are misaligned.
MGL_WIRE_DECLINE(DispatchIndirectBufferUnbound)
MGL_WIRE_DECLINE(DispatchIndirectRange)
// ---- ProgramFactory::ScreenModuleForDriver ------------------------------------------------
// One of the bound program's final modules fails SPIR-V validation, so no shader module of it was
// created (VkProgramObject::invalidSpirvDeclined): SetupWireDraw refuses the draw, and the two
// dispatch arms the dispatch. A driver handed the module could fault the GPU into device loss.
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV=1 builds the module anyway, and then neither row moves.
MGL_WIRE_DECLINE(ProgramInvalidSpirv)
MGL_WIRE_DECLINE(ComputeProgramInvalidSpirv)
@@ -230,6 +230,10 @@
flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
programObject = &m_programFactory->GetOrCreateProgram(program, flags);
}
if (programObject->invalidSpirvDeclined) {
MGL_WIRE_DECLINE_AT(ProgramInvalidSpirv, "a module of the program fails SPIR-V validation and was never built");
return false;
}
if (programObject->pointSizeCapabilityUnsupported) {
MGL_WIRE_DECLINE_AT(PointSizeCapability, "the program writes gl_PointSize and the device cannot express it");
return false;
@@ -775,6 +779,10 @@
return;
}
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
if (programObj.invalidSpirvDeclined) {
MGL_WIRE_DECLINE_AT(ComputeProgramInvalidSpirv, "a module of the compute program fails SPIR-V validation and was never built");
return;
}
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
return;
@@ -847,6 +855,10 @@
return;
}
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
if (programObj.invalidSpirvDeclined) {
MGL_WIRE_DECLINE_AT(ComputeProgramInvalidSpirv, "a module of the compute program fails SPIR-V validation and was never built");
return;
}
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
return;
+55 -1
View File
@@ -32,6 +32,7 @@
#include <dlfcn.h>
#endif
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
namespace MobileGL::MG_Impl::EGLImpl {
@@ -216,6 +217,38 @@ namespace MobileGL::MG_Impl::EGLImpl {
return *surfaces;
}
// EGL_KHR_surfaceless_context. The EGL state records a surfaceless binding as exactly that
// (eglGetCurrentSurface answers EGL_NO_SURFACE), but every backend renders into the surface
// it was bound to, and a session's capabilities are published on its first bind to one. So
// the backend is bound to a 1x1 pbuffer of the display's own instead: one per display, made
// on the first surfaceless bind, never handed to the application, and gone with the display
// (eglTerminate). Guarded by EGLOperationMutex.
std::unordered_map<EGLDisplay, EGLSurface>& SurfacelessStandIns() {
static auto* standIns = new std::unordered_map<EGLDisplay, EGLSurface>();
return *standIns;
}
// The display's stand-in, made now if it has none (or the one it had did not survive).
// EGL_NO_SURFACE when the backend could not make a pbuffer; the EGL error is then set.
EGLSurface SurfacelessStandIn(EGLStateContext* state, EGLDisplay dpy) {
auto& standIns = SurfacelessStandIns();
if (const auto it = standIns.find(dpy); it != standIns.end()) {
if (state->ValidateSurfaceOnDisplay(dpy, it->second)) return it->second;
standIns.erase(it);
}
// Any of the display's configs will do: a stand-in is never read back or presented.
EGLConfig config = nullptr;
EGLint count = 0;
if (!state->GetConfigs(dpy, &config, 1, &count) || count < 1 || config == nullptr) {
state->SetError(EGL_BAD_ALLOC);
return EGL_NO_SURFACE;
}
const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
const EGLSurface standIn = CreatePbufferSurface(dpy, config, attribs);
if (standIn != EGL_NO_SURFACE) standIns[dpy] = standIn;
return standIn;
}
#if MOBILEGL_WAYLAND_WINDOWS
// Every Wayland window surface's presentation, by surface. Guarded by EGLOperationMutex,
// which every entry point that reaches it holds.
@@ -620,7 +653,22 @@ namespace MobileGL::MG_Impl::EGLImpl {
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
return EGL_FALSE;
}
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
// A surfaceless binding (EGL_KHR_surfaceless_context; EGLState took it above and records
// EGL_NO_SURFACE): the backend gets the display's stand-in pbuffer to render into.
EGLSurface backendDraw = draw;
EGLSurface backendRead = read;
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE) {
backendDraw = backendRead = SurfacelessStandIn(state, dpy);
if (backendDraw == EGL_NO_SURFACE) {
const EGLint error = state->ConsumeError();
MGLOG_E_ONCE("eglMakeCurrent: no stand-in pbuffer for a surfaceless binding thread=%s (error=0x%04x)",
threadId.c_str(), error);
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
state->SetError(error != EGL_SUCCESS ? error : EGL_BAD_ALLOC);
return EGL_FALSE;
}
}
if (!backendObject->MakeEGLCurrent(dpy, backendDraw, backendRead, ctx)) {
MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
dpy, draw, read, ctx);
state->SetError(EGL_BAD_ACCESS);
@@ -700,6 +748,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state->TerminateDisplay(dpy)) {
return EGL_FALSE;
}
{
// The display's surfaces went with it, its surfaceless stand-in among them; the backend
// side goes with ReleaseEGLResources below.
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
SurfacelessStandIns().erase(dpy);
}
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
// The display's dma-buf EGLImages went with it; so do their shared-image references.
for (const Uint64 sharedImageId : state->TakeOrphanedSharedImages()) {
@@ -19,14 +19,16 @@ namespace MobileGL::MG_Impl::EGLImpl {
"EGL_EXT_client_extensions EGL_EXT_platform_base EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
inline constexpr const char* kDisplayExtensionString =
"EGL_KHR_create_context EGL_EXT_platform_base EGL_KHR_platform_base "
"EGL_KHR_create_context EGL_KHR_surfaceless_context EGL_KHR_no_config_context EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
// The display list again, with the dma-buf import a backend with shared images has (a split
// client: the server allocates the images and names the descriptors that come back). A
// monolith has no allocator to recognise a dma-buf with, so it never advertises these. The
// same server answers the buffer age of its surfaces and takes swap damage to its presents.
inline constexpr const char* kDisplayExtensionStringWithSharedImages =
"EGL_KHR_create_context EGL_EXT_platform_base EGL_KHR_platform_base "
"EGL_KHR_create_context EGL_KHR_surfaceless_context EGL_KHR_no_config_context EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS
" EGL_KHR_image_base EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers"
" EGL_EXT_buffer_age EGL_KHR_partial_update EGL_KHR_swap_buffers_with_damage"
+12 -2
View File
@@ -659,7 +659,10 @@ namespace MobileGL {
SetError(EGL_NOT_INITIALIZED);
return nullptr;
}
if (!ValidateConfigOnDisplay(display, config)) {
// EGL_KHR_no_config_context: EGL_NO_CONFIG_KHR is a context with no config, which
// every surface is compatible with. eglinfo's ES probe relies on it: it chooses with
// EGL_OPENGL_ES_BIT (ES 1.x), which no config here claims, and passes the NULL on.
if (config != EGL_NO_CONFIG_KHR && !ValidateConfigOnDisplay(display, config)) {
SetError(EGL_BAD_CONFIG);
return nullptr;
}
@@ -802,6 +805,11 @@ namespace MobileGL {
*value = contextObject->EGLContextFlags;
return true;
case EGL_CONFIG_ID: {
// EGL_KHR_no_config_context: a context created without a config reports 0.
if (contextObject->Config == EGL_NO_CONFIG_KHR) {
*value = 0;
return true;
}
const auto* cfg = TryGetConfig(contextObject->Config);
if (!cfg) {
const_cast<EGLContext*>(this)->SetError(EGL_BAD_CONFIG);
@@ -1333,7 +1341,9 @@ namespace MobileGL {
SetError(EGL_BAD_CONTEXT);
return false;
}
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE) {
// EGL_KHR_surfaceless_context: with BOTH surfaces absent the context is bound
// surfacelessly (no default framebuffer). One without the other is still a mismatch.
if ((draw == EGL_NO_SURFACE) != (read == EGL_NO_SURFACE)) {
SetError(EGL_BAD_MATCH);
return false;
}
@@ -135,6 +135,51 @@ TEST(EGLStateProfile, CompatibilityProfileRequiresExplicitCompatBit) {
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
}
// What `eglinfo -B` does for every API: eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx)
// (EGL_KHR_surfaceless_context). It used to be EGL_BAD_MATCH, so eglinfo printed the EGL lines and
// no GL renderer/version lines at all. The binding records no surface: a client asking for the
// current surface must see what it bound.
TEST(EGLStateMakeCurrent, SurfacelessBindIsAcceptedAndRecordsNoSurface) {
auto fixture = CreateFixture();
EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, fixture->Context));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
EXPECT_EQ(fixture->State.GetCurrentContext(), fixture->Context);
EXPECT_EQ(fixture->State.GetCurrentSurface(EGL_DRAW), EGL_NO_SURFACE);
EXPECT_EQ(fixture->State.GetCurrentSurface(EGL_READ), EGL_NO_SURFACE);
EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
}
// The extension relaxes only the both-absent case: one surface without the other stays a mismatch.
TEST(EGLStateMakeCurrent, OneSidedSurfaceIsStillAMismatch) {
auto fixture = CreateFixture();
EXPECT_FALSE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, EGL_NO_SURFACE, fixture->Context));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_MATCH);
EXPECT_FALSE(fixture->State.MakeCurrent(fixture->Display, EGL_NO_SURFACE, fixture->Surface, fixture->Context));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_MATCH);
}
// eglinfo's ES probe chooses with EGL_RENDERABLE_TYPE/EGL_CONFORMANT = EGL_OPENGL_ES_BIT (ES 1.x),
// which no MobileGL config claims, so it passes the NULL config on to eglCreateContext - legal
// with EGL_KHR_no_config_context, where it means "no config". Without it that was
// EGL_BAD_CONFIG and the "OpenGL ES profile" lines never printed.
TEST(EGLStateCreateContext, NoConfigContextIsAcceptedAndReportsConfigIdZero) {
auto fixture = CreateFixture();
const EGLint esAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
const auto context = fixture->State.CreateContext(fixture->Display, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, esAttribs);
ASSERT_NE(context, EGL_NO_CONTEXT);
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
EGLint configId = -1;
EXPECT_TRUE(fixture->State.QueryContext(fixture->Display, context, EGL_CONFIG_ID, &configId));
EXPECT_EQ(configId, 0);
EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, context));
EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
// A non-null handle that names no config is still EGL_BAD_CONFIG.
EXPECT_EQ(fixture->State.CreateContext(fixture->Display, reinterpret_cast<EGLConfig>(0xdead), EGL_NO_CONTEXT,
esAttribs),
EGL_NO_CONTEXT);
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_CONFIG);
}
namespace {
constexpr EGLAttrib kAbgr8888 = StateContext::kDrmFourccAbgr8888;
constexpr EGLAttrib kXbgr8888 = StateContext::kDrmFourccXbgr8888;
+1
View File
@@ -5,6 +5,7 @@ add_executable(
PipelineQuirkTest.cpp
PassthroughTessControlTest.cpp
ViewportIndexReflectionTest.cpp
DriverModuleScreenTest.cpp
)
target_include_directories(PipelineQuirkTest PRIVATE
@@ -0,0 +1,183 @@
// MobileGL - MobileGL/MG_Test/Pipeline/DriverModuleScreenTest.cpp
// 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
//
// ProgramFactory::ScreenModuleForDriver is the last look Magma takes at a module's bytes before
// vkCreateShaderModule. A driver is not a validating entry point: a module that carried a sampler
// inside the default uniform block (a frontend translation defect) was accepted by Adreno, which
// then page-faulted on a descriptor read out of uniform-buffer bytes and lost the device. The
// screen used to report such a module and hand it over anyway; it must DECLINE it, so the GPU
// never sees it, unless MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV asks otherwise.
//
// The invalid module here is that exact shape, assembled by hand so it stays invalid whatever
// the frontend does: a Block-decorated uniform struct with a sampled-image member.
#include <gtest/gtest.h>
#include <chrono>
#include <cstdio>
#include "Includes.h"
#include "Init.h"
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include "spirv-tools/libspirv.hpp"
using namespace MobileGL;
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
using Verdict = ProgramFactory::DriverModuleVerdict;
namespace {
// Shared by both modules: a fragment shader whose default uniform block holds a vec4 and,
// in the invalid one, a sampler2D next to it.
String UniformBlockFragment(Bool withSampler) {
String text =
"OpCapability Shader\n"
"OpMemoryModel Logical GLSL450\n"
"OpEntryPoint Fragment %main \"main\" %out\n"
"OpExecutionMode %main OriginUpperLeft\n"
"OpDecorate %out Location 0\n"
"OpDecorate %Block Block\n"
"OpMemberDecorate %Block 0 Offset 0\n"
"OpDecorate %ubo DescriptorSet 0\n"
"OpDecorate %ubo Binding 0\n"
"%void = OpTypeVoid\n"
"%fn = OpTypeFunction %void\n"
"%float = OpTypeFloat 32\n"
"%v4 = OpTypeVector %float 4\n"
"%int = OpTypeInt 32 1\n"
"%zero = OpConstant %int 0\n"
"%img = OpTypeImage %float 2D 0 0 0 1 Unknown\n"
"%simg = OpTypeSampledImage %img\n";
text += withSampler ? "%Block = OpTypeStruct %v4 %simg\n" : "%Block = OpTypeStruct %v4\n";
text +=
"%pBlock = OpTypePointer Uniform %Block\n"
"%ubo = OpVariable %pBlock Uniform\n"
"%pv4u = OpTypePointer Uniform %v4\n"
"%pv4o = OpTypePointer Output %v4\n"
"%out = OpVariable %pv4o Output\n"
"%main = OpFunction %void None %fn\n"
"%entry = OpLabel\n"
"%ptr = OpAccessChain %pv4u %ubo %zero\n"
"%color = OpLoad %v4 %ptr\n"
"OpStore %out %color\n"
"OpReturn\n"
"OpFunctionEnd\n";
return text;
}
Vector<Uint> Assemble(const String& text) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
std::vector<uint32_t> words;
EXPECT_TRUE(tools.Assemble(text, &words)) << text;
return Vector<Uint>(words.begin(), words.end());
}
// A realistic module out of the real frontend, for the cost measurement.
Vector<Uint> CompileFragment(const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!binaryResult || binaryResult->empty()) return {};
const auto& words = binaryResult->front();
return Vector<Uint>(words.begin(), words.end());
}
class DriverModuleScreenTest : public ::testing::Test {
protected:
void SetUp() override { MobileGL::Initialize(); }
};
} // namespace
// The headline: the sampler-in-uniform-block module never reaches the driver. Red before the
// screen acted on its verdict - it answered AdmittedInvalid, i.e. "logged, handed over anyway".
TEST_F(DriverModuleScreenTest, ASamplerInsideTheDefaultUniformBlockIsDeclined) {
const Vector<Uint> invalid = Assemble(UniformBlockFragment(/*withSampler=*/true));
ASSERT_FALSE(invalid.empty());
String reason;
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(invalid, /*allowInvalid=*/false, &reason),
Verdict::DeclinedInvalid);
EXPECT_FALSE(reason.empty()) << "a decline must carry the validator's reason for its log line";
}
// The positive control: the same block without the sampler validates and is handed over. A screen
// that declined everything would pass the case above and fail this one.
TEST_F(DriverModuleScreenTest, TheSameBlockWithoutTheSamplerPasses) {
const Vector<Uint> valid = Assemble(UniformBlockFragment(/*withSampler=*/false));
ASSERT_FALSE(valid.empty());
String reason = "stale";
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(valid, /*allowInvalid=*/false, &reason), Verdict::Valid);
EXPECT_TRUE(reason.empty());
}
// The escape hatch: with MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV the invalid module is handed over,
// and the verdict says it was invalid rather than pretending it validated.
TEST_F(DriverModuleScreenTest, TheEscapeHatchAdmitsAnInvalidModuleAndSaysSo) {
const Vector<Uint> invalid = Assemble(UniformBlockFragment(/*withSampler=*/true));
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(invalid, /*allowInvalid=*/true), Verdict::AdmittedInvalid);
const Vector<Uint> valid = Assemble(UniformBlockFragment(/*withSampler=*/false));
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(valid, /*allowInvalid=*/true), Verdict::Valid);
}
// The verdict is remembered by content: re-screening the same bytes does not run spirv-val again,
// and a different module does. Also reports what one validation costs next to a cached answer.
TEST_F(DriverModuleScreenTest, TheVerdictIsCachedPerModuleContent) {
const Vector<Uint> module = CompileFragment(R"(#version 330 core
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform vec4 tint;
uniform mat4 colorMatrix;
in vec2 uv;
in vec4 vertexColor;
out vec4 fragColor;
vec3 tonemap(vec3 c) { return c / (c + vec3(1.0)); }
void main() {
vec4 base = texture(tex0, uv) * vertexColor;
vec4 detail = texture(tex1, uv * 4.0);
vec4 c = colorMatrix * mix(base, base * detail, 0.5) * tint;
for (int i = 0; i < 4; ++i) c.rgb += 0.01 * sin(c.gbr * float(i + 1));
fragColor = vec4(tonemap(c.rgb), c.a);
}
)");
ASSERT_FALSE(module.empty());
// A copy with one changed word in the bound field of the header: different bytes, same
// validity, so it must miss the cache.
Vector<Uint> sibling = module;
sibling[3] += 1;
const Uint64 runsBefore = ProgramFactory::DriverModuleValidatorRuns();
const auto t0 = std::chrono::steady_clock::now();
const Verdict first = ProgramFactory::ScreenModuleForDriver(module, false);
const auto t1 = std::chrono::steady_clock::now();
const Verdict second = ProgramFactory::ScreenModuleForDriver(module, false);
const auto t2 = std::chrono::steady_clock::now();
EXPECT_EQ(first, Verdict::Valid);
EXPECT_EQ(second, first);
EXPECT_EQ(ProgramFactory::DriverModuleValidatorRuns(), runsBefore + 1) << "the second screen re-validated";
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(sibling, false), Verdict::Valid);
EXPECT_EQ(ProgramFactory::DriverModuleValidatorRuns(), runsBefore + 2) << "different bytes shared a verdict";
const auto us = [](auto a, auto b) {
return static_cast<long long>(std::chrono::duration_cast<std::chrono::microseconds>(b - a).count());
};
std::printf("[ screen ] %zu-word module: validate %lld us, cached %lld us\n", module.size(), us(t0, t1),
us(t1, t2));
}
+6 -1
View File
@@ -67,6 +67,9 @@ if (NOT WIN32)
# MOBILEGL_IPC_SURFACE=server against a display-less `--serve` supervisor. Both drive real peers
# and (the second, and D8's case) the real server image, as PeerLatchTest does.
list(APPEND MOBILEGL_WIRE_TESTS InProcessServerTest ServerOwnedSurfaceTest)
# `eglinfo -B`'s per-API sequence (surfaceless make-current, the ES probe's NULL config) through
# a real MobileGL client against a real `--serve` supervisor, as ServerOwnedSurfaceTest does.
list(APPEND MOBILEGL_WIRE_TESTS EglInfoSequenceTest)
# P14 S2 (docs/Disaggregated/design/11-state-ownership.md): several sessions in ONE process.
# The in-process unix supervisor serves each session on its own control thread with its own
# ServerSession/ServerLoop/PipeInputs block/latch domain/segment resolver; two SHM clients are
@@ -99,6 +102,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
${MGL_ROOT}/3rdparty/flatbuffers/include
)
if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR
test STREQUAL "EglInfoSequenceTest" OR
test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest" OR test STREQUAL "ServerProbeTest")
# It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State
# the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy
@@ -128,7 +132,8 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
endif ()
if (test STREQUAL "ServerSpawnTest" OR test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR
test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "ServerProbeTest")
test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "EglInfoSequenceTest" OR test STREQUAL "AdoptTierStreamTest" OR
test STREQUAL "ServerProbeTest")
# It execs the real image, so it must be built first and must be told
# where it is. MOBILEGL_TEST_SERVER_PATH is read by the test's
# ServerImage() helper; without it the test would depend on the working
@@ -0,0 +1,276 @@
// MobileGL - MobileGL/MG_Test/Wire/EglInfoSequenceTest.cpp
// 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
// `eglinfo -B` AGAINST MOBILEGL, END TO END ON THE HOST: a real MobileGL client (the EGL entry
// points, the spawn/tcp session) against a real `--serve` supervisor, driven through the exact call
// sequence the distribution's eglinfo runs per display:
//
// eglInitialize; EGL_CLIENT_APIS must name "OpenGL" / "OpenGL_ES"; EGL_KHR_create_context decides
// the attribute form. Then, per API:
// eglBindAPI(api)
// eglChooseConfig({EGL_CONFORMANT, bit, EGL_RED/GREEN/BLUE/ALPHA_SIZE 1, EGL_RENDERABLE_TYPE,
// bit}) and take configs[0], or NULL when nothing matched
// desktop: eglCreateContext({MAJOR, MINOR, PROFILE_MASK}) from 4.6 down (core stops at 3.1)
// ES: eglCreateContext({EGL_CONTEXT_MAJOR_VERSION, n}) for n = 3, 2, 1
// eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx) - surfaceless
// glGetString(GL_VENDOR / GL_RENDERER / GL_VERSION) through eglGetProcAddress
//
// On the device eglinfo printed MobileGL's EGL lines for every platform and no GL lines at all. Two
// refusals, both red here before the fix:
// * the surfaceless eglMakeCurrent was EGL_BAD_MATCH, so no desktop context ever became current;
// * the ES probe's bit is EGL_OPENGL_ES_BIT (ES 1.x), which no MobileGL config claims, so the
// config is NULL and eglCreateContext answered EGL_BAD_CONFIG.
#include "P12ServerRig.h"
#include <MG_Impl/EGLImpl/EGLImpl.h>
using namespace MobileGL;
using namespace P12;
namespace {
namespace EGL = MobileGL::MG_Impl::EGLImpl;
constexpr unsigned kGlVendor = 0x1F00;
constexpr unsigned kGlRenderer = 0x1F01;
constexpr unsigned kGlVersion = 0x1F02;
// One "OpenGL ... profile" block of eglinfo's output, or why it is missing.
struct ProfileReport {
Int32 configFound = 0; // eglChooseConfig matched something
Int32 created = 0; // some eglCreateContext in the version walk returned a context
Int32 madeCurrent = 0; // ...and the surfaceless eglMakeCurrent took it
Int32 lastError = 0; // eglGetError after the last failed create / make-current
Int32 drawIsNone = 0; // eglGetCurrentSurface(EGL_DRAW) == EGL_NO_SURFACE while current
Int32 major = 0;
Int32 minor = 0;
char vendor[96] = {};
char renderer[96] = {};
char version[96] = {};
};
struct EglInfoReport {
Int32 initialized = 0;
Int32 eglMajor = 0;
Int32 eglMinor = 0;
Int32 hasOpenGL = 0;
Int32 hasOpenGLES = 0;
Int32 khrCreateContext = 0;
Int32 surfacelessExtensions = 0; // EGL_KHR_surfaceless_context and EGL_KHR_no_config_context advertised
ProfileReport core{};
ProfileReport compat{};
ProfileReport es{};
char note[128] = {};
};
// eglinfo's extension_supported(): a whole space-separated token.
bool HasToken(const char* list, const char* token) {
if (list == nullptr) return false;
const std::string all = std::string(" ") + list + " ";
return all.find(std::string(" ") + token + " ") != std::string::npos;
}
EGLConfig ChooseLikeEglInfo(EGLDisplay dpy, EGLint apiBit, ProfileReport& out) {
const EGLint attribs[] = {EGL_CONFORMANT, apiBit, EGL_RED_SIZE, 1, EGL_GREEN_SIZE, 1, EGL_BLUE_SIZE, 1,
EGL_ALPHA_SIZE, 1, EGL_RENDERABLE_TYPE, apiBit, EGL_NONE};
EGLConfig configs[64] = {};
EGLint count = 0;
(void)EGL::ChooseConfig(dpy, attribs, configs, 64, &count);
out.configFound = count > 0 ? 1 : 0;
return count > 0 ? configs[0] : nullptr;
}
// eglinfo's doOneContext, minus the printing.
void ReadStrings(ProfileReport& out) {
using GetStringFn = const unsigned char* (*)(unsigned);
const auto getString = reinterpret_cast<GetStringFn>(EGL::GetProcAddress("glGetString"));
if (getString == nullptr) return;
const auto copy = [&](unsigned name, char* into, size_t size) {
const unsigned char* text = getString(name);
std::snprintf(into, size, "%s", text != nullptr ? reinterpret_cast<const char*>(text) : "");
};
copy(kGlVendor, out.vendor, sizeof(out.vendor));
copy(kGlRenderer, out.renderer, sizeof(out.renderer));
copy(kGlVersion, out.version, sizeof(out.version));
}
// Returns the context made current, or EGL_NO_CONTEXT - eglinfo's createEGLContext.
EGLContext TryCurrent(EGLDisplay dpy, EGLConfig config, const EGLint* attribs, ProfileReport& out) {
const EGLContext ctx = EGL::CreateContext(dpy, config, EGL_NO_CONTEXT, attribs);
if (ctx == EGL_NO_CONTEXT) {
out.lastError = EGL::GetError();
return EGL_NO_CONTEXT;
}
out.created = 1;
if (EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx) != EGL_TRUE) {
out.lastError = EGL::GetError();
(void)EGL::DestroyContext(dpy, ctx);
return EGL_NO_CONTEXT;
}
out.madeCurrent = 1;
out.drawIsNone = EGL::GetCurrentSurface(EGL_DRAW) == EGL_NO_SURFACE ? 1 : 0;
return ctx;
}
void Release(EGLDisplay dpy, EGLContext ctx, ProfileReport& out) {
ReadStrings(out);
(void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
(void)EGL::DestroyContext(dpy, ctx);
}
void DesktopProfile(EGLDisplay dpy, EGLConfig config, bool core, bool khrCreateContext, ProfileReport& out) {
static const int kVersions[][2] = {{4, 6}, {4, 5}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0}, {3, 3},
{3, 2}, {3, 1}, {3, 0}, {2, 1}, {2, 0}, {1, 5}, {1, 4}, {1, 3},
{1, 2}, {1, 1}, {1, 0}};
for (const auto& v : kVersions) {
if (core && v[0] == 3 && v[1] == 0) return;
const EGLint withProfile[] = {EGL_CONTEXT_MAJOR_VERSION, v[0], EGL_CONTEXT_MINOR_VERSION, v[1],
EGL_CONTEXT_OPENGL_PROFILE_MASK,
core ? EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT
: EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
EGL_NONE};
const EGLint old[] = {EGL_CONTEXT_CLIENT_VERSION, v[0], EGL_NONE};
const EGLContext ctx = TryCurrent(dpy, config, khrCreateContext ? withProfile : old, out);
if (ctx != EGL_NO_CONTEXT) {
out.major = v[0];
out.minor = v[1];
Release(dpy, ctx, out);
return;
}
}
}
void EsProfile(EGLDisplay dpy, EGLConfig config, ProfileReport& out) {
for (int major = 3; major > 0; --major) {
const EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, major, EGL_NONE};
const EGLContext ctx = TryCurrent(dpy, config, attribs, out);
if (ctx != EGL_NO_CONTEXT) {
out.major = major;
Release(dpy, ctx, out);
return;
}
}
}
EglInfoReport RunEglInfoPeer(const ServerProcess& server, const std::string& clientLogBase) {
EglInfoReport report{};
int fds[2] = {-1, -1};
if (::pipe(fds) != 0) return report;
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid == 0) {
::close(fds[0]);
::setenv("MOBILEGL_LOG_FILE_PATH", clientLogBase.c_str(), 1);
::setenv("MOBILEGL_TRANSPORT", "spawn", 1);
::setenv("MOBILEGL_IPC_CONTROL", server.endpoint.c_str(), 1);
::setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1);
::unsetenv("MOBILEGL_IPC_SURFACE");
EglInfoReport r{};
// eglinfo walks every platform; surfaceless is the one a display-less container has.
const EGLDisplay dpy = EGL::GetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, nullptr);
EGLint major = 0;
EGLint minor = 0;
if (dpy == EGL_NO_DISPLAY || EGL::Initialize(dpy, &major, &minor) != EGL_TRUE) {
std::snprintf(r.note, sizeof(r.note), "eglInitialize failed (no spawn session?)");
} else {
r.initialized = 1;
r.eglMajor = major;
r.eglMinor = minor;
const char* apis = EGL::QueryString(dpy, EGL_CLIENT_APIS);
const char* exts = EGL::QueryString(dpy, EGL_EXTENSIONS);
r.hasOpenGL = HasToken(apis, "OpenGL") ? 1 : 0;
r.hasOpenGLES = HasToken(apis, "OpenGL_ES") ? 1 : 0;
const bool khr = major == 1 && minor >= 4 && HasToken(exts, "EGL_KHR_create_context");
r.khrCreateContext = khr ? 1 : 0;
r.surfacelessExtensions =
HasToken(exts, "EGL_KHR_surfaceless_context") && HasToken(exts, "EGL_KHR_no_config_context") ? 1 : 0;
if (r.hasOpenGL && EGL::BindAPI(EGL_OPENGL_API) == EGL_TRUE) {
const EGLConfig config = ChooseLikeEglInfo(dpy, EGL_OPENGL_BIT, r.core);
r.compat.configFound = r.core.configFound;
if (khr) DesktopProfile(dpy, config, /*core=*/true, khr, r.core);
DesktopProfile(dpy, config, /*core=*/false, khr, r.compat);
}
if (r.hasOpenGLES && EGL::BindAPI(EGL_OPENGL_ES_API) == EGL_TRUE) {
const EGLConfig config = ChooseLikeEglInfo(dpy, EGL_OPENGL_ES_BIT, r.es);
EsProfile(dpy, config, r.es);
}
(void)EGL::Terminate(dpy);
}
const ssize_t wrote = ::write(fds[1], &r, sizeof(r));
(void)wrote;
std::fflush(nullptr);
::_exit(0);
}
::close(fds[1]);
if (pid < 0) {
::close(fds[0]);
return report;
}
pollfd pfd{fds[0], POLLIN, 0};
if (::poll(&pfd, 1, 90000) > 0) {
if (::read(fds[0], &report, sizeof(report)) != static_cast<ssize_t>(sizeof(report))) {
report = EglInfoReport{};
std::snprintf(report.note, sizeof(report.note), "peer died before reporting");
}
} else {
std::snprintf(report.note, sizeof(report.note), "peer timed out");
::kill(pid, SIGKILL);
}
::close(fds[0]);
int status = 0;
::waitpid(pid, &status, 0);
return report;
}
std::string Describe(const char* label, const ProfileReport& p) {
char text[512];
std::snprintf(text, sizeof(text),
"%s: config=%d created=%d current=%d eglError=0x%04x version=%d.%d vendor='%s' renderer='%s' "
"GL_VERSION='%s'",
label, p.configFound, p.created, p.madeCurrent, p.lastError, p.major, p.minor, p.vendor,
p.renderer, p.version);
return text;
}
void ExpectProfilePrinted(const char* label, const ProfileReport& p) {
EXPECT_EQ(p.created, 1) << Describe(label, p);
EXPECT_EQ(p.madeCurrent, 1) << "the surfaceless eglMakeCurrent was refused - " << Describe(label, p);
EXPECT_EQ(p.drawIsNone, 1) << "a surfaceless binding reported a current draw surface - " << Describe(label, p);
EXPECT_NE(p.renderer[0], '\0') << "no renderer line - " << Describe(label, p);
EXPECT_NE(p.version[0], '\0') << "no version line - " << Describe(label, p);
}
} // namespace
TEST(EglInfoSequence, EveryProfilePrintsItsRendererAndVersion) {
ServerProcess server;
ASSERT_TRUE(LaunchSupervisor("eglinfo", &server)) << server.Log();
const std::string clientLog = "/tmp/mgl-eglinfo-client-" + std::to_string(::getpid()) + ".log";
Debug::TruncateRoleLogs(clientLog.c_str());
const EglInfoReport r = RunEglInfoPeer(server, clientLog);
ASSERT_EQ(r.initialized, 1) << r.note;
// eglinfo only takes the KHR_create_context attribute form on EGL >= 1.4 with the extension.
EXPECT_EQ(r.khrCreateContext, 1) << "EGL " << r.eglMajor << "." << r.eglMinor;
ASSERT_EQ(r.hasOpenGL, 1);
ASSERT_EQ(r.hasOpenGLES, 1);
EXPECT_EQ(r.surfacelessExtensions, 1) << "the display does not advertise what it now accepts";
std::printf("[ eglinfo ] %s\n[ eglinfo ] %s\n[ eglinfo ] %s\n", Describe("core", r.core).c_str(),
Describe("compat", r.compat).c_str(), Describe("es", r.es).c_str());
if (r.core.madeCurrent == 1 && r.core.renderer[0] == '\0')
GTEST_SKIP() << "the surfaceless binding was accepted; the strings need headless EGL on the server";
ExpectProfilePrinted("OpenGL core profile", r.core);
ExpectProfilePrinted("OpenGL compatibility profile", r.compat);
ExpectProfilePrinted("OpenGL ES profile", r.es);
std::error_code ec;
if (!::testing::Test::HasFailure()) {
std::filesystem::remove(Debug::RoleLogPath(clientLog.c_str(), Debug::LogRole::Client), ec);
std::filesystem::remove(Debug::RoleLogPath(clientLog.c_str(), Debug::LogRole::Server), ec);
std::filesystem::remove(clientLog, ec);
}
}
+1
View File
@@ -166,6 +166,7 @@ MobileGL supports runtime configuration via environment variables.
| `MOBILEGL_MAGMA_DISABLE_SUBGROUP` | Disable Vulkan shader subgroup support. | `0`, `1` | `0` |
| `MOBILEGL_ADVERTISE_FP64` | Advertise `GL_ARB_gpu_shader_fp64`. GLSL `double`/`dvec`/`dmat` compile and run either way - they are narrowed to 32 bits - so this only changes whether an application is told it has 64-bit precision, which it does not. | `0`, `1` | `0` |
| `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | Use Magma's R11G11B10F format fallback. | `0`, `1` | `0` |
| `MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV` | Hand a shader module that fails SPIR-V validation to the driver anyway. By default Magma declines such a program (no shader module is created, its draws and dispatches are refused and logged by name), because a driver that accepts invalid SPIR-V can fault the GPU into device loss. Diagnostic use only. | `0`, `1` | `0` |
| `MOBILEGL_MAGMA_FRAMESINFLIGHT` | Set Magma frames in flight. | Integer `1`–`64` | `3` |
| `MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER` | Draws and dispatches Magma records into one command buffer before it submits the buffer mid-frame and continues on a fresh one. `0` never splits. | Integer `0`–`16777216` | `16384` |
| `MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES` | Frames a frame slot's descriptor pools must stay under a quarter full before Magma frees the slot's cached descriptor sets and grown pools. `0` never trims. | Integer `0`–`1048576` | `120` |