[Feat] (MG_Util/SelfTest): grouped POST rows, merged probes, MobileGL-reported strings

Rows in each backend section now sort FAIL -> WARN -> PASS -> INFO
(stable within groups), with identity strings always last: the device
strings renamed to 'Backend driver reported GL_*' and a new bottom
group 'MobileGL reported GL_VENDOR/GL_VERSION/GL_RENDERER/GL_EXTENSIONS'
showing exactly what MobileGL advertises to applications on that
backend, assembled from the same sources as GL_Getter and the backend
objects (extension-list construction extracted into shared helpers so
POST cannot drift from the real advertisement).

Rows probing the same subject are merged into single verdicts whose
details keep every sub-fact and causal chain: the six EGL setup steps
become one 'ES3 context' row, extension presence + functional probe
become one 'Timer queries' row per backend (including the
MOBILEGL_DISABLE_TIMERQUERY override explanation), and the Vulkan
loader/instance, surface-extension pair, and physical-device/queue/API
chains each collapse into one row.

Capability rows previously dumped as INFO now carry verdicts: index
type uint8 (WARN when absent - uint8 index buffers have no conversion
fallback), VK_KHR_draw_indirect_count (WARN when absent - count draws
degrade to CPU readback loops); buffer_storage/base_instance stay
honest INFO when absent since no MobileGL path degrades.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 03:28:50 +00:00
co-authored by Claude Fable 5
parent f0dd5d667b
commit 7b00255b11
6 changed files with 559 additions and 232 deletions
@@ -626,18 +626,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
.Extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, // OpenGL Extensions
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
E_GL_ARB_shader_image_size},
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries yet); reconciled once
// the ES capabilities exist, see UpdateAdvertisedTimerQueryExtension.
.Extensions = BuildAdvertisedExtensions(false),
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -655,15 +648,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// m_rendererInfo + UpdateAdvertisedExtensions). InitCapabilities
// completes inside the first MakeEGLCurrent on a context, so an app
// thread can only observe the extension string after the
// advertisement for its context has settled; erase-then-append keeps
// the re-run after a context recreation idempotent.
// advertisement for its context has settled; rebuilding the whole
// list keeps the re-run after a context recreation idempotent.
void UpdateAdvertisedTimerQueryExtension() {
auto& extensions = MutableRendererInfo().RendererGLInfo.Extensions;
extensions.erase(std::remove(extensions.begin(), extensions.end(), E_GL_ARB_timer_query),
extensions.end());
if (AreTimerQueriesSupported() && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(AreTimerQueriesSupported());
}
} // namespace
@@ -838,11 +826,39 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!m_initialized) {
return "<uninitialized DirectGLES backend>";
}
return FormatBackendAPIVersionString(m_GLESCapabilities.GLESRendererString,
m_GLESCapabilities.GLESVersion.Major,
m_GLESCapabilities.GLESVersion.Minor);
}
const RendererInfo& GetRendererIdentity() {
return MutableRendererInfo();
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
E_GL_ARB_shader_image_size};
// Only advertised when the device driver actually has usable timer queries
// (GL_EXT_disjoint_timer_query plus its entry points) and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
return extensions;
}
String FormatBackendAPIVersionString(const String& glesRendererString, Int glesMajor, Int glesMinor) {
// Format:
// <OpenGL ES Renderer>, OpenGL ES <OpenGL ES Version>
String versionString = std::format("{}, OpenGL ES {}.{}", m_GLESCapabilities.GLESRendererString,
m_GLESCapabilities.GLESVersion.Major, m_GLESCapabilities.GLESVersion.Minor);
return versionString;
return std::format("{}, OpenGL ES {}.{}", glesRendererString, glesMajor, glesMinor);
}
BackendType BackendObject_DirectGLES::GetBackendType() const {
@@ -47,4 +47,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_External::GLESCapabilities m_GLESCapabilities;
DynamicBackendParameters m_dynamicParameters;
};
// Single-source-of-truth helpers shared with the driver POST
// (MG_Util/SelfTest/DriverPost.cpp), so the identity strings and extension list
// MobileGL reports to applications on this backend cannot drift from what the
// POST screen shows.
// Static identity of the Espryt renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor). The Extensions vector inside is live backend state that
// is reconciled after capability init; callers that need the advertised list for
// a known capability set must use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries are (or are not) usable. The
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
// inside the application-visible GL_RENDERER string).
String FormatBackendAPIVersionString(const String& glesRendererString, Int glesMajor, Int glesMinor);
} // namespace MobileGL::MG_Backend::DirectGLES
@@ -340,28 +340,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
BackendObject_DirectVulkan::BackendObject_DirectVulkan():
m_rendererInfo{
.RendererName = "Magma",
.BackendName = "Direct (Vulkan)",
.ExtraVendor = Nullopt,
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0},
.TargetGLSLVersion = {4, 6, 0},
.Extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size},
.IsCompatibilityProfile = false
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}} {}
BackendObject_DirectVulkan::BackendObject_DirectVulkan(): m_rendererInfo{GetRendererIdentity()} {}
Bool BackendObject_DirectVulkan::InitWindowSurface() {
if (!m_windowHandle.Handle) {
@@ -512,11 +491,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!m_initialized) {
return "<uninitialized DirectVulkan backend>";
}
return FormatBackendAPIVersionString(m_vulkanCaps.DeviceName, m_vulkanCaps.VulkanAPIVersion.toString(),
m_vulkanCaps.DriverVersionString);
}
const RendererInfo& GetRendererIdentity() {
static const RendererInfo rendererInfo = {
.RendererName = "Magma",
.BackendName = "Direct (Vulkan)",
.ExtraVendor = Nullopt,
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0},
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false),
.IsCompatibilityProfile = false
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
// only advertised when the device actually supports timestamp queries and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
return extensions;
}
String FormatBackendAPIVersionString(const String& deviceName, const String& vulkanApiVersionString,
const String& driverVersionString) {
// Format:
// <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version>
String str = m_vulkanCaps.DeviceName + ", Vulkan " + m_vulkanCaps.VulkanAPIVersion.toString() + ", Driver " +
m_vulkanCaps.DriverVersionString;
return str;
return deviceName + ", Vulkan " + vulkanApiVersionString + ", Driver " + driverVersionString;
}
BackendType BackendObject_DirectVulkan::GetBackendType() const {
@@ -615,25 +639,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void BackendObject_DirectVulkan::UpdateAdvertisedExtensions() {
auto& extensions = m_rendererInfo.RendererGLInfo.Extensions;
extensions.erase(std::remove(extensions.begin(), extensions.end(), E_GL_KHR_shader_subgroup),
extensions.end());
if (m_vulkanCaps.SupportsShaderSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
// string). InitCapabilities runs after InitWindowSurface has created
// and initialized the renderer, so the advertisement can be gated on
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
// run without a renderer; nothing is advertised then.
extensions.erase(std::remove(extensions.begin(), extensions.end(), E_GL_ARB_timer_query),
extensions.end());
if (pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported() &&
!MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// run without a renderer; no timer query is advertised then. Rebuilding
// the whole list keeps re-runs idempotent.
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported());
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -47,4 +47,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_External::VulkanCapabilities m_vulkanCaps;
RendererInfo m_rendererInfo;
};
// Single-source-of-truth helpers shared with the driver POST
// (MG_Util/SelfTest/DriverPost.cpp), so the identity strings and extension list
// MobileGL reports to applications on this backend cannot drift from what the
// POST screen shows.
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor) with the baseline extension advertisement (no shader
// subgroup, no timer queries). A live backend copies this in its constructor and
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
// exist; callers that need the advertised list for a known capability set must
// use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
// ends up inside the application-visible GL_RENDERER string).
String FormatBackendAPIVersionString(const String& deviceName, const String& vulkanApiVersionString,
const String& driverVersionString);
} // namespace MobileGL::MG_Backend::DirectVulkan
+416 -169
View File
@@ -9,6 +9,10 @@
#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>
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
#include <chrono>
#include <thread>
@@ -18,28 +22,120 @@
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)}); }
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)});
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)});
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)}); }
void Info(String name, String detail) {
report.checks.push_back({Move(name), "INFO", Move(detail), RankInfo});
}
void Finalize() { report.verdict = fatalFailed ? "UNSUPPORTED" : (warnUnmet ? "DEGRADED" : "OK"); }
// 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>
@@ -60,6 +156,17 @@ namespace MobileGL::MG_Util::SelfTest {
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"
: "";
}
void EvaluateGlesChecklist(ReportBuilder& builder, const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& glesFuncs) {
const Int major = caps.GLESVersion.Major;
@@ -106,10 +213,20 @@ namespace MobileGL::MG_Util::SelfTest {
}
}
builder.Info("GL_EXT_buffer_storage",
caps.SupportsPersistentMapping ? "supported (persistent buffer mapping)" : "not supported");
builder.Info("GL_EXT_base_instance",
caps.SupportsBaseInstance ? "supported (native baseInstance draws)" : "not supported");
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 {
@@ -123,33 +240,37 @@ namespace MobileGL::MG_Util::SelfTest {
"zero-based)"
: "conforming (zero-based)");
builder.Info("GL_VENDOR", caps.GLESVendorString);
builder.Info("GL_RENDERER", caps.GLESRendererString);
builder.Info("GL_VERSION", caps.GLESVersionString);
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);
}
// Timer-query rows: extension presence plus a real GL_TIME_ELAPSED_EXT span
// around a trivial workload on the probe context. Requires the probe context
// to still be current.
// 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) {
if (MG_Config::Features.DisableTimerQuery) {
builder.Info("Timer queries", "timer queries disabled by MOBILEGL_DISABLE_TIMERQUERY");
}
const String disabledNote = TimerQueryDisabledNote();
if (!caps.SupportsDisjointTimerQuery) {
builder.Warn("GL_EXT_disjoint_timer_query",
"not supported; timer queries unavailable; Minecraft F3 GPU% will not show");
builder.Warn("Timer queries",
"GL_EXT_disjoint_timer_query not supported; timer queries unavailable; "
"Minecraft F3 GPU% will not show" +
disabledNote);
return;
}
builder.Pass("GL_EXT_disjoint_timer_query", "extension present");
// 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) {
builder.Fail("Timer query probe",
"GL_EXT_disjoint_timer_query is advertised but the query entry points did not "
"resolve through eglGetProcAddress");
fail("the query entry points did not resolve through eglGetProcAddress");
return;
}
@@ -160,7 +281,7 @@ namespace MobileGL::MG_Util::SelfTest {
GLuint queryId = 0;
glesFuncs.glGenQueries(1, &queryId);
if (queryId == 0) {
builder.Fail("Timer query probe", "glGenQueries did not return a query object");
fail("glGenQueries did not return a query object");
return;
}
const ScopeGuard deleteQuery([&]() { glesFuncs.glDeleteQueries(1, &queryId); });
@@ -175,8 +296,7 @@ namespace MobileGL::MG_Util::SelfTest {
const GLenum spanError = glesFuncs.glGetError();
if (spanError != GL_NO_ERROR) {
builder.Fail("Timer query probe",
format("GL error 0x{:x} while recording the GL_TIME_ELAPSED_EXT span", spanError));
fail(format("GL error 0x{:x} while recording the GL_TIME_ELAPSED_EXT span", spanError));
return;
}
@@ -193,9 +313,8 @@ namespace MobileGL::MG_Util::SelfTest {
}
}
if (available == 0) {
builder.Fail("Timer query probe",
"GL_QUERY_RESULT_AVAILABLE never became true after glFinish "
"(1000 polls over ~100ms)");
fail("GL_QUERY_RESULT_AVAILABLE never became true after glFinish "
"(1000 polls over ~100ms)");
return;
}
@@ -203,17 +322,41 @@ namespace MobileGL::MG_Util::SelfTest {
glesFuncs.glGetQueryObjectui64vEXT(queryId, GL_QUERY_RESULT, &elapsedNs);
const GLenum resultError = glesFuncs.glGetError();
if (resultError != GL_NO_ERROR) {
builder.Fail("Timer query probe",
format("GL error 0x{:x} while reading GL_QUERY_RESULT", resultError));
fail(format("GL error 0x{:x} while reading GL_QUERY_RESULT", resultError));
return;
}
builder.Pass("Timer query probe", format("timer query functional (observed {} ns)", elapsedNs));
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
BackendPostReport RunGlesDriverPost() {
MGLOG_I("Driver POST: probing the device GLES driver");
ReportBuilder builder;
// 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);
@@ -222,27 +365,23 @@ namespace MobileGL::MG_Util::SelfTest {
eglFuncs.eglCreateContext && eglFuncs.eglMakeCurrent && eglFuncs.eglDestroySurface &&
eglFuncs.eglDestroyContext && eglFuncs.eglTerminate && eglFuncs.eglGetProcAddress;
if (!eglLoaded) {
builder.Fail("EGL library", "libEGL.so or one of its required entry points is missing");
builder.Finalize();
return builder.report;
failStage("EGL library", "libEGL.so or one of its required entry points is missing");
return;
}
builder.Pass("EGL library", "libEGL.so loaded with all required entry points");
stageDone("libEGL.so loaded with all required entry points");
EGLDisplay display = eglFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
builder.Fail("EGL display", "eglGetDisplay returned EGL_NO_DISPLAY");
builder.Finalize();
return builder.report;
failStage("EGL display", "eglGetDisplay returned EGL_NO_DISPLAY");
return;
}
EGLint eglMajor = 0;
EGLint eglMinor = 0;
if (!eglFuncs.eglInitialize(display, &eglMajor, &eglMinor)) {
builder.Fail("EGL display",
"eglInitialize failed on the default display" + EGLErrorSuffix(eglFuncs));
builder.Finalize();
return builder.report;
failStage("EGL display", "eglInitialize failed on the default display" + EGLErrorSuffix(eglFuncs));
return;
}
builder.Pass("EGL display", format("EGL {}.{} initialized on the default display", eglMajor, eglMinor));
stageDone(format("EGL {}.{} initialized on the default display", eglMajor, eglMinor));
builder.report.available = true;
EGLSurface surface = EGL_NO_SURFACE;
@@ -262,10 +401,10 @@ namespace MobileGL::MG_Util::SelfTest {
});
do {
if (!eglFuncs.eglBindAPI(EGL_OPENGL_ES_API)) {
builder.Fail("OpenGL ES API bind", "eglBindAPI(EGL_OPENGL_ES_API) failed" + EGLErrorSuffix(eglFuncs));
failStage("OpenGL ES API bind", "eglBindAPI(EGL_OPENGL_ES_API) failed" + EGLErrorSuffix(eglFuncs));
break;
}
builder.Pass("OpenGL ES API bind", "eglBindAPI(EGL_OPENGL_ES_API) succeeded");
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,
@@ -274,49 +413,72 @@ namespace MobileGL::MG_Util::SelfTest {
EGLConfig config = nullptr;
EGLint numConfigs = 0;
if (!eglFuncs.eglChooseConfig(display, configAttribs, &config, 1, &numConfigs)) {
builder.Fail("ES3 RGBA8888 pbuffer config", "eglChooseConfig failed" + EGLErrorSuffix(eglFuncs));
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.
builder.Fail("ES3 RGBA8888 pbuffer config", "no ES3-capable RGBA8888 pbuffer config");
failStage("ES3 RGBA8888 pbuffer config", "no ES3-capable RGBA8888 pbuffer config");
break;
}
builder.Pass("ES3 RGBA8888 pbuffer config", "ES3-renderable RGBA8888 pbuffer config found");
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) {
builder.Fail("1x1 pbuffer surface",
"eglCreatePbufferSurface failed" + EGLErrorSuffix(eglFuncs));
failStage("1x1 pbuffer surface", "eglCreatePbufferSurface failed" + EGLErrorSuffix(eglFuncs));
break;
}
builder.Pass("1x1 pbuffer surface", "probe surface created");
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) {
builder.Fail("OpenGL ES 3 context", "eglCreateContext failed" + EGLErrorSuffix(eglFuncs));
failStage("OpenGL ES 3 context", "eglCreateContext failed" + EGLErrorSuffix(eglFuncs));
break;
}
if (!eglFuncs.eglMakeCurrent(display, surface, surface, context)) {
builder.Fail("OpenGL ES 3 context", "eglMakeCurrent failed" + EGLErrorSuffix(eglFuncs));
failStage("OpenGL ES 3 context", "eglMakeCurrent failed" + EGLErrorSuffix(eglFuncs));
break;
}
builder.Pass("OpenGL ES 3 context", "context created and made current");
stageDone("ES 3 context created and made current");
builder.Pass("ES3 context", chain);
MG_External::GLESFunctionsTable glesFuncs{};
BackendLoader::AcquireGLESFunctions(glesFuncs, eglFuncs.eglGetProcAddress);
MG_External::GLESCapabilities caps{};
if (!BackendLoader::FillInGLESCapabilities(caps, glesFuncs)) {
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);
} 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));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
builder.Finalize();
MGLOG_I("Driver POST: GLES verdict = %s", builder.report.verdict.c_str());
@@ -373,14 +535,23 @@ namespace MobileGL::MG_Util::SelfTest {
VK_VERSION_PATCH(version));
}
// Real timestamp-query probe: a throwaway logical device records two
// vkCmdWriteTimestamp(BOTTOM_OF_PIPE) queries and reads them back. Every
// created object is torn down from a scope guard before the caller's
// instance guard runs.
// 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 =
@@ -426,9 +597,8 @@ namespace MobileGL::MG_Util::SelfTest {
vkCmdWriteTimestampFn == nullptr || vkCreateFenceFn == nullptr || vkDestroyFenceFn == nullptr ||
vkWaitForFencesFn == nullptr || vkQueueSubmitFn == nullptr || vkGetQueryPoolResultsFn == nullptr ||
vkDeviceWaitIdleFn == nullptr) {
builder.Fail("Timer query probe",
"vkGetInstanceProcAddr could not resolve the entry points required for the "
"timestamp probe");
fail("vkGetInstanceProcAddr could not resolve the entry points required for the "
"timestamp probe");
return;
}
@@ -447,8 +617,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkDevice device = VK_NULL_HANDLE;
VkResult result = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device);
if (result != VK_SUCCESS || device == VK_NULL_HANDLE) {
builder.Fail("Timer query probe",
format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(result)));
return;
}
@@ -485,7 +654,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkQueue queue = VK_NULL_HANDLE;
vkGetDeviceQueueFn(device, graphicsQueueFamilyIndex, 0, &queue);
if (queue == VK_NULL_HANDLE) {
builder.Fail("Timer query probe", "vkGetDeviceQueue returned a null graphics queue");
fail("vkGetDeviceQueue returned a null graphics queue");
return;
}
@@ -494,8 +663,7 @@ namespace MobileGL::MG_Util::SelfTest {
poolInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
result = vkCreateCommandPoolFn(device, &poolInfo, nullptr, &commandPool);
if (result != VK_SUCCESS) {
builder.Fail("Timer query probe",
format("vkCreateCommandPool failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkCreateCommandPool failed (VkResult = {})", static_cast<Int>(result)));
return;
}
@@ -507,8 +675,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
result = vkAllocateCommandBuffersFn(device, &allocInfo, &commandBuffer);
if (result != VK_SUCCESS) {
builder.Fail("Timer query probe",
format("vkAllocateCommandBuffers failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkAllocateCommandBuffers failed (VkResult = {})", static_cast<Int>(result)));
return;
}
@@ -518,8 +685,7 @@ namespace MobileGL::MG_Util::SelfTest {
queryPoolInfo.queryCount = 2;
result = vkCreateQueryPoolFn(device, &queryPoolInfo, nullptr, &queryPool);
if (result != VK_SUCCESS) {
builder.Fail("Timer query probe",
format("vkCreateQueryPool failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkCreateQueryPool failed (VkResult = {})", static_cast<Int>(result)));
return;
}
@@ -528,8 +694,7 @@ namespace MobileGL::MG_Util::SelfTest {
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
result = vkBeginCommandBufferFn(commandBuffer, &beginInfo);
if (result != VK_SUCCESS) {
builder.Fail("Timer query probe",
format("vkBeginCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkBeginCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
return;
}
vkCmdResetQueryPoolFn(commandBuffer, queryPool, 0, 2);
@@ -537,8 +702,7 @@ namespace MobileGL::MG_Util::SelfTest {
vkCmdWriteTimestampFn(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool, 1);
result = vkEndCommandBufferFn(commandBuffer);
if (result != VK_SUCCESS) {
builder.Fail("Timer query probe",
format("vkEndCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkEndCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
return;
}
@@ -546,8 +710,7 @@ namespace MobileGL::MG_Util::SelfTest {
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
result = vkCreateFenceFn(device, &fenceInfo, nullptr, &fence);
if (result != VK_SUCCESS) {
builder.Fail("Timer query probe",
format("vkCreateFence failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkCreateFence failed (VkResult = {})", static_cast<Int>(result)));
return;
}
@@ -557,8 +720,7 @@ namespace MobileGL::MG_Util::SelfTest {
submitInfo.pCommandBuffers = &commandBuffer;
result = vkQueueSubmitFn(queue, 1, &submitInfo, fence);
if (result != VK_SUCCESS) {
builder.Fail("Timer query probe",
format("vkQueueSubmit failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkQueueSubmit failed (VkResult = {})", static_cast<Int>(result)));
return;
}
@@ -568,9 +730,8 @@ namespace MobileGL::MG_Util::SelfTest {
// Skip the teardown idle wait too (see the scope guard): the
// submission is still pending on a possibly-hung GPU.
fenceWaitTimedOut = true;
builder.Fail("Timer query probe",
format("vkWaitForFences did not signal within 5 s (VkResult = {})",
static_cast<Int>(result)));
fail(format("vkWaitForFences did not signal within 5 s (VkResult = {})",
static_cast<Int>(result)));
return;
}
@@ -578,8 +739,7 @@ namespace MobileGL::MG_Util::SelfTest {
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) {
builder.Fail("Timer query probe",
format("vkGetQueryPoolResults failed (VkResult = {})", static_cast<Int>(result)));
fail(format("vkGetQueryPoolResults failed (VkResult = {})", static_cast<Int>(result)));
return;
}
@@ -588,36 +748,63 @@ namespace MobileGL::MG_Util::SelfTest {
const Uint64 t0 = timestamps[0] & validMask;
const Uint64 t1 = timestamps[1] & validMask;
if (t1 < t0) {
builder.Fail("Timer query probe",
format("timestamps are not monotonic (t0 = {}, t1 = {})", t0, t1));
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 query probe",
format("timer query functional (t1 >= t0, elapsed {} ns using timestampPeriod {})",
elapsedNs, 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;
};
} // namespace
BackendPostReport RunVulkanDriverPost() {
MGLOG_I("Driver POST: probing the device Vulkan driver");
ReportBuilder builder;
// 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) {
builder.Fail("Vulkan loader", "libvulkan.so could not be loaded; no Vulkan loader on this device");
builder.Finalize();
return builder.report;
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) {
builder.Fail("Vulkan loader", "vkGetInstanceProcAddr is missing from the Vulkan loader library");
builder.Finalize();
return builder.report;
failInstanceStage("Vulkan loader", "vkGetInstanceProcAddr is missing from the Vulkan loader library");
return;
}
builder.Pass("Vulkan loader", "Vulkan loader library loaded and vkGetInstanceProcAddr resolved");
instanceStageDone("Vulkan loader library loaded and vkGetInstanceProcAddr resolved");
const auto vkCreateInstanceFn =
reinterpret_cast<PFN_vkCreateInstance>(getInstanceProcAddr(nullptr, "vkCreateInstance"));
@@ -633,13 +820,13 @@ namespace MobileGL::MG_Util::SelfTest {
}
if (vkCreateInstanceFn == nullptr || vkEnumerateInstanceVersionFn == nullptr ||
instanceApiVersion < VK_API_VERSION_1_1) {
builder.Fail("Instance API version",
format("instance API {} (< 1.1); the DirectVulkan backend requires a Vulkan 1.1 instance",
VkApiVersionToString(instanceApiVersion)));
builder.Finalize();
return builder.report;
failInstanceStage("Instance API version",
format("instance API {} (< 1.1); the DirectVulkan backend requires a Vulkan 1.1 "
"instance",
VkApiVersionToString(instanceApiVersion)));
return;
}
builder.Pass("Instance API version", format("instance API {}", VkApiVersionToString(instanceApiVersion)));
instanceStageDone(format("instance API {}", VkApiVersionToString(instanceApiVersion)));
Vector<VkExtensionProperties> instanceExtensions;
if (vkEnumerateInstanceExtensionPropertiesFn != nullptr) {
@@ -655,19 +842,33 @@ namespace MobileGL::MG_Util::SelfTest {
}
}
}
if (HasVkExtension(instanceExtensions, VK_KHR_SURFACE_EXTENSION_NAME)) {
builder.Pass("VK_KHR_surface", "instance extension present");
} else {
builder.Fail("VK_KHR_surface", "required instance extension missing; on-screen rendering is impossible");
}
// 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)
if (HasVkExtension(instanceExtensions, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME)) {
builder.Pass("VK_KHR_android_surface", "instance extension present");
} else {
builder.Fail("VK_KHR_android_surface",
"required instance extension missing; ANativeWindow surfaces cannot be created");
}
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{};
@@ -683,12 +884,12 @@ namespace MobileGL::MG_Util::SelfTest {
VkInstance instance = VK_NULL_HANDLE;
const VkResult createResult = vkCreateInstanceFn(&instanceInfo, nullptr, &instance);
if (createResult != VK_SUCCESS || instance == VK_NULL_HANDLE) {
builder.Fail("Vulkan instance",
format("vkCreateInstance failed (VkResult = {})", static_cast<Int>(createResult)));
builder.Finalize();
return builder.report;
failInstanceStage("Vulkan instance creation",
format("vkCreateInstance failed (VkResult = {})", static_cast<Int>(createResult)));
return;
}
builder.Pass("Vulkan instance", "Vulkan 1.1 instance created");
instanceStageDone("Vulkan 1.1 instance created");
builder.Pass("Vulkan instance", instanceChain);
const auto vkDestroyInstanceFn =
reinterpret_cast<PFN_vkDestroyInstance>(getInstanceProcAddr(instance, "vkDestroyInstance"));
@@ -722,31 +923,29 @@ namespace MobileGL::MG_Util::SelfTest {
vkEnumerateDeviceExtensionPropertiesFn == nullptr) {
builder.Fail("Vulkan core entry points",
"vkGetInstanceProcAddr could not resolve required Vulkan 1.0 functions");
builder.Finalize();
return builder.report;
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("Physical device", format("vkEnumeratePhysicalDevices failed (VkResult = {})",
builder.Fail("Graphics device", format("vkEnumeratePhysicalDevices failed (VkResult = {})",
static_cast<Int>(countResult)));
builder.Finalize();
return builder.report;
return;
}
if (deviceCount == 0) {
builder.Fail("Physical device", "no Vulkan physical devices found");
builder.Finalize();
return builder.report;
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("Physical device", format("vkEnumeratePhysicalDevices failed (VkResult = {})",
builder.Fail("Graphics device", format("vkEnumeratePhysicalDevices failed (VkResult = {})",
static_cast<Int>(enumerateResult)));
builder.Finalize();
return builder.report;
return;
}
devices.resize(deviceCount);
@@ -772,31 +971,39 @@ namespace MobileGL::MG_Util::SelfTest {
}
}
if (physicalDevice == VK_NULL_HANDLE) {
builder.Fail("Graphics queue",
builder.Fail("Graphics device",
format("none of the {} physical device(s) exposes a graphics queue family", deviceCount));
builder.Finalize();
return builder.report;
return;
}
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDevicePropertiesFn(physicalDevice, &properties);
builder.Pass("Physical device",
format("{} ({} device(s) enumerated, picked the first with a graphics queue)",
String(properties.deviceName), deviceCount));
builder.Pass("Graphics queue", "graphics queue family present");
// 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("Device API version", format("Vulkan {}", VkApiVersionToString(properties.apiVersion)));
builder.Pass("Graphics device",
deviceFacts +
format("; device API Vulkan {}", VkApiVersionToString(properties.apiVersion)));
} else {
builder.Fail("Device API version",
format("Vulkan {} (< 1.1); the DirectVulkan backend requires a Vulkan 1.1 device",
VkApiVersionToString(properties.apiVersion)));
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)));
}
Vector<VkExtensionProperties> deviceExtensions;
@@ -870,6 +1077,10 @@ namespace MobileGL::MG_Util::SelfTest {
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 {}",
@@ -882,34 +1093,70 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
}
builder.Info("VK_KHR_draw_indirect_count",
HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)
? "supported (GPU-driven indirect draw counts)"
: "not supported");
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);
builder.Info("Index type uint8",
indexTypeUint8 ? "supported (native GL_UNSIGNED_BYTE index buffers)" : "not supported");
builder.Info("Device", String(properties.deviceName));
builder.Info("Driver version", driverVersionString + " (vendor-encoded)");
if (MG_Config::Features.DisableTimerQuery) {
builder.Info("Timer queries", "timer queries disabled by MOBILEGL_DISABLE_TIMERQUERY");
}
const Float timestampPeriod = properties.limits.timestampPeriod;
if (graphicsQueueTimestampValidBits > 0) {
builder.Pass("Timestamp valid bits",
format("timestampValidBits = {} on the graphics queue family",
graphicsQueueTimestampValidBits));
if (indexTypeUint8) {
builder.Pass("Index type uint8", "supported (native GL_UNSIGNED_BYTE index buffers)");
} else {
builder.Warn("Timestamp valid bits",
"timestampValidBits = 0 on the graphics queue family; timer queries unavailable");
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.Info("Timestamp period", format("timestampPeriod = {} ns per tick", timestampPeriod));
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());
}
}
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));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
builder.Finalize();
MGLOG_I("Driver POST: Vulkan verdict = %s", builder.report.verdict.c_str());
+5
View File
@@ -15,6 +15,11 @@ namespace MobileGL::MG_Util::SelfTest {
String name;
String status; // "PASS" | "WARN" | "FAIL" | "INFO"
String detail;
// Display ordering rank within a backend section (lower renders first): FAIL,
// WARN, PASS, INFO, then the device-driver identity strings, then the strings
// MobileGL itself reports to applications. Rows are stable-sorted by this rank
// before the report is returned; it is not serialized to JSON.
Int displayRank = 0;
};
// Verdict for one backend's device driver.