[Fix, Feat, Test] (MG_Util, MG_Backend, MG_Impl, MG_State): correct the log severity ordering and unwind the diagnostics it silenced

This commit is contained in:
Swung0x48
2026-08-13 02:02:49 -04:00
parent 373aa44dd7
commit dd2a62228f
55 changed files with 940 additions and 590 deletions
+3 -3
View File
@@ -32,11 +32,11 @@ namespace MobileGL::MG_Util::Async {
try {
continuation();
} catch (const std::exception& e) {
MGLOG_E("JobNode: a terminal continuation threw (%s); it has been contained, but whatever it "
MGLOG_E_ONCE("JobNode: a terminal continuation threw (%s); it has been contained, but whatever it "
"was going to do did not happen",
e.what());
} catch (...) {
MGLOG_E("JobNode: a terminal continuation threw a non-std exception; it has been contained, "
MGLOG_E_ONCE("JobNode: a terminal continuation threw a non-std exception; it has been contained, "
"but whatever it was going to do did not happen");
}
}
@@ -165,7 +165,7 @@ namespace MobileGL::MG_Util::Async {
Vector<String> lines;
lines.swap(node.diagnostics.logLines);
for (const String& line : lines) {
MGLOG_W("%s", line.c_str());
MGLOG_D("%s", line.c_str());
}
}
+1 -1
View File
@@ -281,7 +281,7 @@ namespace MobileGL::MG_Util::Async {
enqueued = true;
}
} catch (...) {
MGLOG_E("ShaderCompilePool::Post: enqueue failed; cancelling the job so its joiner "
MGLOG_E_ONCE("ShaderCompilePool::Post: enqueue failed; cancelling the job so its joiner "
"cannot block forever");
if (node) node->Cancel();
return;
@@ -594,9 +594,10 @@ namespace MobileGL::MG_Util::BackendLoader {
#endif // !_WIN32
if (!eglLib) {
// MGLOG_F, not MGLOG_E: at the INFO log level every shipping and CI build
// uses, MGLOG_E is compiled out (Log.h orders DEBUG < WARN < ERROR < INFO),
// so this diagnosis was invisible in precisely the builds that needed it.
// MGLOG_F, not MGLOG_E: with no EGL there is no rendering at all, so this is a
// bring-up abort rather than a recoverable error. It was forced to F while the
// Log.h ordering compiled MGLOG_E out of every shipping and CI build; F is still
// the right level on its own merits, so it stays.
MGLOG_F("Failed to open EGL library: none of libEGL.so.1 / libEGL.so could be "
"dlopened; every EGL entry point will be null");
return;
@@ -986,6 +987,12 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.SupportsBaseInstance ? "yes" : "no");
MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
// LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's
// is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to
// decide whether MobileGL got far enough to have a working context: if the probe ran,
// a later surface loss is a real defect rather than an emulator fault worth retrying.
// Demoting this line, renaming it, or moving it before the context is usable silently
// inverts that retry logic. It is init-phase, so MGLOG_I is correct and it stays.
MGLOG_I("OpenGL ES capabilities:");
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
MGLOG_I(" GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: %d", caps.UniformBufferOffsetAlignment);
@@ -135,7 +135,7 @@ namespace MobileGL {
case TexturePixelDataType::UnsignedShort:
return TextureInternalFormat::RGBA16;
default:
MGLOG_W("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
MGLOG_W_ONCE("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
"returning original.",
__func__, MG_Util::ConvertTextureInternalFormatToString(internalformat).c_str(),
MG_Util::ConvertTextureInputFormatToString(format).c_str(),
@@ -148,7 +148,7 @@ namespace MobileGL {
case TexturePixelDataType::UnsignedByte:
return TextureInternalFormat::RGB8;
default:
MGLOG_W("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
MGLOG_W_ONCE("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
"returning original.",
__func__, MG_Util::ConvertTextureInternalFormatToString(internalformat).c_str(),
MG_Util::ConvertTextureInputFormatToString(format).c_str(),
@@ -163,7 +163,7 @@ namespace MobileGL {
case TexturePixelDataType::UnsignedShort:
return TextureInternalFormat::RG16;
default:
MGLOG_W("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
MGLOG_W_ONCE("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
"returning original.",
__func__, MG_Util::ConvertTextureInternalFormatToString(internalformat).c_str(),
MG_Util::ConvertTextureInputFormatToString(format).c_str(),
@@ -178,7 +178,7 @@ namespace MobileGL {
case TexturePixelDataType::UnsignedShort:
return TextureInternalFormat::R16;
default:
MGLOG_W("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
MGLOG_W_ONCE("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
"returning original.",
__func__, MG_Util::ConvertTextureInternalFormatToString(internalformat).c_str(),
MG_Util::ConvertTextureInputFormatToString(format).c_str(),
@@ -195,7 +195,7 @@ namespace MobileGL {
case TexturePixelDataType::Float:
return TextureInternalFormat::DepthComponent32F;
default:
MGLOG_W("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
MGLOG_W_ONCE("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
"returning original.",
__func__, MG_Util::ConvertTextureInternalFormatToString(internalformat).c_str(),
MG_Util::ConvertTextureInputFormatToString(format).c_str(),
@@ -208,7 +208,7 @@ namespace MobileGL {
case TexturePixelDataType::UnsignedInt248:
return TextureInternalFormat::Depth24Stencil8;
default:
MGLOG_W("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
MGLOG_W_ONCE("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, "
"returning original.",
__func__, MG_Util::ConvertTextureInternalFormatToString(internalformat).c_str(),
MG_Util::ConvertTextureInputFormatToString(format).c_str(),
@@ -217,7 +217,7 @@ namespace MobileGL {
}
}
default: {
MGLOG_W("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, returning "
MGLOG_W_ONCE("%s: Can't infer sized internal format from internalformat=%s, format=%s, type=%s, returning "
"original.",
__func__, MG_Util::ConvertTextureInternalFormatToString(internalformat).c_str(),
MG_Util::ConvertTextureInputFormatToString(format).c_str(),
@@ -310,7 +310,7 @@ namespace MobileGL {
case TextureInternalFormat::DepthStencil:
return TextureInternalFormat::DepthStencil;
default:
MGLOG_W("%s: Unknown or unhandled internal format %s, returning original.", __func__,
MGLOG_W_ONCE("%s: Unknown or unhandled internal format %s, returning original.", __func__,
MG_Util::ConvertTextureInternalFormatToString(internalformat).c_str());
return internalformat;
}
@@ -28,7 +28,7 @@ namespace MobileGL {
// DrawArrays/DrawElements rewrite line loops into closed indexed
// strips; entry points without that rewrite (instanced/indirect)
// degrade to an open strip, which only misses the closing segment.
MGLOG_W("GL_LINE_LOOP without index rewrite; drawing as LINE_STRIP");
MGLOG_W_ONCE("GL_LINE_LOOP without index rewrite; drawing as LINE_STRIP");
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
case GL_LINES_ADJACENCY:
return VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY;
@@ -43,7 +43,7 @@ namespace MobileGL {
// state (patchControlPoints), not part of the topology.
return VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
default:
MGLOG_W("Unrecognized primitive topology");
MGLOG_W_ONCE("Unrecognized primitive topology");
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
}
}
@@ -57,7 +57,7 @@ namespace MobileGL {
case GL_POINT:
return VK_POLYGON_MODE_POINT;
default:
MGLOG_W("Unrecognized polygon mode");
MGLOG_W_ONCE("Unrecognized polygon mode");
return VK_POLYGON_MODE_FILL;
}
}
@@ -73,7 +73,7 @@ namespace MobileGL {
case CullFaceMode::Unknown:
case CullFaceMode::CullFaceModeCount:
default:
MGLOG_W("Unrecognized cull face mode");
MGLOG_W_ONCE("Unrecognized cull face mode");
return VK_CULL_MODE_BACK_BIT;
}
}
+7 -1
View File
@@ -115,7 +115,13 @@ namespace MobileGL {
#endif
#if MOBILEGL_LOG_ENABLE_ANDROID && defined(__ANDROID__)
__android_log_print(androidLogLevel, "MobileGL", "%s", out.c_str());
// Without the trailing newline that the file sink needs: logcat terminates
// records itself, so handing it an already-newline-terminated string made
// every MobileGL log occupy TWO logcat records, the second one empty. That
// halved the useful depth of every `adb logcat -t N` window the CI
// diagnostics read (android-plugin/trace-replay-ci.sh).
__android_log_print(androidLogLevel, "MobileGL", "%.*s", static_cast<int>(out.size() - 1),
out.c_str());
#endif
WriteToFile(out.c_str());
+70 -3
View File
@@ -9,10 +9,24 @@
#pragma once
#include <Includes.h>
#include <atomic>
// Severity order, ascending. MOBILEGL_LOG_ACTIVE_LEVEL names the LOWEST severity that is
// compiled in, so every level at or above it survives and everything below it becomes a
// no-op: the production default INFO admits I/W/E/F and drops only D.
//
// This ordering was inverted until 2026-08-13 (DEBUG < WARN < ERROR < INFO < FATAL), which
// silently compiled MGLOG_W and MGLOG_E out of every production and CI build and cost
// several real diagnostic blackouts. Do not reorder without re-reading every
// `#if MOBILEGL_LOG_ACTIVE_LEVEL <= ...` in the tree.
//
// These five constants are duplicated verbatim in Defines.h, which needs them for the
// MOBILEGL_ASSERT gate in translation units that do not include Log.h. Keep both copies
// in sync; the values are load-bearing, not cosmetic.
#define MOBILEGL_LOG_LEVEL_DEBUG 0
#define MOBILEGL_LOG_LEVEL_WARN 1
#define MOBILEGL_LOG_LEVEL_ERROR 2
#define MOBILEGL_LOG_LEVEL_INFO 3
#define MOBILEGL_LOG_LEVEL_INFO 1
#define MOBILEGL_LOG_LEVEL_WARN 2
#define MOBILEGL_LOG_LEVEL_ERROR 3
#define MOBILEGL_LOG_LEVEL_FATAL 4
#define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \
@@ -20,6 +34,28 @@
MobileGL::MG_Util::Debug::Log(levelTag, androidLogLevel, fmt, ##__VA_ARGS__); \
} while (0)
// Emit `inner` at most once per call site, for the life of the process.
//
// Production logging is not allowed to repeat: a diagnostic on a per-draw or per-frame
// path costs frame time on every occurrence and buries the rest of the log. Anything at
// W or E that sits on such a path must either be latched with one of the _ONCE forms
// below or be demoted to MGLOG_D, which production compiles out entirely.
//
// The latch is a function-local atomic - zero-initialised before any dynamic
// initialisation runs, so it is safe from any thread at any time, needs no guard
// variable, and costs one relaxed test-and-set on the already-cold failure path. Note
// that the latch is per CALL SITE, not per subject: a site that reports "texture %u is
// unsupported" reports only the first such texture. That is the intended trade - the
// first occurrence is what a user shares for troubleshooting, and MGLOG_D still shows
// every occurrence in a dev build.
#define MOBILEGL_LOG_ONCE_INTERNAL(inner, fmt, ...) \
do { \
static ::std::atomic_flag mobileglLogOnceLatch; \
if (!mobileglLogOnceLatch.test_and_set(::std::memory_order_relaxed)) { \
inner(fmt, ##__VA_ARGS__); \
} \
} while (0)
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MGLOG_D(fmt, ...) MOBILEGL_LOG_INTERNAL("DEBUG", ANDROID_LOG_DEBUG, fmt, ##__VA_ARGS__)
#else
@@ -55,6 +91,37 @@
{}
#endif
// One-shot forms. Each is gated on its own level so that a suppressed level leaves no
// latch behind - MGLOG_D_ONCE in a production build is nothing at all, not a byte of
// state plus a test-and-set.
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MGLOG_D_ONCE(fmt, ...) MOBILEGL_LOG_ONCE_INTERNAL(MGLOG_D, fmt, ##__VA_ARGS__)
#else
#define MGLOG_D_ONCE(fmt, ...) \
{}
#endif
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_INFO
#define MGLOG_I_ONCE(fmt, ...) MOBILEGL_LOG_ONCE_INTERNAL(MGLOG_I, fmt, ##__VA_ARGS__)
#else
#define MGLOG_I_ONCE(fmt, ...) \
{}
#endif
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_WARN
#define MGLOG_W_ONCE(fmt, ...) MOBILEGL_LOG_ONCE_INTERNAL(MGLOG_W, fmt, ##__VA_ARGS__)
#else
#define MGLOG_W_ONCE(fmt, ...) \
{}
#endif
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_ERROR
#define MGLOG_E_ONCE(fmt, ...) MOBILEGL_LOG_ONCE_INTERNAL(MGLOG_E, fmt, ##__VA_ARGS__)
#else
#define MGLOG_E_ONCE(fmt, ...) \
{}
#endif
namespace MobileGL {
namespace MG_Util {
namespace Debug {
+1 -1
View File
@@ -517,7 +517,7 @@ namespace MobileGL {
// parameter queries on the initial state); every size stays 0.
break;
default:
MGLOG_W("Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
MGLOG_W_ONCE("Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
static_cast<Int>(internal));
break;
}
@@ -463,11 +463,10 @@ namespace MobileGL {
case SPV_MSG_FATAL:
case SPV_MSG_INTERNAL_ERROR:
case SPV_MSG_ERROR:
// MGLOG_I, deliberately: at the INFO compile level of every
// CI/WSL/retrace build, MGLOG_E and MGLOG_W are compiled out
// (Log.h orders DEBUG < WARN < ERROR < INFO) and the VUID
// would never reach a log.
MGLOG_I("[spirv] %s: %s (word index %zu)", site, text, position.index);
// Unlatched: only reachable with the validation switch armed,
// and every VUID names a different defect. (Parked at MGLOG_I
// until the Log.h ordering fix made E live at INFO.)
MGLOG_E("[spirv] %s: %s (word index %zu)", site, text, position.index);
break;
default:
MGLOG_D("[spirv] %s: %s", site, text);
@@ -486,7 +485,7 @@ namespace MobileGL {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(MakeSpirvMessageConsumer(site));
if (!tools.Validate(binary)) {
MGLOG_I("[spirv] %s: produced a module that fails validation (failure #%llu)",
MGLOG_E("[spirv] %s: produced a module that fails validation (failure #%llu)",
site,
static_cast<unsigned long long>(
ShaderCompiler::NoteSpirvValidationFailure()));
@@ -837,10 +836,10 @@ namespace MobileGL {
}
if (LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(outputBinary)) {
// MGLOG_I, deliberately: MGLOG_E/W are compiled out at the INFO level every
// CI and retrace build uses, and this is precisely the diagnostic that has
// to survive to explain a shader the driver is about to reject.
MGLOG_I("[spirv] LegalizeFragmentOutputIndexingForEssl: a fragment output is still "
// MGLOG_W, latched: this runs per shader compile, and shader packs compile
// lazily mid-session, so an unlatched line here is unbounded runtime noise.
// (Parked at MGLOG_I until the Log.h ordering fix made W live at INFO.)
MGLOG_W_ONCE("[spirv] LegalizeFragmentOutputIndexingForEssl: a fragment output is still "
"indexed dynamically; a strict ES driver will reject this shader");
}
return true;
@@ -866,9 +865,8 @@ namespace MobileGL {
// access path there is no correct answer to substitute, because the ES texture
// genuinely has a height the GL one does not.
//
// MGLOG_I, deliberately: MGLOG_E/W are compiled out at the INFO level every CI,
// retrace and release build uses, and this is exactly the diagnostic that has to
// survive to explain the shader the driver is about to reject.
// MGLOG_W, latched: per shader compile, and shader packs compile lazily
// mid-session. (Parked at MGLOG_I until the Log.h ordering fix made W live.)
const auto traits = Lower1DArrayImagesPass::InspectBinary(inputBinary);
// The overwhelmingly common answer, and the reason the inspection exists: no
// 1D-array storage image, so the module is handed back byte for byte without an
@@ -879,7 +877,7 @@ namespace MobileGL {
return true;
}
if (traits.queriesImageSize) {
MGLOG_I("[spirv] Lower1DArrayImagesForEssl: the module queries the size of a 1D-array "
MGLOG_W_ONCE("[spirv] Lower1DArrayImagesForEssl: the module queries the size of a 1D-array "
"storage image, which cannot be answered in the 2D-array shape ES stores it in; "
"leaving the module alone, and a strict ES driver will reject it");
outputBinary = inputBinary;
@@ -1275,7 +1275,7 @@ namespace MobileGL {
// matches (e.g. the pack shipped a new shader revision), the affected device
// silently falls back to the driver's miscompiled path. Make that visible.
if (CountToken(tokens, "subgroupInclusiveAdd") > 0) {
MGLOG_W("%s: subgroupInclusiveAdd present but the linear prefix-scan template "
MGLOG_W_ONCE("%s: subgroupInclusiveAdd present but the linear prefix-scan template "
"did not match; the wide-subgroup rewrite was NOT applied",
__func__);
}
@@ -1357,7 +1357,7 @@ namespace MobileGL {
continue;
}
if (quirk.Apply(quirkContext, source)) {
MGLOG_I("ApplyShaderSourceQuirks: applied '%s'%s", quirk.name,
MGLOG_D("ApplyShaderSourceQuirks: applied '%s'%s", quirk.name,
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
}
}
@@ -203,7 +203,7 @@ namespace MobileGL {
static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0));
if (executionModel == spv::ExecutionModel::Geometry ||
executionModel == spv::ExecutionModel::TessellationControl) {
MGLOG_I("FlattenXfbInterfaceBlocksPass: execution model %u publishes outputs outside "
MGLOG_D("FlattenXfbInterfaceBlocksPass: execution model %u publishes outputs outside "
"the entry point's return; leaving its blocks declared as blocks",
static_cast<Uint32>(executionModel));
return Status::SuccessWithoutChange;
@@ -302,7 +302,7 @@ namespace MobileGL {
target.members.push_back(member);
}
if (!usable || target.members.empty()) {
MGLOG_I("FlattenXfbInterfaceBlocksPass: block '%s' has a member this pass cannot "
MGLOG_D("FlattenXfbInterfaceBlocksPass: block '%s' has a member this pass cannot "
"place; leaving it declared as a block",
blockName.c_str());
continue;
@@ -370,7 +370,7 @@ namespace MobileGL {
continue;
}
if (derivedPointers.count(operand.words[0]) == 0) continue;
MGLOG_I("FlattenXfbInterfaceBlocksPass: interface block %%%u reaches a "
MGLOG_D("FlattenXfbInterfaceBlocksPass: interface block %%%u reaches a "
"SPIR-V opcode %u that this pass cannot follow; leaving it "
"declared as a block",
operand.words[0], static_cast<Uint32>(opcode));
@@ -82,7 +82,7 @@ namespace MobileGL {
// 6 or 8 uint32 components has no single vertex format, and GL spreads such
// an input over two attribute locations. Left alone; the vertex-input
// factory declines the matching attribute for the same reason.
MGLOG_E("PackDoubleVertexInputsPass: vertex input %%%u is a %u-component 64-bit "
MGLOG_E_ONCE("PackDoubleVertexInputsPass: vertex input %%%u is a %u-component 64-bit "
"float; only double and dvec2 inputs can be packed",
inst.result_id(), components);
continue;
@@ -128,7 +128,7 @@ namespace MobileGL {
const Uint32 elementTypeId = typeMgr->GetId(arrayType->element_type());
const Uint32 locationsPerElement = LocationsPerElement(arrayType->element_type());
if (elementTypeId == 0 || locationsPerElement == 0) {
MGLOG_I("SplitArrayVertexInputsPass: vertex input %%%u is an array whose element "
MGLOG_D("SplitArrayVertexInputsPass: vertex input %%%u is an array whose element "
"type has no single-location mapping; leaving it declared as an array",
inst.result_id());
continue;
@@ -195,7 +195,7 @@ namespace MobileGL {
continue;
}
if (derivedPointers.count(operand.words[0]) == 0) continue;
MGLOG_I("SplitArrayVertexInputsPass: array vertex input %%%u reaches a "
MGLOG_D("SplitArrayVertexInputsPass: array vertex input %%%u reaches a "
"SPIR-V opcode %u that this pass cannot follow; leaving it "
"declared as an "
"array",
@@ -95,7 +95,7 @@ namespace MobileGL {
if (member.array.dims_count > 1) {
// Arrays of arrays of structs cannot be declared in the GL 3.3-era GLSL
// MobileGL ingests; record the base so at least element 0 resolves.
MGLOG_W("FlattenGlobalUboMember: multi-dimensional struct array '%s' is not supported, "
MGLOG_W_ONCE("FlattenGlobalUboMember: multi-dimensional struct array '%s' is not supported, "
"flattening element 0 only",
name.c_str());
}
@@ -455,7 +455,7 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
break;
default:
MGLOG_E("NormalizePixelFormat: outFormat: unhandled internalFormat: %s",
MGLOG_E_ONCE("NormalizePixelFormat: outFormat: unhandled internalFormat: %s",
MG_Util::ConvertGLEnumToString(internalFormat).c_str());
// Fallback handling for other formats
// Try to infer format from internal format name
@@ -676,7 +676,7 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
break;
default:
MGLOG_E("NormalizePixelFormat: outType: unhandled internalFormat: %s",
MGLOG_E_ONCE("NormalizePixelFormat: outType: unhandled internalFormat: %s",
MG_Util::ConvertGLEnumToString(internalFormat).c_str());
// Fallback handling for other formats
*outType = GL_UNSIGNED_BYTE;