Files
MobileGL/MobileGL/MG_Util/SelfTest/DriverPost.cpp
T

1294 lines
70 KiB
C++

// MobileGL - MobileGL/MG_Util/SelfTest/DriverPost.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
#include "DriverPost.h"
#include "MG_Util/BackendLoaders/OpenGL/Loader.h"
#include <Config.h>
#include <MGGitHash.h>
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
// Only for the compile-time MAX_VERTEX_ATTRIBS constant asserted below. The POST still executes no
// MG_State code: it runs standalone, before MG_State::Init().
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
#include <chrono>
#include <thread>
#if !defined(_WIN32)
#include <dlfcn.h>
#endif
namespace MobileGL::MG_Util::SelfTest {
namespace {
// Display ranks for PostCheck::displayRank: within one backend section, FAIL
// rows render first, then WARN, PASS, INFO, then the device-driver identity
// strings, and always last (regardless of status) the strings MobileGL itself
// reports to applications. Rows are stable-sorted, so relative order within a
// rank is preserved. Purely cosmetic: the verdict computation is unaffected.
enum DisplayRank : Int {
RankFail = 0,
RankWarn = 1,
RankPass = 2,
RankInfo = 3,
RankDriverReported = 4,
RankMobileGLReported = 5,
};
struct ReportBuilder {
BackendPostReport report;
Bool fatalFailed = false;
Bool warnUnmet = false;
void Pass(String name, String detail) {
report.checks.push_back({Move(name), "PASS", Move(detail), RankPass});
}
void Fail(String name, String detail) {
fatalFailed = true;
report.checks.push_back({Move(name), "FAIL", Move(detail), RankFail});
}
void Warn(String name, String detail) {
warnUnmet = true;
report.checks.push_back({Move(name), "WARN", Move(detail), RankWarn});
}
void Info(String name, String detail) {
report.checks.push_back({Move(name), "INFO", Move(detail), RankInfo});
}
// A "Backend driver reported ..." identity string straight from the device
// driver; rendered after the regular rows.
void DriverReported(String name, String detail) {
report.checks.push_back({Move(name), "INFO", Move(detail), RankDriverReported});
}
// A "MobileGL reported ..." string: what MobileGL itself reports to
// applications on this backend; always rendered at the very bottom.
void MobileGLReported(String name, String detail) {
report.checks.push_back({Move(name), "INFO", Move(detail), RankMobileGLReported});
}
void Finalize() {
report.verdict = fatalFailed ? "UNSUPPORTED" : (warnUnmet ? "DEGRADED" : "OK");
std::stable_sort(report.checks.begin(), report.checks.end(),
[](const PostCheck& a, const PostCheck& b) { return a.displayRank < b.displayRank; });
}
};
// ---- "MobileGL reported ..." row assembly -------------------------------
// The vendor/version/renderer strings mirror GL_Getter.cpp's GL_VENDOR /
// GL_VERSION / GL_RENDERER cases; the backend API version string and the
// extension list come from the per-backend single-source-of-truth helpers
// (GetRendererIdentity / FormatBackendAPIVersionString /
// BuildAdvertisedExtensions) shared with the real backends.
// Mirrors GL_Getter.cpp's GL_VENDOR case.
String BuildReportedGLVendor(const RendererInfo& identity) {
if (identity.ExtraVendor.has_value()) {
return format("{}{}", MG_Config::CoreVendor, identity.ExtraVendor.value());
}
return MG_Config::CoreVendor;
}
// Mirrors GL_Getter.cpp's GL_VERSION case.
String BuildReportedGLVersion(const RendererInfo& identity) {
return format("{} {} {}, {} Backend, GIT@" GIT_COMMIT_HASH_SHORT,
identity.RendererGLInfo.TargetGLVersion.toString(), MG_Config::ProjectName,
MG_Config::CoreVersion.toFormattedString(MG_Config::DefaultVersionStringFormatAttrib),
identity.BackendName);
}
// Mirrors GL_Getter.cpp's GL_RENDERER case.
String BuildReportedGLRenderer(const RendererInfo& identity, const String& backendApiVersionString) {
return format("{} ({}) ({})", identity.RendererName, MG_Config::CoreName, backendApiVersionString);
}
// Mirrors GL_Getter.cpp's GL_EXTENSIONS case (space-separated).
String JoinAdvertisedExtensions(const Vector<GLExtension>& extensions) {
String result;
for (const auto& extension : extensions) {
if (!result.empty()) {
result += " ";
}
result += ConvertGLExtToString(extension);
}
return result;
}
// Appends the four "MobileGL reported ..." rows for one backend section.
// GL_VENDOR and GL_VERSION only depend on the backend's static identity, so
// they are always concrete; GL_RENDERER and GL_EXTENSIONS need data from the
// device probe and degrade to an explanatory detail when it failed.
void AppendMobileGLReportedRows(ReportBuilder& builder, const RendererInfo& identity,
const Optional<String>& backendApiVersionString,
const Optional<String>& advertisedExtensions) {
static const String Unavailable = "unavailable (backend probe failed)";
builder.MobileGLReported("MobileGL reported GL_VENDOR", BuildReportedGLVendor(identity));
builder.MobileGLReported("MobileGL reported GL_VERSION", BuildReportedGLVersion(identity));
builder.MobileGLReported("MobileGL reported GL_RENDERER",
backendApiVersionString.has_value()
? BuildReportedGLRenderer(identity, backendApiVersionString.value())
: Unavailable);
builder.MobileGLReported("MobileGL reported GL_EXTENSIONS",
advertisedExtensions.has_value() ? advertisedExtensions.value() : Unavailable);
}
// Runs a callable when the enclosing scope exits, so driver teardown still happens
// even if a String/format allocation throws while report rows are being built.
template <typename Callable>
struct ScopeGuard {
explicit ScopeGuard(Callable callable) : onExit(Move(callable)) {}
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
~ScopeGuard() { onExit(); }
private:
Callable onExit;
};
String EGLErrorSuffix(const MG_External::EGLFunctionsTable& eglFuncs) {
if (!eglFuncs.eglGetError) {
return "";
}
return format(" (EGL error 0x{:x})", eglFuncs.eglGetError());
}
// Suffix folded into each backend's single "Timer queries" row when the user
// disabled timer queries; the note rides along with whatever combined verdict
// the row carries instead of being a standalone INFO row, and spells out the
// cause (the environment variable) and its consequence explicitly.
String TimerQueryDisabledNote() {
return MG_Config::Features.DisableTimerQuery
? "; environment variable MOBILEGL_DISABLE_TIMERQUERY is set, disabling timer "
"queries as a result"
: "";
}
// ---- Vertex attribute limit --------------------------------------------
// GL 3.3 Core mandates GL_MAX_VERTEX_ATTRIBS >= 16 (spec table 6.32); a driver below
// that cannot back a conformant core context at all.
constexpr Int kGL33MinVertexAttribs = 16;
// The capacity of the per-context current-vertex-attribute array, which is also the width of
// the Uint32 attribute masks the backends pass around. Pinned to the state layer's constant so
// the two can never drift: a mismatch between them is precisely the defect this row guards.
constexpr Int kMobileGLMaxVertexAttribs = MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS;
static_assert(kMobileGLMaxVertexAttribs <= 32, "Vertex attribute masks are Uint32");
static_assert(kMobileGLMaxVertexAttribs >= kGL33MinVertexAttribs,
"MobileGL cannot advertise a conformant GL 3.3 Core GL_MAX_VERTEX_ATTRIBS");
// Both backends index a fixed-size, per-context array of current generic vertex attribute
// values by shader input location, and both clamp the GL_MAX_VERTEX_ATTRIBS they advertise
// to that array's capacity. A driver reporting more attributes than the array can hold used
// to make the DirectVulkan draw path walk locations past the end of it -- an out-of-bounds
// read in release builds, and a MOBILEGL_ASSERT abort in debug builds -- as soon as a shader
// declared a vertex input at a high location whose array was disabled. The clamp closes that
// hole, so this row exists to make the underlying driver/host mismatch visible rather than
// silently swallowed.
void EvaluateVertexAttribLimit(ReportBuilder& builder, Int deviceLimit, const char* rowName,
const char* driverLimitName) {
if (deviceLimit < kGL33MinVertexAttribs) {
builder.Fail(rowName,
format("{} = {} (< {}); OpenGL 3.3 Core requires at least {} generic vertex "
"attributes, so this driver cannot back a conformant core context",
driverLimitName, deviceLimit, kGL33MinVertexAttribs, kGL33MinVertexAttribs));
return;
}
if (deviceLimit > kMobileGLMaxVertexAttribs) {
builder.Warn(rowName,
format("{} = {} (> {}); MobileGL clamps GL_MAX_VERTEX_ATTRIBS to {} because its "
"current-vertex-attribute storage and its Uint32 attribute masks hold {} "
"locations, so the driver's extra attributes stay unusable",
driverLimitName, deviceLimit, kMobileGLMaxVertexAttribs,
kMobileGLMaxVertexAttribs, kMobileGLMaxVertexAttribs));
return;
}
builder.Pass(rowName, format("{} = {}; MobileGL advertises GL_MAX_VERTEX_ATTRIBS = {}",
driverLimitName, deviceLimit, deviceLimit));
}
void EvaluateGlesChecklist(ReportBuilder& builder, const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& glesFuncs) {
const Int major = caps.GLESVersion.Major;
const Int minor = caps.GLESVersion.Minor;
const Bool es31 = major > 3 || (major == 3 && minor >= 1);
const Bool es32 = major > 3 || (major == 3 && minor >= 2);
const String versionLabel = format("OpenGL ES {}.{}", major, minor);
if (es32) {
builder.Pass("OpenGL ES version", versionLabel + " (>= 3.2, full native feature set)");
} else if (es31) {
builder.Warn("OpenGL ES version",
versionLabel +
" (compute shaders and native indirect draws available; ES 3.2 is recommended)");
} else {
builder.Fail("OpenGL ES version",
versionLabel + " (< 3.1: no compute shaders or native indirect draws)");
}
EvaluateVertexAttribLimit(builder, caps.MaxVertexAttribs, "Vertex attributes",
"GL_MAX_VERTEX_ATTRIBS");
if (caps.SupportsPolygonMode) {
builder.Pass("Polygon mode",
"glPolygonMode GL_LINE/GL_POINT available via GL_NV/ANGLE_polygon_mode");
} else {
builder.Warn("Polygon mode",
"no GL_NV/ANGLE_polygon_mode; glPolygonMode GL_LINE/GL_POINT falls back to GL_FILL");
}
if (caps.SupportsIndexedColorMask) {
builder.Pass("Indexed color mask",
"per-draw-buffer glColorMaski available (ES 3.2 core or draw_buffers_indexed)");
} else {
builder.Warn("Indexed color mask",
"no indexed glColorMaski; per-draw-buffer color masks fall back to draw buffer 0");
}
if (caps.SupportsDualSourceBlend) {
builder.Pass("Dual-source blend",
"GL_SRC1_* dual-source blend factors available via GL_EXT_blend_func_extended");
} else {
builder.Warn("Dual-source blend",
"no GL_EXT_blend_func_extended; GL_SRC1_* dual-source blend factors hard-fail at draw");
}
if (es31) {
GLint maxVertexSsboBlocks = 0;
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexSsboBlocks);
while (glesFuncs.glGetError && glesFuncs.glGetError() != GL_NO_ERROR) {
}
if (maxVertexSsboBlocks >= 1) {
builder.Pass("Vertex shader storage blocks",
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}", maxVertexSsboBlocks));
} else {
builder.Warn("Vertex shader storage blocks",
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}; the Flywheel/Create indirect draw "
"machinery cannot read indirect command buffers from the vertex stage",
maxVertexSsboBlocks));
}
if (caps.MaxShaderStorageBufferBindings >= 8) {
builder.Pass("Shader storage buffer bindings",
format("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS = {} (the last binding is reserved "
"for mg_IndirectParams)",
caps.MaxShaderStorageBufferBindings));
} else {
builder.Warn("Shader storage buffer bindings",
format("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS = {} (< 8); reserving the last "
"binding for mg_IndirectParams leaves little room for app SSBOs",
caps.MaxShaderStorageBufferBindings));
}
}
if (caps.SupportsPersistentMapping) {
builder.Pass("GL_EXT_buffer_storage", "supported (persistent buffer mapping)");
} else {
builder.Info("GL_EXT_buffer_storage",
"not supported; no impact today: the frontend fully emulates persistent "
"mapping regardless of this extension");
}
if (caps.SupportsBaseInstance) {
builder.Pass("GL_EXT_base_instance", "supported (native baseInstance draws)");
} else {
builder.Info("GL_EXT_base_instance",
"not supported; no impact: the native indirect path deliberately does not "
"rely on it (shader-side emulation handles baseInstance semantics)");
}
if (caps.SupportsNorm16Texture) {
builder.Pass("GL_EXT_texture_norm16", "supported");
} else {
builder.Warn("GL_EXT_texture_norm16",
"not supported; 16-bit normalized texture formats need emulation");
}
builder.Info("Indirect gl_InstanceID semantics",
caps.IndirectDrawInstanceIdIncludesBaseInstance
? "includes baseInstance (ANGLE-style; MobileGL's shader rewrite keeps gl_InstanceID "
"zero-based)"
: "conforming (zero-based)");
builder.DriverReported("Backend driver reported GL_VENDOR", caps.GLESVendorString);
builder.DriverReported("Backend driver reported GL_RENDERER", caps.GLESRendererString);
builder.DriverReported("Backend driver reported GL_VERSION", caps.GLESVersionString);
}
// Single "Timer queries" row: GL_EXT_disjoint_timer_query presence and a real
// GL_TIME_ELAPSED_EXT span around a trivial workload on the probe context fold
// into one combined verdict (WARN when absent, PASS when the probe works, FAIL
// naming the step that broke). Requires the probe context to still be current.
void ProbeGlesTimerQuery(ReportBuilder& builder, const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& glesFuncs) {
const String disabledNote = TimerQueryDisabledNote();
if (!caps.SupportsDisjointTimerQuery) {
builder.Warn("Timer queries",
"GL_EXT_disjoint_timer_query not supported; timer queries unavailable; "
"Minecraft F3 GPU% will not show" +
disabledNote);
return;
}
// Every emit carries the extension-presence fact the old standalone
// GL_EXT_disjoint_timer_query row showed, plus the probe outcome.
const String extensionPresent = "GL_EXT_disjoint_timer_query extension present";
const auto fail = [&](const String& detail) {
builder.Fail("Timer queries", extensionPresent + "; but " + detail + disabledNote);
};
if (!glesFuncs.glGenQueries || !glesFuncs.glDeleteQueries || !glesFuncs.glBeginQuery ||
!glesFuncs.glEndQuery || !glesFuncs.glGetQueryObjectuiv || !glesFuncs.glGetQueryObjectui64vEXT ||
!glesFuncs.glClearColor || !glesFuncs.glClear || !glesFuncs.glFlush || !glesFuncs.glFinish ||
!glesFuncs.glGetError) {
fail("the query entry points did not resolve through eglGetProcAddress");
return;
}
// Drain stale errors so probe failures are attributable to the probe itself.
while (glesFuncs.glGetError() != GL_NO_ERROR) {
}
GLuint queryId = 0;
glesFuncs.glGenQueries(1, &queryId);
if (queryId == 0) {
fail("glGenQueries did not return a query object");
return;
}
const ScopeGuard deleteQuery([&]() { glesFuncs.glDeleteQueries(1, &queryId); });
glesFuncs.glBeginQuery(GL_TIME_ELAPSED_EXT, queryId);
// Trivial workload inside the span: clear the 1x1 probe pbuffer and flush.
glesFuncs.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glesFuncs.glClear(GL_COLOR_BUFFER_BIT);
glesFuncs.glFlush();
glesFuncs.glEndQuery(GL_TIME_ELAPSED_EXT);
glesFuncs.glFinish();
const GLenum spanError = glesFuncs.glGetError();
if (spanError != GL_NO_ERROR) {
fail(format("GL error 0x{:x} while recording the GL_TIME_ELAPSED_EXT span", spanError));
return;
}
// glFinish already drained the GPU, so a conforming driver reports the
// result available immediately; the bounded loop only covers drivers
// that latch availability lazily. Paced at ~100us per poll to match
// the runtime GetQueryResult64 wait loop, bounding the worst case
// at ~100ms so a broken driver cannot stall the POST.
GLuint available = 0;
for (Int attempt = 0; attempt < 1000 && available == 0; ++attempt) {
glesFuncs.glGetQueryObjectuiv(queryId, GL_QUERY_RESULT_AVAILABLE, &available);
if (available == 0) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
if (available == 0) {
fail("GL_QUERY_RESULT_AVAILABLE never became true after glFinish "
"(1000 polls over ~100ms)");
return;
}
GLuint64 elapsedNs = 0;
glesFuncs.glGetQueryObjectui64vEXT(queryId, GL_QUERY_RESULT, &elapsedNs);
const GLenum resultError = glesFuncs.glGetError();
if (resultError != GL_NO_ERROR) {
fail(format("GL error 0x{:x} while reading GL_QUERY_RESULT", resultError));
return;
}
builder.Pass("Timer queries",
extensionPresent + format("; timer query functional (probe observed {} ns)", elapsedNs) +
disabledNote);
}
// Everything the "MobileGL reported ..." rows need from the GLES device probe.
struct GlesProbeSummary {
Bool capsValid = false;
MG_External::GLESCapabilities caps{};
};
} // namespace
// The GLES device probe proper. Split out of RunGlesDriverPost so that the
// "MobileGL reported ..." rows are appended on every path (including early
// probe failures) before the report is finalized.
//
// The whole EGL bring-up chain (library load, display init, API bind, config,
// pbuffer surface, context) is one "ES3 context" row. The detail accumulates one
// completed-stage description per stage so no sub-fact of the old per-stage rows
// is lost: PASS enumerates every stage's result, FAIL lists the stages that
// completed and then names the exact stage that broke with its detail string.
static void ProbeGlesDriver(ReportBuilder& builder, GlesProbeSummary& summary) {
String chain;
const auto stageDone = [&](const String& description) {
if (!chain.empty()) {
chain += "; ";
}
chain += description;
};
const auto failStage = [&](const String& stage, const String& detail) {
builder.Fail("ES3 context", (chain.empty() ? "" : chain + "; but ") + stage + ": " + detail);
};
MG_External::EGLFunctionsTable eglFuncs{};
BackendLoader::AcquireEGLFunctions(eglFuncs);
const Bool eglLoaded = eglFuncs.eglGetDisplay && eglFuncs.eglInitialize && eglFuncs.eglBindAPI &&
eglFuncs.eglChooseConfig && eglFuncs.eglCreatePbufferSurface &&
eglFuncs.eglCreateContext && eglFuncs.eglMakeCurrent && eglFuncs.eglDestroySurface &&
eglFuncs.eglDestroyContext && eglFuncs.eglTerminate && eglFuncs.eglGetProcAddress;
if (!eglLoaded) {
failStage("EGL library", "libEGL.so or one of its required entry points is missing");
return;
}
stageDone("libEGL.so loaded with all required entry points");
EGLDisplay display = eglFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
failStage("EGL display", "eglGetDisplay returned EGL_NO_DISPLAY");
return;
}
EGLint eglMajor = 0;
EGLint eglMinor = 0;
if (!eglFuncs.eglInitialize(display, &eglMajor, &eglMinor)) {
failStage("EGL display", "eglInitialize failed on the default display" + EGLErrorSuffix(eglFuncs));
return;
}
stageDone(format("EGL {}.{} initialized on the default display", eglMajor, eglMinor));
builder.report.available = true;
EGLSurface surface = EGL_NO_SURFACE;
EGLContext context = EGL_NO_CONTEXT;
const ScopeGuard eglTeardown([&]() {
eglFuncs.eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (surface != EGL_NO_SURFACE) {
eglFuncs.eglDestroySurface(display, surface);
}
if (context != EGL_NO_CONTEXT) {
eglFuncs.eglDestroyContext(display, context);
}
// eglTerminate is deliberately not called: the probe shares EGL_DEFAULT_DISPLAY with
// the process UI renderer (HWUI), and terminating it can invalidate the UI's EGL
// objects on pre-refcounting Android builds. Unbinding and destroying our own
// surface/context is sufficient cleanup.
});
do {
if (!eglFuncs.eglBindAPI(EGL_OPENGL_ES_API)) {
failStage("OpenGL ES API bind", "eglBindAPI(EGL_OPENGL_ES_API) failed" + EGLErrorSuffix(eglFuncs));
break;
}
stageDone("eglBindAPI(EGL_OPENGL_ES_API) succeeded");
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
EGL_NONE};
EGLConfig config = nullptr;
EGLint numConfigs = 0;
if (!eglFuncs.eglChooseConfig(display, configAttribs, &config, 1, &numConfigs)) {
failStage("ES3 RGBA8888 pbuffer config", "eglChooseConfig failed" + EGLErrorSuffix(eglFuncs));
break;
}
if (numConfigs < 1) {
// No EGL error suffix here: eglChooseConfig succeeded, so it would read EGL_SUCCESS.
failStage("ES3 RGBA8888 pbuffer config", "no ES3-capable RGBA8888 pbuffer config");
break;
}
stageDone("ES3-renderable RGBA8888 pbuffer config found");
const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
surface = eglFuncs.eglCreatePbufferSurface(display, config, surfaceAttribs);
if (surface == EGL_NO_SURFACE) {
failStage("1x1 pbuffer surface", "eglCreatePbufferSurface failed" + EGLErrorSuffix(eglFuncs));
break;
}
stageDone("1x1 probe surface created");
const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
context = eglFuncs.eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
if (context == EGL_NO_CONTEXT) {
failStage("OpenGL ES 3 context", "eglCreateContext failed" + EGLErrorSuffix(eglFuncs));
break;
}
if (!eglFuncs.eglMakeCurrent(display, surface, surface, context)) {
failStage("OpenGL ES 3 context", "eglMakeCurrent failed" + EGLErrorSuffix(eglFuncs));
break;
}
stageDone("ES 3 context created and made current");
builder.Pass("ES3 context", chain);
MG_External::GLESFunctionsTable glesFuncs{};
BackendLoader::AcquireGLESFunctions(glesFuncs, eglFuncs.eglGetProcAddress);
if (!BackendLoader::FillInGLESCapabilities(summary.caps, glesFuncs)) {
builder.Fail("GLES capability query",
"required GLES entry points could not be resolved through eglGetProcAddress");
break;
}
summary.capsValid = true;
const MG_External::GLESCapabilities& caps = summary.caps;
builder.report.rendererInfo = format("{} ({})", caps.GLESRendererString, caps.GLESVersionString);
EvaluateGlesChecklist(builder, caps, glesFuncs);
ProbeGlesTimerQuery(builder, caps, glesFuncs);
builder.report.formatCapabilities.emplace();
MG_Backend::DirectGLES::PopulateFormatCapabilities(
glesFuncs, caps, builder.report.formatCapabilities.value());
} while (false);
}
BackendPostReport RunGlesDriverPost() {
MGLOG_I("Driver POST: probing the device GLES driver");
ReportBuilder builder;
GlesProbeSummary summary;
ProbeGlesDriver(builder, summary);
// "MobileGL reported ..." rows: what applications running on the DirectGLES
// backend (Espryt) would see. The backend API version string and the extension
// list are built from the probe's own capability data through the same helpers
// the real backend uses, so they cannot drift.
Optional<String> backendApiVersionString;
Optional<String> advertisedExtensions;
if (summary.capsValid) {
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
builder.Finalize();
MGLOG_I("Driver POST: GLES verdict = %s", builder.report.verdict.c_str());
return builder.report;
}
namespace {
// The Vulkan loader is bootstrapped through dlopen + vkGetInstanceProcAddr instead of
// static linking so the POST also works in build configurations that do not link a
// Vulkan loader (and degrades gracefully when the device ships none). The library
// handle is intentionally never closed: Android Vulkan ICDs may register threads and
// state that do not survive unloading, and the loader stays resident for the real
// backend anyway.
void* OpenVulkanLoaderLibrary() {
#if defined(_WIN32)
return reinterpret_cast<void*>(LoadLibraryA("vulkan-1.dll"));
#else
static const char* const LoaderNames[] = {
#if defined(__APPLE__)
"libvulkan.dylib",
"libvulkan.1.dylib",
"libMoltenVK.dylib",
#else
"libvulkan.so.1",
"libvulkan.so",
#endif
};
for (const char* name : LoaderNames) {
if (void* library = dlopen(name, RTLD_LOCAL | RTLD_NOW)) {
MGLOG_I("Driver POST: loaded Vulkan loader library: %s", name);
return library;
}
}
return nullptr;
#endif
}
void* VulkanLoaderSymbol(void* library, const char* name) {
#if defined(_WIN32)
return reinterpret_cast<void*>(GetProcAddress(reinterpret_cast<HMODULE>(library), name));
#else
return dlsym(library, name);
#endif
}
Bool HasVkExtension(const Vector<VkExtensionProperties>& extensions, const char* name) {
return std::any_of(extensions.begin(), extensions.end(), [name](const VkExtensionProperties& extension) {
return std::strcmp(extension.extensionName, name) == 0;
});
}
String VkApiVersionToString(Uint32 version) {
return format("{}.{}.{}", VK_VERSION_MAJOR(version), VK_VERSION_MINOR(version),
VK_VERSION_PATCH(version));
}
// Real timestamp-query probe, emitting the backend's single "Timer queries" row:
// a throwaway logical device records two vkCmdWriteTimestamp(BOTTOM_OF_PIPE)
// queries and reads them back. Both outcomes state the validBits and period
// values (the facts of the old standalone rows): PASS adds the observed span,
// FAIL names the step (and VkResult) that broke. Every created object is torn
// down from a scope guard before the caller's instance guard runs.
void ProbeVulkanTimerQuery(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
VkInstance instance, VkPhysicalDevice physicalDevice,
Uint32 graphicsQueueFamilyIndex, Uint32 timestampValidBits,
Float timestampPeriod) {
const String disabledNote = TimerQueryDisabledNote();
const String timestampFacts =
format("timestampValidBits = {} on the graphics queue family; timestampPeriod = {} ns per tick",
timestampValidBits, timestampPeriod);
const auto fail = [&](const String& detail) {
builder.Fail("Timer queries", timestampFacts + "; but " + detail + disabledNote);
};
const auto vkCreateDeviceFn =
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
const auto vkDestroyDeviceFn =
reinterpret_cast<PFN_vkDestroyDevice>(getInstanceProcAddr(instance, "vkDestroyDevice"));
const auto vkGetDeviceQueueFn =
reinterpret_cast<PFN_vkGetDeviceQueue>(getInstanceProcAddr(instance, "vkGetDeviceQueue"));
const auto vkCreateCommandPoolFn =
reinterpret_cast<PFN_vkCreateCommandPool>(getInstanceProcAddr(instance, "vkCreateCommandPool"));
const auto vkDestroyCommandPoolFn =
reinterpret_cast<PFN_vkDestroyCommandPool>(getInstanceProcAddr(instance, "vkDestroyCommandPool"));
const auto vkAllocateCommandBuffersFn = reinterpret_cast<PFN_vkAllocateCommandBuffers>(
getInstanceProcAddr(instance, "vkAllocateCommandBuffers"));
const auto vkBeginCommandBufferFn =
reinterpret_cast<PFN_vkBeginCommandBuffer>(getInstanceProcAddr(instance, "vkBeginCommandBuffer"));
const auto vkEndCommandBufferFn =
reinterpret_cast<PFN_vkEndCommandBuffer>(getInstanceProcAddr(instance, "vkEndCommandBuffer"));
const auto vkCreateQueryPoolFn =
reinterpret_cast<PFN_vkCreateQueryPool>(getInstanceProcAddr(instance, "vkCreateQueryPool"));
const auto vkDestroyQueryPoolFn =
reinterpret_cast<PFN_vkDestroyQueryPool>(getInstanceProcAddr(instance, "vkDestroyQueryPool"));
const auto vkCmdResetQueryPoolFn =
reinterpret_cast<PFN_vkCmdResetQueryPool>(getInstanceProcAddr(instance, "vkCmdResetQueryPool"));
const auto vkCmdWriteTimestampFn =
reinterpret_cast<PFN_vkCmdWriteTimestamp>(getInstanceProcAddr(instance, "vkCmdWriteTimestamp"));
const auto vkCreateFenceFn =
reinterpret_cast<PFN_vkCreateFence>(getInstanceProcAddr(instance, "vkCreateFence"));
const auto vkDestroyFenceFn =
reinterpret_cast<PFN_vkDestroyFence>(getInstanceProcAddr(instance, "vkDestroyFence"));
const auto vkWaitForFencesFn =
reinterpret_cast<PFN_vkWaitForFences>(getInstanceProcAddr(instance, "vkWaitForFences"));
const auto vkQueueSubmitFn =
reinterpret_cast<PFN_vkQueueSubmit>(getInstanceProcAddr(instance, "vkQueueSubmit"));
const auto vkGetQueryPoolResultsFn = reinterpret_cast<PFN_vkGetQueryPoolResults>(
getInstanceProcAddr(instance, "vkGetQueryPoolResults"));
const auto vkDeviceWaitIdleFn =
reinterpret_cast<PFN_vkDeviceWaitIdle>(getInstanceProcAddr(instance, "vkDeviceWaitIdle"));
if (vkCreateDeviceFn == nullptr || vkDestroyDeviceFn == nullptr || vkGetDeviceQueueFn == nullptr ||
vkCreateCommandPoolFn == nullptr || vkDestroyCommandPoolFn == nullptr ||
vkAllocateCommandBuffersFn == nullptr || vkBeginCommandBufferFn == nullptr ||
vkEndCommandBufferFn == nullptr || vkCreateQueryPoolFn == nullptr ||
vkDestroyQueryPoolFn == nullptr || vkCmdResetQueryPoolFn == nullptr ||
vkCmdWriteTimestampFn == nullptr || vkCreateFenceFn == nullptr || vkDestroyFenceFn == nullptr ||
vkWaitForFencesFn == nullptr || vkQueueSubmitFn == nullptr || vkGetQueryPoolResultsFn == nullptr ||
vkDeviceWaitIdleFn == nullptr) {
fail("vkGetInstanceProcAddr could not resolve the entry points required for the "
"timestamp probe");
return;
}
const Float queuePriority = 1.0f;
VkDeviceQueueCreateInfo queueInfo{};
queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = &queuePriority;
VkDeviceCreateInfo deviceInfo{};
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
VkDevice device = VK_NULL_HANDLE;
VkResult result = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device);
if (result != VK_SUCCESS || device == VK_NULL_HANDLE) {
fail(format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandPool commandPool = VK_NULL_HANDLE;
VkQueryPool queryPool = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
Bool fenceWaitTimedOut = false;
// Same teardown-on-every-path style as the caller's instance guard; runs
// before that guard, so device objects die before the instance does. The
// idle wait keeps an in-flight submission from racing object destruction.
const ScopeGuard destroyDeviceObjects([&]() {
if (fenceWaitTimedOut) {
// The probe fence never signaled within its timeout, so the
// submission may still be executing - or the GPU is hung.
// vkDeviceWaitIdle could then block forever and destroying
// in-flight objects is undefined, so the probe deliberately
// leaks the device objects (device, pools, fence): a hung
// GPU must not hang the POST.
return;
}
vkDeviceWaitIdleFn(device);
if (fence != VK_NULL_HANDLE) {
vkDestroyFenceFn(device, fence, nullptr);
}
if (queryPool != VK_NULL_HANDLE) {
vkDestroyQueryPoolFn(device, queryPool, nullptr);
}
if (commandPool != VK_NULL_HANDLE) {
vkDestroyCommandPoolFn(device, commandPool, nullptr);
}
vkDestroyDeviceFn(device, nullptr);
});
VkQueue queue = VK_NULL_HANDLE;
vkGetDeviceQueueFn(device, graphicsQueueFamilyIndex, 0, &queue);
if (queue == VK_NULL_HANDLE) {
fail("vkGetDeviceQueue returned a null graphics queue");
return;
}
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
result = vkCreateCommandPoolFn(device, &poolInfo, nullptr, &commandPool);
if (result != VK_SUCCESS) {
fail(format("vkCreateCommandPool failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
result = vkAllocateCommandBuffersFn(device, &allocInfo, &commandBuffer);
if (result != VK_SUCCESS) {
fail(format("vkAllocateCommandBuffers failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkQueryPoolCreateInfo queryPoolInfo{};
queryPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
queryPoolInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
queryPoolInfo.queryCount = 2;
result = vkCreateQueryPoolFn(device, &queryPoolInfo, nullptr, &queryPool);
if (result != VK_SUCCESS) {
fail(format("vkCreateQueryPool failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
result = vkBeginCommandBufferFn(commandBuffer, &beginInfo);
if (result != VK_SUCCESS) {
fail(format("vkBeginCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
return;
}
vkCmdResetQueryPoolFn(commandBuffer, queryPool, 0, 2);
vkCmdWriteTimestampFn(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool, 0);
vkCmdWriteTimestampFn(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool, 1);
result = vkEndCommandBufferFn(commandBuffer);
if (result != VK_SUCCESS) {
fail(format("vkEndCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
result = vkCreateFenceFn(device, &fenceInfo, nullptr, &fence);
if (result != VK_SUCCESS) {
fail(format("vkCreateFence failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
result = vkQueueSubmitFn(queue, 1, &submitInfo, fence);
if (result != VK_SUCCESS) {
fail(format("vkQueueSubmit failed (VkResult = {})", static_cast<Int>(result)));
return;
}
constexpr Uint64 FenceTimeoutNs = 5'000'000'000ull; // 5 s: a POST must never hang the launcher
result = vkWaitForFencesFn(device, 1, &fence, VK_TRUE, FenceTimeoutNs);
if (result != VK_SUCCESS) {
// Skip the teardown idle wait too (see the scope guard): the
// submission is still pending on a possibly-hung GPU.
fenceWaitTimedOut = true;
fail(format("vkWaitForFences did not signal within 5 s (VkResult = {})",
static_cast<Int>(result)));
return;
}
Uint64 timestamps[2] = {0, 0};
result = vkGetQueryPoolResultsFn(device, queryPool, 0, 2, sizeof(timestamps), timestamps,
sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
if (result != VK_SUCCESS) {
fail(format("vkGetQueryPoolResults failed (VkResult = {})", static_cast<Int>(result)));
return;
}
const Uint64 validMask =
timestampValidBits >= 64 ? ~0ull : ((1ull << timestampValidBits) - 1ull);
const Uint64 t0 = timestamps[0] & validMask;
const Uint64 t1 = timestamps[1] & validMask;
if (t1 < t0) {
fail(format("timestamps are not monotonic (t0 = {}, t1 = {})", t0, t1));
return;
}
const Uint64 elapsedNs =
static_cast<Uint64>(static_cast<Double>(t1 - t0) * static_cast<Double>(timestampPeriod));
builder.Pass("Timer queries",
timestampFacts +
format("; timer query functional (t1 >= t0, probe observed {} ns)", elapsedNs) +
disabledNote);
}
// Everything the "MobileGL reported ..." rows need from the Vulkan device probe.
struct VulkanProbeSummary {
Bool devicePropsValid = false;
String deviceName;
String apiVersionString;
String driverVersionString; // raw hex, vendor-encoded (see RunVulkanDriverPost)
Bool shaderSubgroupUsable = false;
Bool timerQueriesSupported = false;
Bool samplerAnisotropySupported = false;
};
} // namespace
// The Vulkan device probe proper. Split out of RunVulkanDriverPost so that the
// "MobileGL reported ..." rows are appended on every path (including early
// probe failures) before the report is finalized.
//
// The loader bring-up chain (dlopen, instance API version, vkCreateInstance) is one
// "Vulkan instance" row, and the two required surface instance extensions are one
// "Surface extensions" row. Details carry every sub-fact of the old per-stage rows:
// PASS enumerates each stage's result (and each extension's presence), FAIL lists
// the stages that completed and then names the exact stage that broke (or states
// per extension whether it is present or missing) with the stage detail strings.
static void ProbeVulkanDriver(ReportBuilder& builder, VulkanProbeSummary& summary) {
String instanceChain;
const auto instanceStageDone = [&](const String& description) {
if (!instanceChain.empty()) {
instanceChain += "; ";
}
instanceChain += description;
};
const auto failInstanceStage = [&](const String& stage, const String& detail) {
builder.Fail("Vulkan instance",
(instanceChain.empty() ? "" : instanceChain + "; but ") + stage + ": " + detail);
};
void* loaderLibrary = OpenVulkanLoaderLibrary();
if (loaderLibrary == nullptr) {
failInstanceStage("Vulkan loader", "libvulkan.so could not be loaded; no Vulkan loader on this device");
return;
}
const auto getInstanceProcAddr =
reinterpret_cast<PFN_vkGetInstanceProcAddr>(VulkanLoaderSymbol(loaderLibrary, "vkGetInstanceProcAddr"));
if (getInstanceProcAddr == nullptr) {
failInstanceStage("Vulkan loader", "vkGetInstanceProcAddr is missing from the Vulkan loader library");
return;
}
instanceStageDone("Vulkan loader library loaded and vkGetInstanceProcAddr resolved");
const auto vkCreateInstanceFn =
reinterpret_cast<PFN_vkCreateInstance>(getInstanceProcAddr(nullptr, "vkCreateInstance"));
const auto vkEnumerateInstanceVersionFn = reinterpret_cast<PFN_vkEnumerateInstanceVersion>(
getInstanceProcAddr(nullptr, "vkEnumerateInstanceVersion"));
const auto vkEnumerateInstanceExtensionPropertiesFn =
reinterpret_cast<PFN_vkEnumerateInstanceExtensionProperties>(
getInstanceProcAddr(nullptr, "vkEnumerateInstanceExtensionProperties"));
Uint32 instanceApiVersion = VK_API_VERSION_1_0;
if (vkEnumerateInstanceVersionFn != nullptr) {
vkEnumerateInstanceVersionFn(&instanceApiVersion);
}
if (vkCreateInstanceFn == nullptr || vkEnumerateInstanceVersionFn == nullptr ||
instanceApiVersion < VK_API_VERSION_1_1) {
failInstanceStage("Instance API version",
format("instance API {} (< 1.1); the DirectVulkan backend requires a Vulkan 1.1 "
"instance",
VkApiVersionToString(instanceApiVersion)));
return;
}
instanceStageDone(format("instance API {}", VkApiVersionToString(instanceApiVersion)));
Vector<VkExtensionProperties> instanceExtensions;
if (vkEnumerateInstanceExtensionPropertiesFn != nullptr) {
Uint32 extensionCount = 0;
if (vkEnumerateInstanceExtensionPropertiesFn(nullptr, &extensionCount, nullptr) == VK_SUCCESS &&
extensionCount > 0) {
instanceExtensions.resize(extensionCount);
if (vkEnumerateInstanceExtensionPropertiesFn(nullptr, &extensionCount, instanceExtensions.data()) ==
VK_SUCCESS) {
instanceExtensions.resize(extensionCount);
} else {
instanceExtensions.clear();
}
}
}
// One row for the required surface instance extensions; the detail states each
// extension's presence individually, and a missing one carries the "required
// instance extension" fact plus its consequence from the old per-extension rows.
{
String surfaceDetail;
Bool anySurfaceExtensionMissing = false;
const auto recordExtension = [&](const char* name, const char* consequence) {
if (!surfaceDetail.empty()) {
surfaceDetail += "; ";
}
if (HasVkExtension(instanceExtensions, name)) {
surfaceDetail += format("{} instance extension present", name);
} else {
anySurfaceExtensionMissing = true;
surfaceDetail += format("{} missing (required instance extension; {})", name, consequence);
}
};
recordExtension(VK_KHR_SURFACE_EXTENSION_NAME, "on-screen rendering is impossible");
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
recordExtension(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, "ANativeWindow surfaces cannot be created");
#endif
if (anySurfaceExtensionMissing) {
builder.Fail("Surface extensions", surfaceDetail);
} else {
builder.Pass("Surface extensions", surfaceDetail);
}
}
// The probe never creates a surface, so the instance is created without extensions.
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "MobileGL Driver POST";
appInfo.pEngineName = "MobileGL";
appInfo.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo instanceInfo{};
instanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instanceInfo.pApplicationInfo = &appInfo;
VkInstance instance = VK_NULL_HANDLE;
const VkResult createResult = vkCreateInstanceFn(&instanceInfo, nullptr, &instance);
if (createResult != VK_SUCCESS || instance == VK_NULL_HANDLE) {
failInstanceStage("Vulkan instance creation",
format("vkCreateInstance failed (VkResult = {})", static_cast<Int>(createResult)));
return;
}
instanceStageDone("Vulkan 1.1 instance created");
builder.Pass("Vulkan instance", instanceChain);
const auto vkDestroyInstanceFn =
reinterpret_cast<PFN_vkDestroyInstance>(getInstanceProcAddr(instance, "vkDestroyInstance"));
const auto vkEnumeratePhysicalDevicesFn = reinterpret_cast<PFN_vkEnumeratePhysicalDevices>(
getInstanceProcAddr(instance, "vkEnumeratePhysicalDevices"));
const auto vkGetPhysicalDevicePropertiesFn = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties>(
getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties"));
const auto vkGetPhysicalDeviceQueueFamilyPropertiesFn =
reinterpret_cast<PFN_vkGetPhysicalDeviceQueueFamilyProperties>(
getInstanceProcAddr(instance, "vkGetPhysicalDeviceQueueFamilyProperties"));
const auto vkGetPhysicalDeviceFeaturesFn = reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures>(
getInstanceProcAddr(instance, "vkGetPhysicalDeviceFeatures"));
const auto vkEnumerateDeviceExtensionPropertiesFn =
reinterpret_cast<PFN_vkEnumerateDeviceExtensionProperties>(
getInstanceProcAddr(instance, "vkEnumerateDeviceExtensionProperties"));
const auto vkGetPhysicalDeviceFeatures2Fn = reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures2>(
getInstanceProcAddr(instance, "vkGetPhysicalDeviceFeatures2"));
const auto vkGetPhysicalDeviceProperties2Fn = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties2"));
const auto vkGetPhysicalDeviceFormatPropertiesFn =
reinterpret_cast<PFN_vkGetPhysicalDeviceFormatProperties>(
getInstanceProcAddr(instance, "vkGetPhysicalDeviceFormatProperties"));
// The instance is destroyed from a scope guard so it is released on every early-return
// path and even if a String/format allocation throws while report rows are being built.
const ScopeGuard destroyInstance([&]() {
if (vkDestroyInstanceFn != nullptr) {
vkDestroyInstanceFn(instance, nullptr);
}
});
if (vkEnumeratePhysicalDevicesFn == nullptr || vkGetPhysicalDevicePropertiesFn == nullptr ||
vkGetPhysicalDeviceQueueFamilyPropertiesFn == nullptr || vkGetPhysicalDeviceFeaturesFn == nullptr ||
vkEnumerateDeviceExtensionPropertiesFn == nullptr) {
builder.Fail("Vulkan core entry points",
"vkGetInstanceProcAddr could not resolve required Vulkan 1.0 functions");
return;
}
// Device discovery (physical device enumeration, graphics queue selection, device
// API version) is one "Graphics device" row; FAIL names the failing stage.
Uint32 deviceCount = 0;
const VkResult countResult = vkEnumeratePhysicalDevicesFn(instance, &deviceCount, nullptr);
if (countResult != VK_SUCCESS) {
builder.Fail("Graphics device", format("vkEnumeratePhysicalDevices failed (VkResult = {})",
static_cast<Int>(countResult)));
return;
}
if (deviceCount == 0) {
builder.Fail("Graphics device", "no Vulkan physical devices found");
return;
}
builder.report.available = true;
Vector<VkPhysicalDevice> devices(deviceCount);
const VkResult enumerateResult = vkEnumeratePhysicalDevicesFn(instance, &deviceCount, devices.data());
if (enumerateResult != VK_SUCCESS) {
builder.Fail("Graphics device", format("vkEnumeratePhysicalDevices failed (VkResult = {})",
static_cast<Int>(enumerateResult)));
return;
}
devices.resize(deviceCount);
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
Uint32 graphicsQueueFamilyIndex = 0;
Uint32 graphicsQueueTimestampValidBits = 0;
for (VkPhysicalDevice candidate : devices) {
Uint32 queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyPropertiesFn(candidate, &queueFamilyCount, nullptr);
Vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyPropertiesFn(candidate, &queueFamilyCount, queueFamilies.data());
for (Uint32 familyIndex = 0; familyIndex < queueFamilyCount; ++familyIndex) {
const VkQueueFamilyProperties& family = queueFamilies[familyIndex];
if (family.queueCount > 0 && (family.queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0) {
physicalDevice = candidate;
graphicsQueueFamilyIndex = familyIndex;
graphicsQueueTimestampValidBits = family.timestampValidBits;
break;
}
}
if (physicalDevice != VK_NULL_HANDLE) {
break;
}
}
if (physicalDevice == VK_NULL_HANDLE) {
builder.Fail("Graphics device",
format("none of the {} physical device(s) exposes a graphics queue family", deviceCount));
return;
}
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDevicePropertiesFn(physicalDevice, &properties);
// driverVersion is vendor-encoded (each vendor packs its own bit layout), so it is
// reported as raw hex instead of being decoded with the VK_VERSION_* macros.
const String driverVersionString = format("0x{:08x}", properties.driverVersion);
builder.report.rendererInfo = format("{} (Vulkan {}, driver {})", String(properties.deviceName),
VkApiVersionToString(properties.apiVersion), driverVersionString);
summary.devicePropsValid = true;
summary.deviceName = String(properties.deviceName);
summary.apiVersionString = VkApiVersionToString(properties.apiVersion);
summary.driverVersionString = driverVersionString;
// The chosen-device facts (name, enumeration count, graphics queue) ride along
// on both outcomes so the device API verdict never hides them.
const String deviceFacts =
format("{} ({} device(s) enumerated, picked the first with a graphics queue); "
"graphics queue family present",
String(properties.deviceName), deviceCount);
if (properties.apiVersion >= VK_API_VERSION_1_1) {
builder.Pass("Graphics device",
deviceFacts +
format("; device API Vulkan {}", VkApiVersionToString(properties.apiVersion)));
} else {
builder.Fail("Graphics device",
deviceFacts + format("; but Device API version: Vulkan {} (< 1.1); the DirectVulkan "
"backend requires a Vulkan 1.1 device",
VkApiVersionToString(properties.apiVersion)));
}
EvaluateVertexAttribLimit(builder, static_cast<Int>(properties.limits.maxVertexInputAttributes),
"Vertex attributes", "maxVertexInputAttributes");
Vector<VkExtensionProperties> deviceExtensions;
Uint32 deviceExtensionCount = 0;
if (vkEnumerateDeviceExtensionPropertiesFn(physicalDevice, nullptr, &deviceExtensionCount, nullptr) ==
VK_SUCCESS &&
deviceExtensionCount > 0) {
deviceExtensions.resize(deviceExtensionCount);
if (vkEnumerateDeviceExtensionPropertiesFn(physicalDevice, nullptr, &deviceExtensionCount,
deviceExtensions.data()) == VK_SUCCESS) {
deviceExtensions.resize(deviceExtensionCount);
} else {
deviceExtensions.clear();
}
}
if (HasVkExtension(deviceExtensions, VK_KHR_SWAPCHAIN_EXTENSION_NAME)) {
builder.Pass("VK_KHR_swapchain", "device extension present");
} else {
builder.Fail("VK_KHR_swapchain", "required device extension missing; presentation is impossible");
}
VkPhysicalDeviceFeatures features{};
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
if (features.multiDrawIndirect == VK_TRUE) {
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
} else {
builder.Warn("multiDrawIndirect",
"unsupported; indirect multi-draw batches fall back to one draw per command");
}
if (features.drawIndirectFirstInstance == VK_TRUE) {
builder.Pass("drawIndirectFirstInstance", "indirect commands may carry a non-zero firstInstance");
} else {
builder.Warn("drawIndirectFirstInstance",
"unsupported; indirect commands with a non-zero baseInstance cannot run natively");
}
if (features.vertexPipelineStoresAndAtomics == VK_TRUE) {
builder.Pass("vertexPipelineStoresAndAtomics",
"supported by driver (not currently enabled by the DirectVulkan backend)");
} else {
builder.Warn("vertexPipelineStoresAndAtomics",
"unsupported; shaders that write storage buffers from the vertex stage will not work");
}
if (features.fillModeNonSolid == VK_TRUE) {
builder.Pass("fillModeNonSolid", "glPolygonMode GL_LINE/GL_POINT rasterization supported");
} else {
builder.Warn("fillModeNonSolid",
"unsupported; glPolygonMode GL_LINE/GL_POINT falls back to GL_FILL (no wireframe/point "
"rasterization)");
}
if (features.independentBlend == VK_TRUE) {
builder.Pass("independentBlend", "per-draw-buffer glColorMaski and indexed blend state supported");
} else {
builder.Warn("independentBlend",
"unsupported; per-draw-buffer glColorMaski falls back to draw buffer 0 for all attachments");
}
if (features.dualSrcBlend == VK_TRUE) {
builder.Pass("dualSrcBlend", "GL_SRC1_* dual-source blend factors supported");
} else {
builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw");
}
Bool shaderDrawParameters = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
VkPhysicalDeviceShaderDrawParametersFeatures drawParametersFeatures{};
drawParametersFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES;
VkPhysicalDeviceFeatures2 features2{};
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
features2.pNext = &drawParametersFeatures;
vkGetPhysicalDeviceFeatures2Fn(physicalDevice, &features2);
shaderDrawParameters = drawParametersFeatures.shaderDrawParameters == VK_TRUE;
} else if (HasVkExtension(deviceExtensions, VK_KHR_SHADER_DRAW_PARAMETERS_EXTENSION_NAME)) {
// Vulkan 1.0 device: the extension alone exposes the SPIR-V DrawParameters capability.
shaderDrawParameters = true;
}
if (shaderDrawParameters) {
builder.Pass("shaderDrawParameters", "gl_DrawID/gl_BaseVertex/gl_BaseInstance shaders supported");
} else {
builder.Warn("shaderDrawParameters",
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
}
Bool primitiveTopologyListRestart = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr &&
HasVkExtension(deviceExtensions, VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME)) {
VkPhysicalDevicePrimitiveTopologyListRestartFeaturesEXT listRestartFeatures{};
listRestartFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVE_TOPOLOGY_LIST_RESTART_FEATURES_EXT;
VkPhysicalDeviceFeatures2 features2{};
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
features2.pNext = &listRestartFeatures;
vkGetPhysicalDeviceFeatures2Fn(physicalDevice, &features2);
primitiveTopologyListRestart = listRestartFeatures.primitiveTopologyListRestart == VK_TRUE;
}
if (primitiveTopologyListRestart) {
builder.Pass("primitiveTopologyListRestart",
"primitive restart supported on list topologies (GL_PRIMITIVE_RESTART)");
} else {
builder.Warn("primitiveTopologyListRestart",
"unsupported; primitive restart works on strip/fan topologies only, list-topology restart "
"hard-fails at draw");
}
if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
VkPhysicalDeviceProperties2 properties2{};
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
properties2.pNext = &subgroupProperties;
vkGetPhysicalDeviceProperties2Fn(physicalDevice, &properties2);
const Bool subgroupUsable = subgroupProperties.subgroupSize > 0 &&
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
// Same usability rule as the Vulkan capability loader's
// HasUsableShaderSubgroupSupport, which feeds the GL_KHR_shader_subgroup
// advertisement of the real backend.
summary.shaderSubgroupUsable = subgroupUsable;
if (subgroupUsable) {
builder.Pass("Compute shader subgroup",
format("basic subgroup operations in compute, subgroup size {}",
subgroupProperties.subgroupSize));
} else {
builder.Warn("Compute shader subgroup",
"basic subgroup operations are not usable from compute shaders");
}
} else {
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
}
if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) {
builder.Pass("VK_KHR_draw_indirect_count",
"supported (count-buffer indirect draws run as single native "
"vkCmdDraw*IndirectCount commands)");
} else {
builder.Warn("VK_KHR_draw_indirect_count",
"not supported; count-buffer indirect draws (glMultiDraw*IndirectCount) fall "
"back to a CPU readback of the parameter buffer and one draw per command");
}
const Bool indexTypeUint8 = HasVkExtension(deviceExtensions, VK_KHR_INDEX_TYPE_UINT8_EXTENSION_NAME) ||
HasVkExtension(deviceExtensions, VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME);
if (indexTypeUint8) {
builder.Pass("Index type uint8", "supported (native GL_UNSIGNED_BYTE index buffers)");
} else {
builder.Warn("Index type uint8",
"not supported; GL_UNSIGNED_BYTE index buffers cannot be drawn (the backend "
"has no conversion fallback and asserts on uint8 index draws)");
}
builder.DriverReported("Backend driver reported device", String(properties.deviceName));
builder.DriverReported("Backend driver reported driver version", driverVersionString + " (vendor-encoded)");
// Single "Timer queries" row: timestampValidBits, timestampPeriod, and the
// functional timestamp probe fold into one combined verdict whose detail
// always states the validBits and period values; the
// MOBILEGL_DISABLE_TIMERQUERY note is appended to the same row.
const Float timestampPeriod = properties.limits.timestampPeriod;
// Same support rule as VulkanRenderer::CreateLogicalDeviceAndQueues
// (m_timerQuerySupported): usable timer queries need valid timestamp bits on
// the graphics queue family and a non-zero tick period.
summary.timerQueriesSupported = graphicsQueueTimestampValidBits > 0 && timestampPeriod > 0.0f;
if (graphicsQueueTimestampValidBits > 0) {
ProbeVulkanTimerQuery(builder, getInstanceProcAddr, instance, physicalDevice,
graphicsQueueFamilyIndex, graphicsQueueTimestampValidBits, timestampPeriod);
} else {
builder.Warn("Timer queries",
format("timestampValidBits = 0 on the graphics queue family; timestampPeriod = {} ns "
"per tick; timestamps unsupported on the graphics queue; timer queries "
"unavailable",
timestampPeriod) +
TimerQueryDisabledNote());
}
if (vkGetPhysicalDeviceFormatPropertiesFn != nullptr) {
MG_External::VulkanCapabilities formatProbeCapabilities{};
BackendLoader::FillInVulkanCapabilities(formatProbeCapabilities, properties);
builder.report.formatCapabilities.emplace();
MG_Backend::DirectVulkan::PopulateFormatCapabilities(
physicalDevice, vkGetPhysicalDeviceFormatPropertiesFn, formatProbeCapabilities,
builder.report.formatCapabilities.value());
}
}
BackendPostReport RunVulkanDriverPost() {
MGLOG_I("Driver POST: probing the device Vulkan driver");
ReportBuilder builder;
VulkanProbeSummary summary;
ProbeVulkanDriver(builder, summary);
// "MobileGL reported ..." rows: what applications running on the DirectVulkan
// backend (Magma) would see. The backend API version string reuses the exact
// GetBackendAPIVersionString format, fed with the strings this probe collected
// (so the driver version appears in the probe's raw vendor-encoded hex form);
// the extension list is built by the same helper the real backend uses.
Optional<String> backendApiVersionString;
Optional<String> advertisedExtensions;
if (summary.devicePropsValid) {
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
builder.Finalize();
MGLOG_I("Driver POST: Vulkan verdict = %s", builder.report.verdict.c_str());
return builder.report;
}
} // namespace MobileGL::MG_Util::SelfTest