mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Merge magma-validate: Magma SPIR-V validation declines and surfaceless EGL.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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` |
|
||||
|
||||
Reference in New Issue
Block a user