Compare commits

...
Author SHA1 Message Date
Swung0x48 cee17025a0 [Test] (MG_Test): pin the plain mutable-texture copy every new validator rule is a new way to reject 2026-08-13 03:17:27 -04:00
Swung0x48 9642ae4d20 [Fix, Test] (MG_Impl/Texture, MG_Backend, MG_Test): copy_image compatibility is texel-block size, and neither backend was told which slice to copy 2026-08-13 03:17:27 -04:00
swung0x48 595d140036 Merge branch "feat/cts-copyimage-magma-layers" into dev 2026-08-13 03:11:21 -04:00
swung0x48 b62d1f2078 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): map a layered CopyImageSubData onto the axis each endpoint keeps its slices on, instead of copying slice 0 and calling it done 2026-08-13 02:49:57 -04:00
swung0x48 5e82ff968a Merge branch "feat/cts-rgb9e5-raw-transfer" into dev 2026-08-13 02:47:18 -04:00
swung0x48 6a80a82dd3 [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Test): a packed texel whose client type already spells its storage word must cross glGetTexImage unencoded, or the RGB9_E5 shared exponent gets canonicalized 2026-08-13 02:41:37 -04:00
swung0x48 f9182a5ca3 Merge branch "fix/cts-copyimage-level-validation" into dev 2026-08-13 02:38:43 -04:00
swung0x48 f37b511fca [Fix, Test] (MG_Impl/GLImpl, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_Test, MG_IntegrationTest): a glCopyImageSubData to a mipmap level the texture never had is INVALID_VALUE, not a subresource handed to the driver 2026-08-13 02:28:55 -04:00
swung0x48 38027d21f8 Merge branch "feat/log-level-semantics" into dev 2026-08-13 02:19:26 -04:00
Swung0x48 dd2a62228f [Fix, Feat, Test] (MG_Util, MG_Backend, MG_Impl, MG_State): correct the log severity ordering and unwind the diagnostics it silenced 2026-08-13 02:02:49 -04:00
swung0x48 373aa44dd7 Merge branch "fix/cts-tess-ssbo-crash" into dev 2026-08-13 01:38:58 -04:00
swung0x48 96646df12e [Fix] (MG_Backend/DirectVulkan): review round - failure diagnostics take MGLOG_E, the once-per-patch-size success note takes MGLOG_D, and the per-draw refusal stays latched 2026-08-13 01:34:57 -04:00
swung0x48 f20b20e643 [Fix, Feat, Test] (MG_Backend/DirectVulkan, MG_Test): synthesize the pass-through tessellation control stage GL gives an evaluation-only program, and refuse the half-tessellated pipeline Mali dereferences null inside 2026-08-13 01:28:35 -04:00
swung0x48 2587814970 Merge branch "feat/cts-image-format-qualifier" into dev 2026-08-12 19:23:51 -04:00
swung0x48 43398e33e8 [Fix, Test] (MG_Util, MG_IntegrationTest): review round - anchor a clone only behind a real definition, decline the atomic-image shape, and say what the software lanes cannot falsify 2026-08-12 19:12:39 -04:00
swung0x48 b7557d6615 [Feat, Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_IntegrationTest): bake the bound image format into the ESSL a format-less image declaration needs 2026-08-12 18:56:31 -04:00
77 changed files with 5542 additions and 756 deletions
+1
View File
@@ -285,6 +285,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
+13 -3
View File
@@ -52,11 +52,15 @@
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
// documented to be compiled out of. Log.h redefines them identically, which is legal.
//
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
// drops only D. Any edit here must be mirrored in Log.h.
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
#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
#endif
@@ -91,6 +95,12 @@
#endif
// =============================== Utils ================================ //
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
// 2026-08-13 renumbering unchanged; the contract is and stays
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_ASSERT(condition, ...) \
do { \
+194 -90
View File
@@ -261,14 +261,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
drawBuffer->SyncPersistentMappedRange();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
if (commandOffset + requiredBytes > drawBuffer->GetSize()) {
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
return nullptr;
}
return drawBuffer->MappedData() + commandOffset;
}
if (!indirect) {
MGLOG_E("%s skipped: indirect pointer is null", label);
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
return nullptr;
}
@@ -341,7 +341,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* backendResource = EnsureBufferResource(obj);
if (!backendResource || backendResource->id == 0) {
MGLOG_E("No backend buffer found for %s binding point %zu.",
MGLOG_E_ONCE("No backend buffer found for %s binding point %zu.",
MG_Util::ConvertGLEnumToString(glTarget).c_str(), i);
continue;
}
@@ -385,7 +385,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* backendResource = EnsureBufferResource(bufferObject);
if (!backendResource || backendResource->id == 0) {
MGLOG_E("No backend buffer found for %s.", MG_Util::ConvertGLEnumToString(glTarget).c_str());
MGLOG_E_ONCE("No backend buffer found for %s.", MG_Util::ConvertGLEnumToString(glTarget).c_str());
return;
}
BindBufferId(glTarget, backendResource->id);
@@ -410,7 +410,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// PBO is not needed since it should be handled in frontend
if (!currentVAOObject) {
MGLOG_E("No VAO is currently bound, cannot sync necessary buffers.");
MGLOG_E_ONCE("No VAO is currently bound, cannot sync necessary buffers.");
return;
}
@@ -643,7 +643,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<GLintptr>(target.start),
static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E("EndTransformFeedback: failed to map backend buffer %u for capture readback",
MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u for capture readback",
target.backendId);
continue;
}
@@ -709,7 +709,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<GLsizeiptr>(packedStride * vertices),
GL_MAP_READ_BIT);
if (packed == nullptr) {
MGLOG_E("EndTransformFeedback: failed to map the scatter capture buffer");
MGLOG_E_ONCE("EndTransformFeedback: failed to map the scatter capture buffer");
return;
}
@@ -935,7 +935,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_backendVertexArrayObjects.CollectGarbageIfNeeded();
if (!currentVAOObject || !vaoTwin) {
MGLOG_E("No VAO is currently bound, cannot sync current VAO.");
MGLOG_E_ONCE("No VAO is currently bound, cannot sync current VAO.");
return;
}
@@ -999,7 +999,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data());
break;
case MG_State::GLState::VertexAttribBaseType::Unsupported:
MGLOG_E("SyncCurrentVertexAttributeValues: program=%u location=%u has no enabled array and its "
MGLOG_E_ONCE("SyncCurrentVertexAttributeValues: program=%u location=%u has no enabled array and its "
"shader input type 0x%x is not supported as a current generic vertex attribute",
program->GetExternalIndex(), location, program->GetAttribType(location));
break;
@@ -1469,7 +1469,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (!currentFBO) {
MGLOG_E("No FBO is currently bound, cannot sync current FBO.");
MGLOG_E_ONCE("No FBO is currently bound, cannot sync current FBO.");
continue;
}
@@ -2195,7 +2195,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
twin->GetShaderStorageBlockBindingSignature() !=
ComputeShaderStorageBlockBindingSignature(*currentProgram)) {
ComputeShaderStorageBlockBindingSignature(*currentProgram) ||
// A fourth of the same shape, and the reason glBindImageTexture itself does
// nothing: GLSL ES demands a format layout qualifier on an image where desktop
// GLSL lets a writeonly declaration omit one, so a format-less declaration is
// compiled against the format the application BOUND, and a rebind to a
// different one makes what was built wrong. Asked of the twin because only it
// knows which units its own images address - and answered by an empty-vector
// test for every program that declares its formats, which is nearly all of them.
!twin->ImageUnitFormatsStillMatch()) {
twin->SyncToBackend(currentProgram);
}
g_currentDrawFrontendProgram = currentProgram.get();
@@ -2232,7 +2240,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (twin) {
twin->Bind(target);
} else {
MGLOG_E("No backend FBO found (maybe not synced) for current %s FBO, cannot bind FBO.",
MGLOG_E_ONCE("No backend FBO found (maybe not synced) for current %s FBO, cannot bind FBO.",
(target == FramebufferTarget::Read ? "READ" : "DRAW"));
}
} else {
@@ -2836,7 +2844,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
(GLintptr)range.start, (GLintptr)(range.end - range.start));
}
} else {
MGLOG_E("No backend buffer found for UBO binding, cannot bind UBO.");
MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO.");
}
}
}
@@ -2968,7 +2976,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else {
g_GLESFuncs.glUseProgram(0);
PrgramImpl::g_lastUsedBackendProgramId = 0;
MGLOG_E("No backend program found (maybe not synced) for current program, cannot use program.");
MGLOG_E_ONCE("No backend program found (maybe not synced) for current program, cannot use program.");
}
}
}
@@ -3205,13 +3213,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLuint GetBackendProgramId(GLuint program) {
if (!MG_State::pGLContext->ValidateProgramName(program)) {
MGLOG_E("Invalid frontend program object: %u", program);
MGLOG_E_ONCE("Invalid frontend program object: %u", program);
return 0;
}
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
if (!programObject) {
MGLOG_E("Program object %u is null.", program);
MGLOG_E_ONCE("Program object %u is null.", program);
return 0;
}
@@ -3478,7 +3486,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
stride = sizeof(DrawElementsIndirectCommand);
}
if (stride < static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand))) {
MGLOG_E("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu",
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawElementsIndirectCommand));
return;
}
@@ -3488,7 +3496,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type);
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type);
return;
}
@@ -3518,7 +3526,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
stride = sizeof(DrawElementsIndirectCommand);
}
if (stride < static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand))) {
MGLOG_E("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu",
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawElementsIndirectCommand));
return;
}
@@ -3528,18 +3536,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type);
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type);
return;
}
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!drawBuffer) {
MGLOG_E("MultiDrawElementsIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
return;
}
if (!parameterBuffer) {
MGLOG_E("MultiDrawElementsIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
return;
}
@@ -3550,11 +3558,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
sizeof(DrawElementsIndirectCommand);
if (commandBytes > drawBuffer->GetSize()) {
MGLOG_E("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
}
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
MGLOG_E("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return;
}
@@ -3562,7 +3570,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// have - MappedData() is null there and the reads below would be a null dereference,
// not a wrong picture. The DirectVulkan twin declines the same way.
if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) {
MGLOG_E("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or "
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or "
"draw-indirect buffer");
return;
}
@@ -3586,7 +3594,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
stride = sizeof(DrawArraysIndirectCommand);
}
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawArraysIndirectCommand));
return;
}
@@ -3626,7 +3634,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
stride = sizeof(DrawArraysIndirectCommand);
}
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawArraysIndirectCommand));
return;
}
@@ -3637,11 +3645,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!drawBuffer) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
return;
}
if (!parameterBuffer) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
return;
}
@@ -3652,17 +3660,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
sizeof(DrawArraysIndirectCommand);
if (commandBytes > drawBuffer->GetSize()) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
}
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return;
}
// See the indexed twin: no CPU shadow means no count to read, not a wrong one.
if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or "
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or "
"draw-indirect buffer");
return;
}
@@ -3755,7 +3763,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
return;
}
@@ -3952,7 +3960,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(previousDraw));
FramebufferImpl::InvalidateFramebufferBindingCache();
if (!resolved) {
MGLOG_E("BlitFramebuffer: multisample resolve fallback failed");
MGLOG_E_ONCE("BlitFramebuffer: multisample resolve fallback failed");
}
return resolved;
}
@@ -4251,7 +4259,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
s_stencilProgram = BuildProgram(kStencilFragmentSource);
if (s_depthProgram == 0 || s_stencilProgram == 0) {
s_programsFailed = true;
MGLOG_E("BlitFramebuffer: could not build the multisample replicate programs");
MGLOG_E_ONCE("BlitFramebuffer: could not build the multisample replicate programs");
return false;
}
s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform");
@@ -4455,7 +4463,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(previousRead));
FramebufferImpl::InvalidateFramebufferBindingCache();
if (!ok) {
MGLOG_E("BlitFramebuffer: could not stage the source for the multisample replicate");
MGLOG_E_ONCE("BlitFramebuffer: could not stage the source for the multisample replicate");
return false;
}
@@ -4524,7 +4532,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Everything the pass disturbed goes back through emulationState's destructor.
if (!replicated) {
MGLOG_E("BlitFramebuffer: multisample replicate fallback failed");
MGLOG_E_ONCE("BlitFramebuffer: multisample replicate fallback failed");
}
return replicated;
}
@@ -4559,7 +4567,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// outright, desktop GL replicates the source sample into every destination one.
if (ReplicateBlitIntoMultisampleDraw(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask)) {
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
MGLOG_E("BlitFramebuffer: colour replicate into a multisample draw framebuffer is not emulated");
MGLOG_E_ONCE("BlitFramebuffer: colour replicate into a multisample draw framebuffer is not emulated");
}
}
return;
@@ -4660,7 +4668,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
if (!TextureImpl::IsSupportedTextureTarget(textureTarget)) {
MGLOG_E(" Texture target %s is not supported, skipping.",
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
return false;
}
@@ -4669,7 +4677,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
{
const auto& textureObject = bindingSlot.GetBoundObject();
if (!textureObject) {
MGLOG_W("%s: Texture target %s does not have texture bound.", __func__,
MGLOG_D("%s: Texture target %s does not have texture bound.", __func__,
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
}
@@ -4918,7 +4926,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
MGLOG_E("%s failed: %s. target=%s, format=%s", operation,
MGLOG_E_ONCE("%s failed: %s. target=%s, format=%s", operation,
MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(target).c_str(),
MG_Util::ConvertTextureInternalFormatToString(format).c_str());
@@ -5254,7 +5262,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
.GetBoundObject();
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
if (!backendTextureSlot || !*backendTextureSlot) {
MGLOG_E("CopyTexSubImage2D: No backend texture found for texture %u.",
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
textureObject ? textureObject->GetExternalIndex() : 0);
return;
}
@@ -5305,7 +5313,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Uint>(currentTex), target, level, isStencilFormat);
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
return;
}
@@ -5349,7 +5357,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
.GetBoundObject();
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
if (!backendTextureSlot || !*backendTextureSlot) {
MGLOG_E("CopyTexSubImage2D: No backend texture found for texture %u.",
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
textureObject ? textureObject->GetExternalIndex() : 0);
return;
}
@@ -5387,7 +5395,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
return;
}
@@ -5555,6 +5563,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glMemoryBarrierByRegion(barriers);
}
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
//
// The frontend hands this backend the target the APPLICATION named, and three of the
// targets core GL has do not exist in ES at all. They are not missing here either - the
// texture managers already store a 1D texture as a height-1 2D one, a 1D array as a
// height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) -
// but glCopyImageSubData was the one path that never asked for that translation and passed
// 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen,
// and with the error only asserted on (asserts are compiled out of an INFO build) the
// destination silently keeps whatever it held.
//
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
// target.
struct GLESCopyImageEndpoint {
GLenum target = GL_TEXTURE_2D;
GLint x = 0;
GLint y = 0;
GLint z = 0;
};
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) {
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
GLESCopyImageEndpoint endpoint{};
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
if (stateTarget == TextureTarget::Texture1DArray) {
endpoint.x = x;
endpoint.y = 0;
endpoint.z = y;
return endpoint;
}
endpoint.x = x;
endpoint.y = y;
endpoint.z = z;
return endpoint;
}
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
// of the copy is one - GL forbids a copy whose two endpoints disagree about how many layers
// move, so at most one of the two can be a 1D array only in the degenerate single-layer
// case, where the swap is the identity anyway.
static void ApplyGLESCopyImageExtent(GLenum appSrcTarget, GLenum appDstTarget, GLsizei& height, GLsizei& depth) {
const TextureTarget srcStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appSrcTarget);
const TextureTarget dstStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appDstTarget);
if (srcStateTarget != TextureTarget::Texture1DArray && dstStateTarget != TextureTarget::Texture1DArray) {
return;
}
std::swap(height, depth);
}
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
@@ -5573,6 +5631,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::SyncTextureObjectToBackend(srcTexture);
const SharedPtr<TextureImpl::BackendTextureObject> dstBackendTexture =
TextureImpl::SyncTextureObjectToBackend(dstTexture);
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was
// supposed to catch it expands to nothing. The four GetBackendTextureId() calls below
// are the same dereference. The frontend validator is what keeps this unreachable and
// what reports the error the application is owed; declining is only how a future gap up
// there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!srcBackendTexture || !dstBackendTexture) {
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
return;
}
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ);
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ);
GLsizei copyHeight = srcHeight;
GLsizei copyDepth = srcDepth;
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat());
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat());
@@ -5581,12 +5655,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
"DirectGLES CopyImageSubData only supports depth-only image copies.");
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
dstBackendTexture->GetBackendTextureId(), dstLevel, dstX, dstY, srcWidth, srcHeight);
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
return;
}
@@ -5597,29 +5671,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
// with the always-live helper so a stale flag cannot misroute a
// succeeded native copy into the 2D-only fallback.
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth);
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth);
const GLenum copyImageError = g_GLESFuncs.glGetError();
if (copyImageError == GL_NO_ERROR) {
return;
}
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES color CopyImageSubData only supports single-layer copies.");
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY);
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
return;
}
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth);
AssertNoGLError("glCopyImageSubData");
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth);
// Every error condition glCopyImageSubData has was already ruled out by the frontend
// validator, so a driver error here is an internal invariant violation, not something
// the application can provoke. Say so where an INFO build can still see it, then trap
// in the builds that trap - the previous bare assert left a release build with a
// destination that silently kept its old contents.
const GLenum copyImageError = g_GLESFuncs.glGetError();
if (copyImageError != GL_NO_ERROR) {
MGLOG_E_ONCE("glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)",
MG_Util::ConvertGLEnumToString(copyImageError).c_str(),
MG_Util::ConvertGLEnumToString(src.target).c_str(),
MG_Util::ConvertGLEnumToString(srcTarget).c_str(),
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
}
}
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
@@ -5994,7 +6082,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("ReadPixels: %s readback PBO is too small", what);
MGLOG_E_ONCE("ReadPixels: %s readback PBO is too small", what);
return false;
}
Vector<Uint8> rowBuf(rowBytes);
@@ -6148,7 +6236,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
s_stencilProgram = ReplicateBlitImpl::BuildProgram(kStencilFetchFragmentSource);
if (s_depthProgram == 0 || s_stencilProgram == 0) {
s_programsFailed = true;
MGLOG_E("ReadPixels: could not build the depth/stencil readback programs");
MGLOG_E_ONCE("ReadPixels: could not build the depth/stencil readback programs");
return false;
}
s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform");
@@ -6406,7 +6494,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ClearGLErrors();
g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
MGLOG_E("ReadPixels: the %s conversion pass failed", stencilAspect ? "stencil" : "depth");
MGLOG_E_ONCE("ReadPixels: the %s conversion pass failed", stencilAspect ? "stencil" : "depth");
return false;
}
@@ -6422,7 +6510,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, outWords.data());
const GLenum readError = g_GLESFuncs.glGetError();
if (readError != GL_NO_ERROR) {
MGLOG_E("ReadPixels: could not read the %s conversion target back: %s",
MGLOG_E_ONCE("ReadPixels: could not read the %s conversion target back: %s",
stencilAspect ? "stencil" : "depth", MG_Util::ConvertGLEnumToString(readError).c_str());
return false;
}
@@ -6442,20 +6530,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, slot.framebuffer);
if (!StageAspect(slot, candidates, stencilAspect, isDefault, x, y, width, height)) {
MGLOG_E("ReadPixels: no ES-compatible scratch format for the %s source",
MGLOG_E_ONCE("ReadPixels: no ES-compatible scratch format for the %s source",
stencilAspect ? "stencil" : "depth");
return false;
}
if (!EnsureColorTexture(width, height)) {
MGLOG_E("ReadPixels: could not allocate the depth/stencil conversion target");
MGLOG_E_ONCE("ReadPixels: could not allocate the depth/stencil conversion target");
return false;
}
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, s_colorFramebuffer);
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
s_colorTexture, 0);
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E("ReadPixels: the depth/stencil conversion target is not renderable");
MGLOG_E_ONCE("ReadPixels: the depth/stencil conversion target is not renderable");
return false;
}
@@ -6560,7 +6648,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (DepthStencilSamplingReadImpl::Read(x, y, width, height, &outDepth, /*outStencil=*/nullptr)) {
return true;
}
MGLOG_E("ReadPixels: no depth readback path is available: native reads failed with %s and the "
MGLOG_E_ONCE("ReadPixels: no depth readback path is available: native reads failed with %s and the "
"sampling emulation could not service the source",
MG_Util::ConvertGLEnumToString(floatError).c_str());
return false;
@@ -6664,7 +6752,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (DepthStencilSamplingReadImpl::Read(x, y, width, height, /*outDepth=*/nullptr, &outStencil)) {
return true;
}
MGLOG_E("ReadPixels: no stencil readback path is available: native reads failed with %s and the "
MGLOG_E_ONCE("ReadPixels: no stencil readback path is available: native reads failed with %s and the "
"sampling emulation could not service the source",
MG_Util::ConvertGLEnumToString(packedError).c_str());
return false;
@@ -6969,7 +7057,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool integerAttachment =
attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT;
if (mapping.isInteger != integerAttachment) {
MGLOG_E("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
MGLOG_E_ONCE("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
MG_Util::ConvertGLEnumToString(format).c_str());
return true;
}
@@ -7045,7 +7133,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
if (wideType == GL_NONE) {
MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback",
MGLOG_E_ONCE("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return true;
}
@@ -7159,6 +7247,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
// glGetTexImage returns the stored texels, and for a packed internal format read with the
// matching client type the shadow word already IS the client word. Decoding it to float and
// re-encoding would canonicalize an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000: the
// same value, different bits), so those pairs copy the words straight through.
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
textureMipmapObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
if (!ReadbackImpl::StorePackedWordsToClient(static_cast<const Uint8*>(shadow), width, sliceHeight,
sliceCount, type, pixels, applyPackImageParams)) {
return false;
}
MGLOG_D("GetTexImage: copied %s/%s verbatim from the CPU shadow copy",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return true;
}
Vector<Uint8> wide;
Bool isInteger = false;
Bool isSigned = false;
@@ -7228,7 +7332,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) &&
GetReadbackDstPixelSize(conversionMapping, type) != 0;
if (!useNativeReadback && !convertible) {
MGLOG_E("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return;
}
@@ -7249,7 +7353,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str());
if (fbStatus != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E("ReadPixels: bound READ FBO is not complete");
MGLOG_E_ONCE("ReadPixels: bound READ FBO is not complete");
return;
}
// ES only guarantees GL_RGBA/GL_UNSIGNED_BYTE and GL_RGBA_INTEGER/GL_(UNSIGNED_)INT for the
@@ -7274,7 +7378,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: finished via client-format conversion");
return;
}
MGLOG_E("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return;
}
@@ -7307,7 +7411,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject);
MGLOG_D("ReadPixels: Using PBO %u", pixelPackBufferObject->GetExternalIndex());
if (!backendResource || backendResource->id == 0) {
MGLOG_E("ReadPixels: No backend buffer found for PBO %u.",
MGLOG_E_ONCE("ReadPixels: No backend buffer found for PBO %u.",
pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0);
return;
}
@@ -7339,7 +7443,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: finished via client-format conversion after native failure");
return;
}
MGLOG_E("ReadPixels: native read of %s/%s failed (%s) and no conversion path covers it, "
MGLOG_E_ONCE("ReadPixels: native read of %s/%s failed (%s) and no conversion path covers it, "
"skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(),
MG_Util::ConvertGLEnumToString(nativeReadError).c_str());
@@ -7359,7 +7463,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: Unmapping PBO");
g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
} else {
MGLOG_E("ReadPixels: glMapBufferRange returned nullptr");
MGLOG_E_ONCE("ReadPixels: glMapBufferRange returned nullptr");
}
}
MGLOG_D("ReadPixels: finished");
@@ -7395,7 +7499,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) &&
GetReadbackDstPixelSize(conversionMapping, type) != 0;
if (!useNativeReadback && !convertible) {
MGLOG_E("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
MGLOG_E_ONCE("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return;
}
@@ -7423,7 +7527,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
if (!backendTextureSlot || !*backendTextureSlot) {
MGLOG_E("GetTexImage: No backend texture found for texture %u.",
MGLOG_E_ONCE("GetTexImage: No backend texture found for texture %u.",
textureObject ? textureObject->GetExternalIndex() : 0);
return;
}
@@ -7491,7 +7595,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("GetTexImage: texture storage type = %d", (int)storageType);
if (storageType == TextureStorageType::Buffer) {
MGLOG_E("GetTexImage: Texture storage type Buffer is not supported.");
MGLOG_E_ONCE("GetTexImage: Texture storage type Buffer is not supported.");
return;
}
@@ -7503,7 +7607,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// levelRange.y() is GL_TEXTURE_MAX_LEVEL, an inclusive level index — a single-level
// texture has range [0, 0] and level 0 must be readable.
if (static_cast<Uint>(level) < levelRange.x() || static_cast<Uint>(level) > levelRange.y()) {
MGLOG_E("GetTexImage: Requested level %d is out of range (base level %u, max level %u), skipping readback",
MGLOG_E_ONCE("GetTexImage: Requested level %d is out of range (base level %u, max level %u), skipping readback",
level, levelRange.x(), levelRange.y());
return;
}
@@ -7600,15 +7704,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
if (!tempFBOComplete) {
MGLOG_E("GetTexImage: READ FBO incomplete and no shadow copy available, skipping readback");
MGLOG_E_ONCE("GetTexImage: READ FBO incomplete and no shadow copy available, skipping readback");
return;
}
MGLOG_E("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
MGLOG_E_ONCE("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return;
}
if (!tempFBOComplete) {
MGLOG_E("GetTexImage: bound READ FBO is not complete");
MGLOG_E_ONCE("GetTexImage: bound READ FBO is not complete");
return;
}
@@ -7636,7 +7740,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject);
MGLOG_D("GetTexImage: Using PBO %u", pixelPackBufferObject->GetExternalIndex());
if (!backendResource || backendResource->id == 0) {
MGLOG_E("GetTexImage: No backend buffer found for PBO %u.",
MGLOG_E_ONCE("GetTexImage: No backend buffer found for PBO %u.",
pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0);
return;
}
@@ -7672,7 +7776,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ReadPixels: Unmapping PBO");
g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
} else {
MGLOG_E("ReadPixels: glMapBufferRange returned nullptr");
MGLOG_E_ONCE("ReadPixels: glMapBufferRange returned nullptr");
}
}
@@ -8059,7 +8163,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (g_requestedSwapInterval < 0) return;
if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) return;
const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval);
MGLOG_I("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval,
MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval,
ok ? "ok" : "failed");
}
} // namespace
@@ -8170,12 +8274,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool MakeCurrent() {
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE ||
g_Context == EGL_NO_CONTEXT) {
MGLOG_E("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized");
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized");
return false;
}
if (!g_EGLFuncs.eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context)) {
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
MGLOG_E("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
return false;
}
InvalidateEglVerifiedStamp();
@@ -8208,7 +8312,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
MGLOG_E("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
MGLOG_E_ONCE("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
return false;
}
// Clearing the global owner works from ANY thread (a release request can
+278 -78
View File
@@ -598,7 +598,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
if (!ptr) {
MGLOG_E("Ops_AcquirePersistentMap: glMapBufferRange(persistent) failed for buffer %u",
MGLOG_E_ONCE("Ops_AcquirePersistentMap: glMapBufferRange(persistent) failed for buffer %u",
resource->id);
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
@@ -716,7 +716,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
return;
}
MGLOG_E("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData",
MGLOG_E_ONCE("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData",
resource->id);
}
UploadRangeNow(*resource, bufferObject, range.start, range.end);
@@ -739,7 +739,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id);
MGLOG_E_ONCE("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id);
return;
}
bufferObject.WritebackFromBackend({mapped, size}, 0);
@@ -993,8 +993,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else {
g_GLESFuncs.glGenBuffers(1, &resource->id);
if (resource->id == 0) {
MGLOG_E("Failed to generate buffer object.");
MGLOG_E("ES glGetError(): %s",
MGLOG_E_ONCE("Failed to generate buffer object.");
MGLOG_E_ONCE("ES glGetError(): %s",
MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
return resource;
}
@@ -1224,7 +1224,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
if (id == 0) {
MGLOG_E("Global-UBO ring: persistent storage creation failed (%zu bytes); "
MGLOG_E_ONCE("Global-UBO ring: persistent storage creation failed (%zu bytes); "
"falling back to glBufferSubData uploads.",
newSize);
g_uboRing.creationFailed = true;
@@ -1470,8 +1470,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_clientAttributeBufferIds.fill(0);
g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId);
if (m_backendVAOId == 0) {
MGLOG_E("Failed to generate vertex array object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
MGLOG_E_ONCE("Failed to generate vertex array object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated vertex array object with ID: %u.", m_backendVAOId);
}
@@ -1529,13 +1529,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
inline Bool BindAttributeBuffer(const MG_State::GLState::VertexAttribute& attrib) {
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) {
MGLOG_W("Attribute has no bound buffer, skipping.");
MGLOG_W_ONCE("Attribute has no bound buffer, skipping.");
return false;
}
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
if (!backendResource || backendResource->id == 0) {
MGLOG_E("No backend buffer found for attribute's buffer, cannot bind attribute.");
MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute.");
return false;
}
@@ -1586,12 +1586,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
inline Bool SyncZeroStrideAttribute(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib) {
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) {
MGLOG_W("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
return false;
}
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
if (!backendResource || backendResource->id == 0) {
MGLOG_E("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
return false;
}
@@ -1621,7 +1621,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateVAOObject) {
MGLOG_E("State VAO object is null, cannot sync to backend.");
MGLOG_E_ONCE("State VAO object is null, cannot sync to backend.");
return;
}
@@ -1694,7 +1694,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// because the array stayed enabled with no pointer the failed call could set.
// The type test therefore covers the storage, not the spelling.
if (attrib.IsLong || attrib.Type == DataType::Float64) {
MGLOG_I("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
"backend cannot feed - disabling the array",
attribIndex);
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
@@ -1761,7 +1761,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (formatMayBeRefused && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
MGLOG_I("DirectGLES: the driver refused the vertex format of attribute %u "
MGLOG_W_ONCE("DirectGLES: the driver refused the vertex format of attribute %u "
"(size=%d bgra=%d type=%s) - disabling the array so the draw cannot "
"fetch through a pointer the driver never accepted",
attribIndex, attrib.Size, attrib.IsBgra ? 1 : 0,
@@ -1784,7 +1784,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, backendResource->id);
indexBufferSynced = true;
} else {
MGLOG_W("No backend buffer found for index buffer binding, cannot bind index buffer.");
MGLOG_W_ONCE("No backend buffer found for index buffer binding, cannot bind index buffer.");
}
} else {
g_GLESFuncs.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
@@ -1842,7 +1842,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (bufferId == 0) {
g_GLESFuncs.glGenBuffers(1, &bufferId);
if (bufferId == 0) {
MGLOG_E("Failed to create client-side vertex attribute upload buffer.");
MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer.");
continue;
}
}
@@ -1877,8 +1877,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendTextureId == 0) {
MGLOG_E("Failed to generate texture object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
MGLOG_E_ONCE("Failed to generate texture object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated texture object with ID: %u.", m_backendTextureId);
}
@@ -1956,8 +1956,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendTextureId == 0) {
MGLOG_E("Failed to regenerate texture object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
MGLOG_E_ONCE("Failed to regenerate texture object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Regenerated texture object with ID: %u.", m_backendTextureId);
}
@@ -2391,7 +2391,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (!stateTextureObject) {
MGLOG_E("State texture object is null, cannot sync to backend.");
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
@@ -2424,7 +2424,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
MGLOG_E(" Texture target %s is not supported, skipping.",
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
return;
}
@@ -2576,7 +2576,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
default:
MGLOG_E("Unhandled texture target %s",
MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(stateTextureObject->GetTarget()).c_str());
break;
}
@@ -2654,7 +2654,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<GLsizei>(storageSize.z()));
break;
default:
MGLOG_E("Unhandled immutable texture target %s",
MGLOG_E_ONCE("Unhandled immutable texture target %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
break;
}
@@ -2776,7 +2776,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
break;
}
default: {
MGLOG_E("Unhandled texture target %s",
MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
}
}
@@ -2828,7 +2828,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto byteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
if (byteSize == 0) {
MGLOG_W("Mipmap level %d has no data, skipping update.", level);
MGLOG_D("Mipmap level %d has no data, skipping update.", level);
continue;
}
@@ -2979,7 +2979,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
break;
default:
MGLOG_E("Unhandled texture target %s",
MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(stateTextureObject->GetTarget()).c_str());
break;
}
@@ -3007,7 +3007,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Need to sync texture buffer if not synced yet
auto* backendBufferResource = BufferImpl::EnsureBufferResource(buffer);
if (!backendBufferResource || backendBufferResource->id == 0) {
MGLOG_E("Failed to sync backing buffer for texture buffer with ID: %u",
MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u",
stateTextureObject->GetExternalIndex());
return;
}
@@ -3026,15 +3026,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// below that without EXT/OES_texture_buffer. Calling it was an unconditional
// null dereference. There is no conformant way to refuse the call (it is valid
// in the context MobileGL claims), so the texture is left unbacked and the
// reason is stated once per respecify at a level that survives the shipped
// INFO build - MGLOG_E is compiled out there, which is exactly how this class
// of defect stays invisible.
// reason is stated once per object, latched by the flag below. It was parked
// at MGLOG_I while the level ordering compiled MGLOG_W out of INFO builds;
// W is the correct level and now survives there.
if (!AreBufferTexturesSupported()) {
if (m_bufferTextureUnsupportedReported) {
break;
}
m_bufferTextureUnsupportedReported = true;
MGLOG_I("Texture buffer %u cannot be backed: this ES driver has no buffer "
MGLOG_W("Texture buffer %u cannot be backed: this ES driver has no buffer "
"textures (%s). Every draw sampling it will read zero and every "
"shader declaring a samplerBuffer will fail to compile. MobileGL "
"still advertises GL_MAX_TEXTURE_BUFFER_SIZE = %d because an "
@@ -3062,7 +3062,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId,
static_cast<GLintptr>(rangeOffset),
static_cast<GLsizeiptr>(rangeSize))) {
MGLOG_I("Texture buffer %u names a sub-range but the driver has no "
MGLOG_W_ONCE("Texture buffer %u names a sub-range but the driver has no "
"glTexBufferRange; binding the whole buffer instead",
stateTextureObject->GetExternalIndex());
CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
@@ -3080,7 +3080,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a
// backstop for a state object that grew a new storage kind. Skipping the upload
// renders wrong; throwing unwinds through the C GL ABI and kills the process.
MGLOG_I("DirectGLES texture sync: no upload path for storage type %d on texture %u; "
MGLOG_E_ONCE("DirectGLES texture sync: no upload path for storage type %d on texture %u; "
"skipping this sync",
static_cast<int>(stateTextureObject->GetStorageType()),
stateTextureObject->GetExternalIndex());
@@ -3115,7 +3115,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
if (!stateTextureObject) {
MGLOG_E("State texture object is null, cannot sync to backend.");
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
@@ -3136,7 +3136,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
MGLOG_E(" Texture target %s is not supported, skipping.",
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
return;
}
@@ -3225,7 +3225,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
if (!stateTextureObject) {
MGLOG_E("State texture object is null, cannot sync to backend.");
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
@@ -3245,7 +3245,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
MGLOG_E(" Texture target %s is not supported, skipping.",
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
return;
}
@@ -3393,8 +3393,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendFBOId == 0) {
MGLOG_E("Failed to generate framebuffer object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
MGLOG_E_ONCE("Failed to generate framebuffer object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated framebuffer object with ID: %u.", m_backendFBOId);
}
@@ -3530,7 +3530,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
backendTextureObject = newTextureSlot;
}
if (!backendTextureObject) {
MGLOG_E("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
return false;
}
backendTextureObject->SyncMipmapsToBackend(textureObject);
@@ -3899,7 +3899,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateFBOObject) {
MGLOG_E("State FBO object is null, cannot sync to backend.");
MGLOG_E_ONCE("State FBO object is null, cannot sync to backend.");
return;
}
MGLOG_D("Syncing FBO with backend ID %u to backend for state ID %u, as %s FBO", m_backendFBOId,
@@ -4418,8 +4418,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
m_backendProgramId = g_GLESFuncs.glCreateProgram();
if (m_backendProgramId == 0) {
MGLOG_E("Failed to create program object in backend.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
MGLOG_E_ONCE("Failed to create program object in backend.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Created backend program object with ID: %u", m_backendProgramId);
@@ -4497,13 +4497,168 @@ namespace MobileGL::MG_Backend::DirectGLES {
return signature;
}
namespace {
// The GL internal format bound to an image unit right now. GL_NONE for a unit
// outside the frontend's array, which cannot be addressed at all.
Uint BoundImageUnitFormat(Int unit) {
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0;
return static_cast<Uint>(MG_State::pGLContext->GetImageTextureBinding(unit).Format);
}
// Combines one (unit, format) pair into a running digest. Commutative, so the order
// the uniforms are walked in cannot change the answer, and mixed rather than summed
// so a unit and a format cannot trade places between two pairs and cancel out.
Uint64 MixImageUnitFormat(Uint64 signature, Int unit, Uint format) {
Uint64 entry = static_cast<Uint64>(static_cast<Uint32>(unit)) + 0x9e3779b97f4a7c15ull;
entry ^= static_cast<Uint64>(format) + 0xbf58476d1ce4e5b9ull + (entry << 6) + (entry >> 2);
return signature + entry;
}
// Reflection names an array uniform after its first element ("g_image[0]") at every
// location it spans; SPIR-V names the variable once, without the subscript. This is
// the name both sides agree on.
String ImageUniformBaseName(const String& reflectionName) {
if (reflectionName.size() >= 3 && reflectionName.compare(reflectionName.size() - 3, 3, "[0]") == 0) {
return reflectionName.substr(0, reflectionName.size() - 3);
}
return reflectionName;
}
// Whether a glslang layout format is one GLSL ES has in core; the rest reach ES only
// through GL_NV_image_formats. Asked of DECLARED formats, which this backend passes
// through untouched - the emitted ESSL still has to be legal for the driver.
Bool IsCoreEsslLayoutFormat(glslang::TLayoutFormat format) {
switch (format) {
case glslang::ElfRgba32f:
case glslang::ElfRgba16f:
case glslang::ElfR32f:
case glslang::ElfRgba8:
case glslang::ElfRgba8Snorm:
case glslang::ElfRgba32i:
case glslang::ElfRgba16i:
case glslang::ElfRgba8i:
case glslang::ElfR32i:
case glslang::ElfRgba32ui:
case glslang::ElfRgba16ui:
case glslang::ElfRgba8ui:
case glslang::ElfR32ui:
return true;
default:
return false;
}
}
} // namespace
// What the format bake needs from the frontend, collected in one walk of the uniform
// reflection: which image uniforms declared NO format (the only ones a bake may touch -
// a declared format is authoritative and stays), what the units they address currently
// hold, and whether any format in play - declared or baked - is outside the ES core set.
ImageFormatBakeInputs CollectImageFormatBakeInputs(
const MG_State::GLState::ProgramObject& stateProgramObject) {
ImageFormatBakeInputs inputs;
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = stateProgramObject.GetUniformName(loc);
if (name.empty()) continue;
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
const glslang::TType* type = stateProgramObject.GetUniformTType(loc);
if (type == nullptr) continue;
if (type->getQualifier().hasFormat()) {
// Declared, and therefore left exactly as written - but a non-core spelling
// still needs the extension directive to survive the ES compiler.
if (!IsCoreEsslLayoutFormat(type->getQualifier().getFormat())) {
inputs.needsExtendedImageFormats = true;
}
continue;
}
const Int unit = stateProgramObject.GetUniformSamplerOrImageUnitIndex(loc);
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) continue;
const Uint boundFormat = BoundImageUnitFormat(unit);
// Every format-less uniform contributes to the rebuild key, including one whose
// unit holds nothing yet: an image bound for the first time AFTER the link has
// to move the key, or the program built against "nothing bound" would never be
// rebuilt against the real format.
inputs.units.push_back(unit);
inputs.signature = MixImageUnitFormat(inputs.signature, unit, boundFormat);
if (boundFormat == 0) continue;
if (!MG_Util::ShaderTranspiler::ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(boundFormat)) {
// Outside the GLSL ES core set, so the emitted ESSL only compiles with
// GL_NV_image_formats. Without the extension there is no legal spelling at
// all, and baking one would trade a "no format qualifier" compile error for
// an "unsupported format" one - so the image is left format-less. Its unit
// stays in the key, so a rebind to a core format still rebuilds and works.
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES "
"core cannot spell and this driver has no GL_NV_image_formats for.",
name.c_str(), unit, boundFormat);
continue;
}
inputs.needsExtendedImageFormats = true;
}
const String baseName = ImageUniformBaseName(name);
const auto existing = inputs.glFormatByUniformName.find(baseName);
if (existing == inputs.glFormatByUniformName.end()) {
inputs.glFormatByUniformName.emplace(baseName, boundFormat);
} else if (existing->second != boundFormat) {
// An ARRAY whose elements were pointed at units holding different formats.
// One declaration carries one qualifier, so there is no spelling for it, and
// the uniform is left format-less rather than given a format that is wrong
// for all but one element. Marked in place with GL_NONE and swept below -
// never by erasing here, because the entry is reached again by the array's
// remaining elements and a flat hash map must not be mutated structurally
// while an iterator into it is live.
existing->second = 0;
}
}
for (const auto& entry : inputs.glFormatByUniformName) {
if (entry.second == 0) inputs.conflictedNames.push_back(entry.first);
}
for (const auto& conflicted : inputs.conflictedNames) {
inputs.glFormatByUniformName.erase(conflicted);
}
// Split off the ones SPIRV-Cross will not print. They cannot go through the module -
// it throws for them when targeting ESSL, and the stage is lost - so they are spelled
// into the emitted text instead. Collected first, erased after, because a flat hash
// map must not be restructured while it is being walked.
Vector<String> textCompleted;
for (const auto& entry : inputs.glFormatByUniformName) {
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(entry.second)) {
continue;
}
String spelling = MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(entry.second);
if (spelling.empty()) continue; // no image-format spelling at all; nothing to write
inputs.esslFormatQualifierByUniformName.emplace(entry.first, Move(spelling));
textCompleted.push_back(entry.first);
}
for (const auto& name : textCompleted) {
inputs.glFormatByUniformName.erase(name);
}
return inputs;
}
Uint64 BackendProgramObjectImpl::ComputeImageUnitFormatSignature() const {
if (m_formatlessImageUnits.empty()) return 0; // all but a handful of programs
Uint64 signature = 0;
for (const Int unit : m_formatlessImageUnits) {
signature = MixImageUnitFormat(signature, unit, BoundImageUnitFormat(unit));
}
return signature;
}
Bool BackendProgramObjectImpl::ImageUnitFormatsStillMatch() const {
if (m_formatlessImageUnits.empty()) return m_imageUnitFormatSignature == 0;
return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature;
}
void BackendProgramObjectImpl::SyncToBackend(
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateProgramObject) {
MGLOG_E("State program object is null, skipping backend sync.");
MGLOG_E_ONCE("State program object is null, skipping backend sync.");
return;
}
// Recorded before either early return below, so Use() can always name the GL
@@ -4516,7 +4671,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// a LINK_STATUS it already reported true, so "linked but not drawable" is the
// answer, and this is where the ES backend expresses it.
if (!stateProgramObject->GetLinkStatus() || !stateProgramObject->GetSpirvStatus()) {
MGLOG_E("Program object is not linked or has no generated SPIR-V, skipping backend sync. State "
MGLOG_E_ONCE("Program object is not linked or has no generated SPIR-V, skipping backend sync. State "
"program ID: %u",
stateProgramObject->GetExternalIndex());
return;
@@ -4537,6 +4692,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
// this build current - the draw path compares the signature and rebuilds on a change.
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
// The same shape again for image FORMATS: what a format-less image declaration
// compiles to depends on live glBindImageTexture state, so the pairs it was built
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
// Taken BEFORE the transpile loop so both the bake and the key see one snapshot.
const ImageFormatBakeInputs imageFormatBake = CollectImageFormatBakeInputs(*stateProgramObject);
m_formatlessImageUnits = imageFormatBake.units;
m_imageUnitFormatSignature = imageFormatBake.signature;
for (const auto& conflicted : imageFormatBake.conflictedNames) {
MGLOG_D("Image uniform '%s' of program %u declares no format and its elements address units with "
"different bound formats; left format-less.",
conflicted.c_str(), stateProgramObject->GetExternalIndex());
}
// Detach all existing shaders
GLint attachedCount = 0;
@@ -4596,7 +4763,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLuint backendShaderId = g_GLESFuncs.glCreateShader(glShaderType);
if (backendShaderId == 0) {
MGLOG_E("Failed to create backend shader for attachment.");
MGLOG_E_ONCE("Failed to create backend shader for attachment.");
continue;
}
String source;
@@ -4606,12 +4773,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ES 3.2 or EXT/OES_texture_buffer on the host. Without it SPIRV-Cross emits
// `#extension GL_EXT_texture_buffer : require` and the driver rejects both that
// and the isamplerBuffer keyword - the program never links and every draw using it
// becomes a silent no-op. Say so here, naming the stage, instead of leaving a
// driver info log the shipped INFO build compiles out (MGLOG_E is inactive there).
// becomes a silent no-op. Say so here, naming the stage. Deliberately unlatched:
// this is bounded by program count, and which stage failed is the whole point.
// Gated on the capability so the module walk never runs on a healthy driver.
if (!AreBufferTexturesSupported() &&
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresBufferTextureSampler(spirvCode)) {
MGLOG_I("Program %u stage %s samples a buffer texture, which this ES driver "
MGLOG_E("Program %u stage %s samples a buffer texture, which this ES driver "
"cannot provide (%s). The shader will not compile and the program will "
"not link; every draw using it is a no-op.",
m_backendProgramId,
@@ -4719,6 +4886,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &arrayImageSpirv;
}
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
// format"), which loses the whole program. Give each such image the format the
// application bound to its unit - the one GL's format-class rules make correct -
// so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which
// also rewrites image types, so this one is looking at the final shapes.
//
// Gated on the module actually declaring one: the map is empty for every program
// whose images all declare formats, and the cheap probe keeps a program that has
// an unbound format-less image from paying an optimizer round trip per stage.
Vector<unsigned int> imageFormatSpirv;
if (!imageFormatBake.glFormatByUniformName.empty() &&
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv) &&
!imageFormatSpirv.empty()) {
effectiveSpirv = &imageFormatSpirv;
}
// GLSL ES demands a constant integral expression to index a fragment output
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
@@ -4761,14 +4947,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
spvcSession.Compile(&result);
if (!result) {
// MGLOG_I, for the same reason as the compile- and link-failure diagnostics
// below: every CI, retrace and release build compiles at
// MOBILEGL_LOG_LEVEL_INFO, where MGLOG_E expands to nothing. A stage that
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics below:
// one line per failing stage is bounded by program count and naming the
// stage is the entire diagnostic value. A stage that
// never reaches the driver leaves the program short of that stage, so the
// link fails with an EMPTY driver info log - the least debuggable failure
// MobileGL can produce, and what hid the whole
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
MGLOG_I("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
"SPIRV-Cross error: %s",
stateProgramObject->GetExternalIndex(),
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
@@ -4786,8 +4972,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// because a header concern reads better before the body ones.
source = RetargetTextureBufferExtension(std::move(source),
g_GLESCapabilities.TextureBufferSupport);
// The other header-level rewrite, and next to that one for the same reason. The
// formats it covers are both the ones the bake above put into the module and the
// ones the application declared itself - either can be outside the thirteen GLSL
// ES has in core, and neither reaches the driver without this directive.
source = RequestExtendedImageFormats(std::move(source),
imageFormatBake.needsExtendedImageFormats &&
g_GLESCapabilities.SupportsExtendedImageFormats);
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
// The completion half of the format bake, for the formats SPIRV-Cross throws on
// rather than prints (r8ui and the rest of its desktop-only set). Empty for every
// program whose format-less images bound a format the module could carry, which
// is the normal case - those were baked into the SPIR-V above and this pass finds
// their declarations already qualified. AFTER the rebind, so the layout qualifier
// it edits is the one that already exists; BEFORE the split and the binding
// strip, so both halves of a split image inherit the format.
source = BakeImageFormatQualifiers(std::move(source),
imageFormatBake.esslFormatQualifierByUniformName);
// Wedged between those two on purpose:
// * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto
// both halves of a split image is already the frontend texture unit (and so
@@ -4839,14 +5041,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
log.back() = '\0';
// MGLOG_I, deliberately. Every CI, retrace and release build compiles at
// MOBILEGL_LOG_LEVEL_INFO, where MGLOG_E and MGLOG_W expand to nothing
// (Log.h orders DEBUG < WARN < ERROR < INFO), so this diagnostic used to
// exist only in debug builds: the Android retrace artifact carried 294
// INFO lines and zero ERROR lines while two generated shaders were being
// rejected outright, and the lane could not say why it was rendering an
// empty translucent layer. A shader the driver refuses is never noise.
MGLOG_I("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
// MGLOG_E, unlatched. This was parked at MGLOG_I while the level ordering
// compiled E and W out of every INFO build: the Android retrace artifact
// carried 294 INFO lines and zero ERROR lines while two generated shaders
// were being rejected outright, and the lane could not say why it was
// rendering an empty translucent layer. A shader the driver refuses is
// never noise, and one line per refused shader is bounded by program count.
MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
"%u, driver log: %s",
stateProgramObject->GetExternalIndex(),
MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId,
@@ -4930,7 +5131,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// MGLOG_I for the same reason as the compile failure above: a program that
// links nothing no-ops every draw that uses it, and that has to be readable
// in an INFO-level artifact.
MGLOG_I("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s",
MGLOG_E("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s",
stateProgramObject->GetExternalIndex(), m_backendProgramId, log.data());
} else {
MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId);
@@ -5022,7 +5223,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_globalUboBackendBlockSize = static_cast<Int>(blockDataSize);
}
} else {
MGLOG_W("Program %u has frontend global UBO storage, but backend has no %s block.",
MGLOG_W_ONCE("Program %u has frontend global UBO storage, but backend has no %s block.",
stateProgramObject->GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
}
}
@@ -5098,13 +5299,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
if (!m_backendProgramUsable) {
// MGLOG_I, not MGLOG_W: at MOBILEGL_LOG_LEVEL_INFO - the level the shipped
// fordebug builds compile at - only I and F survive, and this is precisely the
// line those builds need. Every draw made with this program renders nothing and
// raises no GL error, so without it the only symptom is a framebuffer that kept
// its clear colour. The early return above keeps it to at most one line per
// program state change, not one per draw.
MGLOG_I("Backend program for GL program %u is unusable (a shader failed to transpile, "
// Every draw made with this program renders nothing and raises no GL error, so
// without this line the only symptom is a framebuffer that kept its clear
// colour. Latched: the early return above only dedupes CONSECUTIVE binds, so an
// app alternating a healthy and a broken program would otherwise log every
// single draw. Parked at MGLOG_I until the level ordering was fixed.
MGLOG_E_ONCE("Backend program for GL program %u is unusable (a shader failed to transpile, "
"compile or link); binding program 0 - draws with it will render nothing",
m_frontendProgramId);
}
@@ -5152,8 +5352,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendSamplerId == 0) {
MGLOG_E("Failed to generate sampler object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
MGLOG_E_ONCE("Failed to generate sampler object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated sampler object with ID: %u.", m_backendSamplerId);
}
@@ -5185,7 +5385,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateSamplerObject) {
MGLOG_E("State sampler object is null, cannot sync to backend.");
MGLOG_E_ONCE("State sampler object is null, cannot sync to backend.");
return;
}
@@ -5296,8 +5496,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendRBOId == 0) {
MGLOG_E("Failed to generate renderbuffer object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
MGLOG_E_ONCE("Failed to generate renderbuffer object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
}
}
@@ -5329,7 +5529,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateRBOObject) {
MGLOG_E("State RBO object is null, cannot sync to backend.");
MGLOG_E_ONCE("State RBO object is null, cannot sync to backend.");
return;
}
+62
View File
@@ -1122,6 +1122,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// stale as one built before a relink - while the sampler half, which really is
// re-issued per draw, needs nothing of the sort.
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
// resolve to are still the ones its ESSL was generated against.
//
// A fourth condition of the same family as the three above, and the only one that
// reads live state rather than a program-side counter, because that is where the
// dependency actually is. GLSL ES requires a format layout qualifier on every image
// where desktop GLSL lets a writeonly declaration omit one, and the only correct
// qualifier is whatever glBindImageTexture named - so a declaration with no format
// is compiled against the BINDING, and a rebind to a different format makes the
// built program wrong. Keyed on the units the program's own images address (cached
// at sync, since a unit can only move by glUniform1i, which bumps the image-unit
// version above and forces a re-sync anyway), so the cost on a program with no
// format-less image - which is all but a handful - is one empty-vector test.
//
// Deliberately NOT reached from glBindImageTexture: that entry point must never
// trigger a build (same constraint as glShaderStorageBlockBinding). It moves the
// state and this comparison notices at the next Prepare, which is also what makes
// an image first bound AFTER link work.
Bool ImageUnitFormatsStillMatch() const;
// The value ImageUnitFormatsStillMatch() compares against, recomputed from live
// image-unit state. 0 when the program has no format-less image uniform.
Uint64 ComputeImageUnitFormatSignature() const;
private:
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
@@ -1155,6 +1177,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
Uint32 m_syncedLinkVersion = ~0u;
Uint32 m_syncedImageUnitVersion = ~0u;
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
// that declares a format on all of its images, which is the overwhelming majority -
// and what keeps the per-draw comparison free for them.
Vector<Int> m_formatlessImageUnits;
Uint64 m_imageUnitFormatSignature = 0;
SamplerPassMemo m_samplerPassMemo;
};
@@ -1198,6 +1226,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
// already has costs nothing. 0 when nothing was ever rebound.
Uint64 ComputeShaderStorageBlockBindingSignature(
const MG_State::GLState::ProgramObject& stateProgramObject);
// Everything the image-format bake needs from one walk of a program's uniform
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
// on it.
struct ImageFormatBakeInputs {
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
// one is authoritative and is never overridden.
UnorderedMap<String, Uint> glFormatByUniformName;
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
// write into the emitted declaration instead. Disjoint from the map above by
// construction: a format is baked into the module or completed in the text, never
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
UnorderedMap<String, String> esslFormatQualifierByUniformName;
// Units those uniforms address, kept so the draw path can re-read their formats
// without walking the reflection again.
Vector<Int> units;
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
// image uniform, which is all but a handful.
Uint64 signature = 0;
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
// declaration carries one qualifier, so there is nothing correct to bake and they
// are dropped from the map above. Kept for diagnostics.
Vector<String> conflictedNames;
// Some format in play - declared or baked - is outside the GLSL ES core image
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
Bool needsExtendedImageFormats = false;
};
ImageFormatBakeInputs CollectImageFormatBakeInputs(
const MG_State::GLState::ProgramObject& stateProgramObject);
} // namespace PrgramImpl
namespace SamplerImpl {
+9 -9
View File
@@ -252,7 +252,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
g_resolvedTier =
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
&g_tierResolution);
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
}
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
@@ -267,7 +267,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint32 bit = 1u << static_cast<Uint32>(tier);
if (g_announcedTiers & bit) return;
g_announcedTiers |= bit;
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
}
// The tier this particular batch can actually take. A tier is demoted here when
@@ -490,7 +490,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
subDrawCount, indexSize);
if (!source) {
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
"buffer; skipping the batch",
i);
return false;
@@ -596,7 +596,7 @@ void main() {
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
if (shader == 0) {
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
return false;
}
const char* source = kFlattenComputeSource;
@@ -607,14 +607,14 @@ void main() {
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
g_GLESFuncs.glDeleteShader(shader);
return false;
}
const GLuint program = g_GLESFuncs.glCreateProgram();
if (program == 0) {
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
g_GLESFuncs.glDeleteShader(shader);
return false;
}
@@ -625,7 +625,7 @@ void main() {
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
g_GLESFuncs.glDeleteProgram(program);
return false;
}
@@ -635,7 +635,7 @@ void main() {
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
g_computeProgramFailed = false;
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
return true;
}
@@ -920,7 +920,7 @@ void main() {
feedBaseVertex);
}
if (!drawn) {
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
"the batch was dropped",
drawcount, mode, type);
}
+183 -67
View File
@@ -535,6 +535,92 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode;
}
String RequestExtendedImageFormats(String glslCode, Bool needed) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// GLSL ES core has thirteen image formats; GL has forty. SPIRV-Cross prints whatever
// format the OpTypeImage carries and asks for no extension for it, so an r8ui or
// rg16f image - declared as such, or baked from the bound one - reaches the driver as
// a format its core language does not know. GL_NV_image_formats is the only thing
// that adds them, and it has to be requested by name.
//
// The caller decides `needed`: it knows which formats are in play (from the uniform
// reflection and the image-unit bindings) and whether the driver advertises the
// extension at all - `#extension` on an unadvertised name is itself a hard error, so
// this must never be emitted speculatively.
static constexpr const char* kDirective = "#extension GL_NV_image_formats : require\n";
static constexpr const char* kExtName = "GL_NV_image_formats";
if (!needed || glslCode.find(kExtName) != String::npos) {
return glslCode;
}
// After the #version line, which must stay first. Everything else about the header is
// order-insensitive, and ForceSupporterOutput's scan for the LAST #extension
// directive still finds whichever one that is.
const SizeT versionPos = glslCode.find("#version");
if (versionPos == String::npos) {
return kDirective + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + kDirective;
}
glslCode.insert(lineEnd + 1, kDirective);
return glslCode;
}
String BakeImageFormatQualifiers(String glslCode,
const UnorderedMap<String, String>& esslFormatByUniformName) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (esslFormatByUniformName.empty() || glslCode.find("image") == String::npos) {
return glslCode;
}
// Same declaration shape RebindImageUniformsToFrontendUnits matches, and for the same
// reason: one line, one image uniform, the name in group 3.
static const std::regex imageDeclRegex(
R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)");
// Every image format spelling GLSL has, so a declaration that already carries one is
// recognised whatever it says - the caller's map is consulted only for declarations
// with NO format, never to override a written one.
static const std::regex existingFormatRegex(
R"(\b(rgba32f|rgba16f|rg32f|rg16f|r11f_g11f_b10f|r32f|r16f|rgba16|rgb10_a2|rg16|rg8|r16|r8|rgba16_snorm|rgba8_snorm|rg16_snorm|rg8_snorm|r16_snorm|r8_snorm|rgba32i|rgba16i|rgba8i|rg32i|rg16i|rg8i|r32i|r16i|r8i|rgba32ui|rgba16ui|rgba8ui|rgb10_a2ui|rg32ui|rg16ui|rg8ui|r32ui|r16ui|r8ui)\b)");
String result;
result.reserve(glslCode.size());
SizeT lineStart = 0;
while (lineStart <= glslCode.size()) {
const SizeT lineEnd = glslCode.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
std::smatch match;
if (std::regex_search(line, match, imageDeclRegex)) {
const String name = match[3].str();
const auto formatIt = esslFormatByUniformName.find(name);
const String layoutContents = match[2].matched ? match[2].str() : String();
if (formatIt != esslFormatByUniformName.end() && !formatIt->second.empty() &&
!std::regex_search(layoutContents, existingFormatRegex)) {
if (match[1].matched) {
const SizeT layoutOpen = line.find('(', match.position(1));
line.insert(layoutOpen + 1, formatIt->second + ", ");
} else {
line.insert(match.position(0), "layout(" + formatIt->second + ") ");
}
}
}
result += line;
if (lastLine) {
break;
}
result += '\n';
lineStart = lineEnd + 1;
}
return result;
}
String RemoveLayoutBinding(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -1090,7 +1176,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
for (GLenum err = g_GLESFuncs.glGetError(); err != GL_NO_ERROR; err = g_GLESFuncs.glGetError()) {
MGLOG_E("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
MGLOG_D("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
}
}
@@ -1510,88 +1596,71 @@ namespace MobileGL::MG_Backend::DirectGLES {
return (rowBytes + align - 1) / align * align;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
// Walks the client-side destination the PACK parameters describe and hands each row to
// `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client
// texels. Shared by the converting and the raw-word stores so both address the destination -
// and feed the bound pixel-pack buffer - identically.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
template <typename FillRow>
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("Readback conversion: pixel pack buffer is too small");
return true;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
return true;
}
}
}
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
const SizeT srcPixelBytes = 4 * srcComponentSize;
Vector<Uint8> convertedRow(dstRowBytes);
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
fillRow(slice, row, convertedRow.data());
if (packParams.SwapBytes) {
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
if (groupSize > 1) {
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
if (packParams.SwapBytes && swapGroupSize > 1) {
for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize);
}
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
}
}
if (pixelPackBufferObject) {
// WritebackFromBackend bumps change serials with no backend op; re-open
// the buffer draw-clean memos (once for the whole row loop).
@@ -1599,5 +1668,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return true;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type);
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
return StoreClientRows(dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels,
applyPackImageParams,
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
const SizeT flatRow = static_cast<SizeT>(slice) *
static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow =
wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, dstRow, static_cast<SizeT>(width), wideType,
mapping, type);
});
}
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams) {
PackedReadbackLayout packedLayout{};
if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) {
return false;
}
const SizeT srcRowBytes = static_cast<SizeT>(width) * 4;
return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels,
applyPackImageParams,
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
const SizeT flatRow = static_cast<SizeT>(slice) *
static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes);
});
}
} // namespace ReadbackImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+30
View File
@@ -115,6 +115,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams);
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
// False when `type` is not a 4-byte packed type.
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl
namespace PrgramImpl {
@@ -137,6 +147,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
String RetargetTextureBufferExtension(String glslCode,
MG_External::GLESCapabilities::TextureBufferTier tier);
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
// because only it knows which formats are in play AND whether the driver advertises the
// extension - requesting an unadvertised extension is itself a compile error, so this is
// never emitted speculatively. A no-op when not needed or already present.
String RequestExtendedImageFormats(String glslCode, Bool needed);
// Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
// format in, but SPIRV-Cross throws rather than printing the formats it calls
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
// stage. So those formats stay out of the module and are spelled here instead, on the
// emitted text, where nothing can refuse them.
//
// Declarations that already carry a format are left exactly as they are, whoever wrote
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
// stops being safe to edit by hand.
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
String RemoveLayoutBinding(const String& glslCode);
// Prefix of the writeonly half a read+write image uniform is split into (see
// SplitReadWriteImageUniforms); the suffix is the image's own name.
@@ -269,14 +269,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
drawBuffer->SyncPersistentMappedRange();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
return nullptr;
}
return drawBuffer->MappedData() + commandOffset;
}
if (!indirect) {
MGLOG_E("%s skipped: indirect pointer is null", label);
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
return nullptr;
}
@@ -398,7 +398,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stride = sizeof(DrawArraysIndirectCommand);
}
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawArraysIndirectCommand));
return;
}
@@ -446,20 +446,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stride = sizeof(DrawArraysIndirectCommand);
}
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawArraysIndirectCommand));
return;
}
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return;
}
parameterBuffer->SyncPersistentMappedRange();
if (parameterBuffer->MappedData() == nullptr) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
return;
}
@@ -513,7 +513,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
return;
}
@@ -1009,7 +1009,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// shift - the hardware divide was the hottest instruction of this loop.
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type);
MGLOG_E_ONCE("MultiDrawElements skipped: unsupported index type 0x%x", type);
return;
}
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
@@ -205,7 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// commands away. The device is gone on that path anyway - stay silent-safe
// rather than trade a lost device for a barrier into a closed buffer.
if (frame.hasCommandBufferRecorded) {
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
return false;
}
@@ -259,7 +259,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// is the correct price for a broken pipeline and is bounded by the draw itself being
// skipped.
if (pipeline == VK_NULL_HANDLE) {
MGLOG_I("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
// assembled and the driver refused is a broken invariant, not an expected failure,
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
// Log.h ordering fix made MGLOG_E live in INFO builds.
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
"programHash=0x%llx; not caching the failure",
static_cast<unsigned long long>(hash),
static_cast<unsigned long long>(payload.programHash));
@@ -471,9 +475,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
blend.attachmentCount = payload.colorAttachmentCount;
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
//
// The refusal below is what keeps the half-tessellated shape away from the driver when
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
// takes: the draw is skipped, nothing is memoised, and the process survives.
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
stagesWithPassthrough = *payload.stages;
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
effectiveStages = &stagesWithPassthrough;
}
{
VkShaderStageFlags stagesPresent = 0;
for (const auto& stageInfo : *effectiveStages) {
stagesPresent |= stageInfo.stage;
}
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
if (hasTessControl != hasTessEval) {
// Latched, and the latch is the point: a failed creation is deliberately never
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
// noise, not a diagnostic. One line names the program; the draws it explains are
// all the same draw.
static Bool s_warnedHalfTessellatedPipeline = false;
if (!s_warnedHalfTessellatedPipeline) {
s_warnedHalfTessellatedPipeline = true;
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
"patchControlPoints=%u. Its draws are skipped; logged once.",
hasTessEval ? "an evaluation" : "a control",
hasTessEval ? "control" : "evaluation",
static_cast<unsigned long long>(payload.programHash),
payload.patchControlPoints);
}
return VK_NULL_HANDLE;
}
}
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
gpi.pStages = payload.stages->data();
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
gpi.pStages = effectiveStages->data();
gpi.pVertexInputState = payload.vertexInputState;
gpi.pInputAssemblyState = &ia;
gpi.pTessellationState =
@@ -490,6 +541,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline pipeline = VK_NULL_HANDLE;
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
// does not apply: while this is reachable it should keep saying so on every draw.
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
if (result != VK_SUCCESS) {
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
VkResultToString(result),
@@ -522,8 +580,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
payload.vertexInputState->vertexAttributeDescriptionCount);
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
// investigation) is to name the modules. MGLOG_I, not _D/_E: this must survive in the
// INFO-level builds that CTS actually runs against.
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
// should-never-happen report and must survive in the INFO-level builds that CTS
// actually runs against, alongside the MGLOG_F lines above.
if (payload.stageSpirvDigests) {
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
const auto& digest = (*payload.stageSpirvDigests)[i];
@@ -71,6 +71,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool fragmentReplacesDepth = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
// The tessellation control stage this renderer synthesized for a program that has
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
// fixed-function pass-through; Vulkan has no such thing and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
// pipeline outright). Appended to `stages` at creation. A null module means the
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
// refusal it applies when `stages` itself is half-tessellated.
//
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
// of which ComputeHash already mixes in.
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
@@ -376,12 +376,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
spv_diagnostic diagnostic = nullptr;
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
if (result != SPV_SUCCESS) {
// MGLOG_I, not E: at the INFO compile level of the CI/test lanes that arm
// the validation switch, MGLOG_E is compiled out (Log.h orders
// DEBUG < WARN < ERROR < INFO) and the VUID would never reach a log. The
// latch is what a test harness asserts on.
// MGLOG_E, unlatched: reaching here already requires the validation switch to
// be armed, which bounds the volume, and each VUID names a different defect.
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
// compiled out of every INFO build.) The latch is what a test harness asserts on.
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
MGLOG_I(
MGLOG_E(
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
static_cast<Int>(shaderStage),
programExternalIndex,
@@ -1266,7 +1266,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (SizeT i = 1; i < group.offsets.size(); ++i) {
if (group.elementBytes == 0 ||
group.offsets[i] != group.offsets[i - 1] + group.elementBytes) {
MGLOG_I("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
MGLOG_D("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
"non-contiguous element set; the capture layout will differ from GL's",
key.second, key.first);
break;
@@ -1855,16 +1855,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// unification and the set->0 normalisation this function exists to do. A
// program with an image array plus any second descriptor got aliased
// bindings out of that, and a DEBUG build trapped on the same program.
// Which is also why the message below is MGLOG_I: MGLOG_E is compiled out
// of an INFO build, so a refusal that only said MGLOG_E said nothing at all
// in the builds that ship.
// The refusal below is MGLOG_E and per-program-compile, so it reports every
// program it declines. It spent time at MGLOG_I because the old level
// ordering compiled E out of the builds that ship.
const Bool arraySupportedForKind =
kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic ||
kind == ProgramFactory::DescriptorBindingKind::StorageBuffer ||
kind == ProgramFactory::DescriptorBindingKind::StorageImage ||
kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
if (binding->count != 1 && !arraySupportedForKind) {
MGLOG_I("ProgramFactory: descriptor arrays are unsupported for this descriptor "
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
"kind (name='%s' count=%u type=%d)",
binding->name ? binding->name : "<null>", binding->count,
static_cast<Int>(binding->descriptor_type));
@@ -2468,7 +2468,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// inert; a device whose binding cap is smaller than a shader's array is not a
// configuration MobileGL can serve at all. Needs a >maxBindings-element array to
// reach (256 on desktop, ~16 on mobile).
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
"this device can describe - declining the program",
kindLabel, uniformName.c_str(), binding, count, maxBindings);
outDeclined = true;
@@ -2476,7 +2476,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (baseLocation < 0 ||
!program.UniformLocationsAliasSameUniform(baseLocation, baseLocation + static_cast<Int>(count - 1u))) {
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
"reflection reserved fewer uniform locations for it (base=%d) - a multi-dimensional array "
"is the usual cause, and MobileGL declines it rather than resolve elements onto a "
"neighbouring uniform",
@@ -2713,7 +2713,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a Uint16 on the way, where 65536 would silently become 0.
const Uint32 storageArrayCount = std::max<Uint32>(1u, sampler->count);
if (storageArrayCount > m_maxBindings) {
MGLOG_I("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
MGLOG_D("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
"elements, past the %u this device can describe - declining the program",
uniformName.c_str(), binding, storageArrayCount, m_maxBindings);
entry.declinedDescriptors = true;
@@ -2736,7 +2736,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// so at a level that survives a release build, because dropping the binding
// leaves the shader reading a descriptor the layout never declared.
if (sampler->count > 1) {
MGLOG_I("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
MGLOG_E("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
"descriptor array with no frontend uniform location (a multi-dimensional array "
"of samplers or images is the known cause)",
uniformName.c_str(), binding, sampler->count);
@@ -3190,6 +3190,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#endif
ReflectVertexInputs(shaders, moduleSpirvs, entry);
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
ReflectLayout(program, moduleSpirvs, entry);
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
@@ -3200,7 +3201,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// "the layout and the shader disagree", so route it through that. Set AFTER ReflectLayout,
// which clears the flag.
if (!remapOk) {
MGLOG_I("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
MGLOG_E("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
"be remapped, so the layout does not describe what the shader reads",
program.GetExternalIndex());
entry.declinedDescriptors = true;
@@ -3247,4 +3248,235 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
}
ProgramFactory::~ProgramFactory() {
for (auto& entry : m_passthroughTessControlStages) {
if (entry.second.module != VK_NULL_HANDLE) {
vkDestroyShaderModule(m_device, entry.second.module, nullptr);
}
}
}
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) {
// The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no
// control shader: "the input patch is passed through unmodified", the output patch has
// as many vertices as the input one (PATCH_VERTICES), and the levels come from the
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
//
// Those two levels default to 1.0 and are baked here as literals because
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means
// making the levels a parameter of this source AND of the cache key in
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
// together here.
//
// gl_out carries gl_Position and nothing else on purpose. The evaluation stage that
// reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds
// exactly the built-ins that stage used, and its user-defined inputs (if any) come
// straight off the vertex stage's outputs - which a control stage sitting in between
// would leave unwritten. ReflectPassthroughTessControlNeed refuses those programs rather
// than let this write a partial interface.
//
// All four outer levels and both inner levels are written unconditionally: writing a
// level the evaluation stage's domain does not use is legal and ignored, and it saves
// this from having to know the domain.
String source = "#version 450 core\n";
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
// gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan
// matches built-in interface blocks by their whole shape, and the two obvious spellings
// are both wrong:
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
// zeroes (degenerate triangles, nothing rasterized), and
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members -
// it appends gl_CullDistance - where a linked vertex+evaluation program has three.
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a
// vertex+evaluation program through this same compiler and fails if the two shapes ever
// stop agreeing, rather than letting the mismatch show up as a black frame.
//
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
// feature, which this renderer does not enable, so a program whose evaluation stage reads
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
// this trades for not making every tessellated pipeline depend on an optional feature.
source += "in gl_PerVertex {\n"
" vec4 gl_Position;\n"
" float gl_PointSize;\n"
" float gl_ClipDistance[1];\n"
"} gl_in[gl_MaxPatchVertices];\n";
source += "out gl_PerVertex {\n"
" vec4 gl_Position;\n"
" float gl_PointSize;\n"
" float gl_ClipDistance[1];\n"
"} gl_out[];\n";
source += "void main() {\n";
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
source += " gl_TessLevelOuter[0] = 1.0;\n";
source += " gl_TessLevelOuter[1] = 1.0;\n";
source += " gl_TessLevelOuter[2] = 1.0;\n";
source += " gl_TessLevelOuter[3] = 1.0;\n";
source += " gl_TessLevelInner[0] = 1.0;\n";
source += " gl_TessLevelInner[1] = 1.0;\n";
source += "}\n";
return source;
}
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) {
// A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a
// generator that cannot compile from re-running glslang on every draw.
const auto cached = m_passthroughTessControlStages.find(patchVertices);
if (cached != m_passthroughTessControlStages.end()) {
return cached->second;
}
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
stage.module = VK_NULL_HANDLE;
stage.pName = "main";
using namespace MG_Util::ShaderTranspiler;
const String source = BuildPassthroughTessControlSource(patchVertices);
// Same compile configuration as every other stage of every other program: this runs on
// the GL thread (the draw path), so the live compile env is the right one, and flags=0
// is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds).
const SharedPtr<const CompileEnv>& env = GetCurrentCompileEnv();
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER,
.sourceStr = source,
.flags = 0,
.env = env.get()};
auto compiled = ShaderCompiler::CompileShader(shaderAttrib);
if (!compiled) {
MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for "
"patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s",
patchVertices, compiled.error().log.c_str());
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
ProgramAttrib programAttrib{};
programAttrib.shaders.push_back(compiled.value());
auto linked = ShaderCompiler::LinkProgram(programAttrib);
if (!linked) {
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER}, .program = *linked.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!binary || binary.value().empty() || binary.value().front().empty()) {
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
"for patchVertices=%u", patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
const Vector<Uint>& spirv = binary.value().front();
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
#else
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
}
#endif
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smci.codeSize = spirv.size() * sizeof(Uint);
smci.pCode = spirv.data();
VkShaderModule module = VK_NULL_HANDLE;
const VkResult result = vkCreateShaderModule(m_device, &smci, nullptr, &module);
if (result != VK_SUCCESS) {
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
stage.module = module;
MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u "
"(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
void ProgramFactory::ReflectPassthroughTessControlNeed(
const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const {
entry.needsPassthroughTessControl = false;
entry.passthroughTessControlEmulatable = false;
Bool hasTessEval = false;
Bool hasTessControl = false;
SizeT tessEvalModuleIndex = 0;
for (SizeT i = 0; i < shaders.size(); ++i) {
if (!shaders[i]) continue;
const auto stage = shaders[i]->GetShaderStage();
if (stage == ShaderStage::TessControl) hasTessControl = true;
if (stage == ShaderStage::TessEval) {
hasTessEval = true;
tessEvalModuleIndex = i;
}
}
if (!hasTessEval || hasTessControl) return;
entry.needsPassthroughTessControl = true;
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
const auto& module = spirv[tessEvalModuleIndex];
SpvReflectShaderModule reflectModule{};
const SpvReflectResult createResult =
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
MGLOG_E("ProgramFactory::ReflectPassthroughTessControlNeed: reflection failed (result=%d); the "
"evaluation stage's inputs are unknown, so the pass-through is not offered",
static_cast<Int>(createResult));
return;
}
uint32_t inputCount = 0;
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && inputCount > 0) {
reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, inputs.data());
}
if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) {
spvReflectDestroyShaderModule(&reflectModule);
return;
}
// The question is only ever "does this stage read anything a control stage would have to
// forward", and the answer is: does it have a LOCATION. A located input is a user-defined
// varying (or a per-patch input), which the vertex stage writes today and would stop
// reaching once a control stage sits in between - the pass-through carries gl_Position and
// nothing else, so such a program is declined instead of being handed undefined values.
// Everything without a location is a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn,
// gl_PrimitiveID, gl_TessLevel*, all either forwarded or generated for the evaluation
// stage by the tessellator itself.
//
// This deliberately does NOT judge on SpvReflectInterfaceVariable::built_in. gl_in is an
// array of interface blocks, and for those SPIRV-Reflect reports built_in == -1 on the
// block AND leaves every member's built_in at 0 - which is SpvBuiltInPosition, so a
// member walk reads "Position, Position, Position" for a {Position, PointSize,
// ClipDistance} block and would accept anything on the strength of parse garbage. The
// location, by contrast, is decorated on the OpVariable and is what SPIRV-Reflect reads
// straight through.
constexpr Uint32 kNoLocation = 0xFFFFFFFFu;
Bool emulatable = true;
for (auto* input : inputs) {
if (input == nullptr) continue;
if (input->location == kNoLocation) continue;
MGLOG_E("ProgramFactory: a tessellation evaluation stage with no control stage reads the "
"user-defined input '%s' at location=%u; a synthesized control stage cannot forward it, so "
"this program's draws are declined rather than fed an undefined varying",
input->name != nullptr ? input->name : "<null>", input->location);
emulatable = false;
break;
}
spvReflectDestroyShaderModule(&reflectModule);
entry.passthroughTessControlEmulatable = emulatable;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -151,6 +151,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
// declared, so both variants answer the same and the draw path can ask either.
Bool readsBaseVertexBuiltin = false;
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
// tessellation stages are present or neither
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are
// built-ins. A user-defined varying would arrive at the evaluation stage
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
// silently wrong pixels rather than a crash - so those programs are declined
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
// skipped). See ReflectPassthroughTessControlNeed.
Bool passthroughTessControlEmulatable = false;
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
// entry pointer re-stamps use through a const reference (StampProgramUse).
@@ -202,6 +218,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
@@ -216,6 +234,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.lastUsedFrame = 0;
}
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
@@ -256,6 +276,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
@@ -270,6 +292,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.lastUsedFrame = 0;
return *this;
}
@@ -321,7 +345,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
VkProgramObject::s_device = device;
}
~ProgramFactory() = default;
// Destroys the pass-through tessellation control modules. Runs while the device is
// still alive for the same reason ~VkProgramObject's does: this factory outlives
// nothing that owns the device.
~ProgramFactory();
ProgramFactory(const ProgramFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
@@ -374,6 +401,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// this builtin?
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
// program that has an evaluation stage and no control stage, for an input patch of
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
// the cache below is a rehashing map, so a pointer into it would not survive the next
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
// the caller then has no control stage to inject, and CreatePipeline refuses the
// pipeline rather than handing the driver a half-tessellated one.
//
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
// which is draw state, not link state - the CTS case that motivated this links at the
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
// so the module a pipeline was built with is part of that pipeline's identity.
// Compiling is bounded by the number of distinct patch sizes a program draws with
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
// happens for the rare program that has no control stage at all.
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
// Source of the module above. Exposed for tests: the generated GLSL is the whole
// contract with the evaluation stage, so it is worth pinning independently of a device.
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
private:
struct ProgramLookupCache {
const MG_State::GLState::ProgramObject* program = nullptr;
@@ -391,6 +439,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkProgramObject& entry) const;
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
// records what the evaluation stage's input interface is made of.
void ReflectPassthroughTessControlNeed(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
VkDevice m_device = VK_NULL_HANDLE;
Uint32 m_maxBindings = 0;
@@ -411,6 +465,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
Uint64 m_cacheStructureEpoch = 1;
IEvictionObserver* m_evictionObserver = nullptr;
// Pass-through tessellation control stages by input patch size. Never evicted: at most
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
// ever built from one keeps referencing its module. A failed build is cached as
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
}
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
for (const auto& pm : swapchainCapabilities.presentModes) {
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
}
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPool initialPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(m_setsPerFrame, initialPool)) {
MGLOG_E("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
frameIndex);
Shutdown();
return false;
@@ -345,13 +345,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fallbackHolder = GetFallbackTexture(preferredTarget);
texture = fallbackHolder.get();
if (texture == nullptr) {
MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
"location=%d unit=%d target=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
static_cast<Int>(preferredTarget));
return false;
}
MGLOG_W(
MGLOG_W_ONCE(
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
static_cast<Int>(preferredTarget));
@@ -360,7 +360,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MG_State::GLState::SamplerObject* samplerToUse =
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
if (samplerToUse == nullptr) {
MGLOG_E(
MGLOG_E_ONCE(
"ResolveSamplerDescriptor: sampler binding %u ('%s') has no sampler object (textureId=%d location=%d unit=%d)",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), location,
unit);
@@ -368,7 +368,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkTextureManager::TextureResource* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
if (resource == nullptr) {
MGLOG_E(
MGLOG_E_ONCE(
"ResolveSamplerDescriptor: sampler binding %u ('%s') failed to create/sync texture resource (textureId=%d target=%d location=%d unit=%d)",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(texture->GetTarget()), location, unit);
@@ -380,7 +380,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Int attachmentLevel = 0;
if (drawFbo &&
FindFramebufferAttachmentForTexture(*drawFbo, *texture, attachmentType, attachmentLevel)) {
MGLOG_W("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
MGLOG_W_ONCE("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
"for sampling at binding=%u, but is also attached to drawFbo=%u as %s (level=%d, "
"trackedLayout=%d)",
texture->GetExternalIndex(), binding, drawFbo->GetExternalIndex(),
@@ -390,7 +390,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool readyForSampling = m_textureManager->TransitionTextureForSampling(commandBuffer, *texture);
if (!readyForSampling) {
MGLOG_E("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
texture->GetExternalIndex(), binding);
return false;
}
@@ -432,7 +432,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
MGLOG_E_ONCE("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
@@ -445,7 +445,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
? resource->sampledView
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
if (sampledImageView == VK_NULL_HANDLE) {
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
@@ -671,14 +671,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<MG_State::GLState::ITextureObject> texture;
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
programObj.samplerNameByBinding[binding].c_str());
return false;
}
if (texture->GetStorageType() != TextureStorageType::Buffer ||
texture->GetTarget() != TextureTarget::TextureBuffer) {
MGLOG_E(
MGLOG_E_ONCE(
"ResolveTexelBufferDescriptor: binding %u ('%s') expected texture buffer, got textureId=%u target=%d storage=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(texture->GetTarget()), static_cast<Int>(texture->GetStorageType()));
@@ -688,14 +688,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
binding, programObj.samplerNameByBinding[binding].c_str());
return false;
}
BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
MGLOG_E("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
return false;
}
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto internalFormat = textureBuffer->GetFormat();
const VkFormat vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
if (vkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
static_cast<Int>(internalFormat));
return false;
}
@@ -719,7 +719,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewRange = (viewRange / texelSize) * texelSize;
}
if (viewRange == 0) {
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
return false;
}
@@ -733,7 +733,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferView bufferView = VK_NULL_HANDLE;
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
MGLOG_E("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
return false;
}
@@ -769,13 +769,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int location = programObj.samplerUniformLocationByBinding[binding];
if (location < 0) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
programObj.samplerNameByBinding[binding].c_str());
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
binding);
return false;
}
@@ -783,13 +783,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
const auto& texture = imageBinding.Texture;
if (texture == nullptr) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
binding);
return false;
}
if (texture->GetStorageType() != TextureStorageType::Buffer ||
texture->GetTarget() != TextureTarget::TextureBuffer) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
"unit %d, got textureId=%u target=%d storage=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), imageUnit,
texture->GetExternalIndex(), static_cast<Int>(texture->GetTarget()),
@@ -800,7 +800,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
imageUnit);
return false;
}
@@ -819,7 +819,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) ||
!slice.IsValid()) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
return false;
}
@@ -842,7 +842,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vkFormat = resourceFormat;
}
if (vkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
static_cast<Int>(internalFormat), imageBinding.Format);
return false;
}
@@ -862,7 +862,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewRange = (viewRange / texelSize) * texelSize;
}
if (viewRange == 0) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
texture->GetExternalIndex());
return false;
}
@@ -877,7 +877,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferView bufferView = VK_NULL_HANDLE;
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
return false;
}
@@ -914,7 +914,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
const auto& bufferObject = bindingPoint.GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
return false;
}
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str());
return false;
}
@@ -943,7 +943,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rangeEnd = bufferSize;
}
if (rangeEnd <= rangeStart) {
MGLOG_E("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
programObj.storageBlockNameByBinding[binding].c_str());
return false;
}
@@ -967,7 +967,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
if (baseLocation < 0) {
MGLOG_E("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
MGLOG_E_ONCE("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
return false;
}
// Per ELEMENT, and this is where an image array differs from a storage-block array: GL
@@ -979,26 +979,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// uniform.
const Int location = baseLocation + static_cast<Int>(element);
if (!program.UniformLocationsAliasSameUniform(baseLocation, location)) {
MGLOG_E("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
MGLOG_E_ONCE("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
binding, element);
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
imageUnit, binding);
return false;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
if (imageBinding.Texture == nullptr) {
MGLOG_E("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
return false;
}
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
if (!ready) {
MGLOG_E("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
imageBinding.Texture->GetExternalIndex(), imageUnit);
return false;
}
@@ -1018,7 +1018,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkFormat viewFormat = ResolveStorageImageViewFormat(
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
if (viewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
useBindingFormat ? "true" : "false");
@@ -1027,7 +1027,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
if (view == VK_NULL_HANDLE) {
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
@@ -1048,7 +1048,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Report that there is no fallback and let the caller decline the draw - aborting the
// process over an unbound sampler is never the right answer.
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
MGLOG_E("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
static_cast<Int>(target));
return nullptr;
}
@@ -1224,13 +1224,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
return false;
}
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
if (baseLocation < 0) {
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no image uniform location", binding);
return false;
}
// Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one
@@ -1242,20 +1242,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 element = 0; element < descriptorCount; ++element) {
const Int location = ResolveDescriptorElementLocation(program, baseLocation, element);
if (location < 0) {
MGLOG_E("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
binding, element);
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
imageUnit, binding, element);
return false;
}
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
if (texture == nullptr) {
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
imageUnit, binding, element);
return false;
}
@@ -1406,7 +1406,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint64 descriptorCount64 =
static_cast<Uint64>(maxSets) * static_cast<Uint64>(std::min(m_maxBindings, kEstimatedBindingsPerSet));
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
return false;
}
@@ -1438,7 +1438,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &outPool);
if (result != VK_SUCCESS) {
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
result);
return false;
}
@@ -1457,7 +1457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPool grownPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(grownMaxSets, grownPool)) {
MGLOG_E("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
currentMaxSets, grownMaxSets);
return false;
}
@@ -1516,7 +1516,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
if (!GrowFrameDescriptorPool(frame, frameIndex)) {
MGLOG_E("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
MGLOG_E_ONCE("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
return allocResult;
}
allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
@@ -1647,7 +1647,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
auto& frame = m_frames[frameIndex];
if (frame.descriptorPools.empty()) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
return false;
}
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
@@ -1784,7 +1784,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferView bufferView = VK_NULL_HANDLE;
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
bufferView == VK_NULL_HANDLE) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: texture buffer binding %u has no valid descriptor",
binding);
return false;
@@ -1803,7 +1803,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferView bufferView = VK_NULL_HANDLE;
if (!ResolveStorageTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
bufferView == VK_NULL_HANDLE) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u "
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u "
"has no valid descriptor",
binding);
return false;
@@ -1823,7 +1823,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 element = 0; element < descriptorCount; ++element) {
VkDescriptorBufferInfo bufferInfo{};
if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u "
"element %u has no valid descriptor",
binding, element);
@@ -1850,7 +1850,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorImageInfo imageInfo{};
if (!ResolveStorageImageDescriptor(commandBuffer, program, programObj, binding, element,
imageInfo)) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage image binding %u "
"element %u has no valid descriptor",
binding, element);
@@ -1892,14 +1892,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo, samplerDescriptorsUnchangedHint);
}
if (!hasImage) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
"has no valid texture descriptor",
binding, element);
return false;
}
if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
"has null sampler or imageView",
binding, element);
@@ -1973,7 +1973,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else {
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
allocResult);
return false;
}
@@ -110,7 +110,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkFormat sourceVkFormat =
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
@@ -125,7 +125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
vkFormat = fallbackFormat;
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
MGLOG_W("Vertex attribute location=%u format=%d lacks "
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
"(type=%s size=%d normalized=%s integer=%s)",
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
@@ -135,7 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (conversion == VertexStreamConversion::None) {
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
location, static_cast<Int>(sourceVkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) {
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
"enabled but cannot be sized",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
unsupportedAttribMask |= (1u << location);
@@ -175,7 +175,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format.
conversion = VertexStreamConversion::Repack;
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
location, attr.Offset, sourceStride, requiredAlignment);
}
@@ -302,7 +302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.requiredFlags = requiredFlags,
});
if (!created || resource.buffer.Map() == nullptr) {
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
static_cast<unsigned long long>(size));
resource.buffer.Destroy();
resource.storageSize = 0;
@@ -324,7 +324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
resource.pendingFullUpload = true;
return false;
}
@@ -409,7 +409,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
resource->pendingFullUpload = true;
}
}
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
resource->pendingFullUpload = true;
}
return;
@@ -471,7 +471,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
resource->pendingFullUpload = true;
}
return;
@@ -563,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
return false;
}
@@ -585,7 +585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
resource->buffer.Destroy();
resource->storageSize = 0;
resource->usageFlags = 0;
@@ -620,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
return false;
}
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result =
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
m_allocator = nullptr;
m_buffer = VK_NULL_HANDLE;
m_allocation = nullptr;
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
m_mappedData = nullptr;
return nullptr;
}
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool wasMapped = IsMapped();
void* mapped = wasMapped ? m_mappedData : Map();
if (mapped == nullptr) {
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
return false;
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
if (!wasMapped) {
Unmap();
}
@@ -170,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
return false;
}
return true;
@@ -123,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (!attachment.IsComplete()) {
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
drawBufferIndex,
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
fbo.GetExternalIndex());
@@ -132,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* texture = attachment.GetTexture().get();
if (texture == nullptr) {
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
drawBufferIndex,
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
fbo.GetExternalIndex());
@@ -311,7 +311,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
renderbuffer->GetSamples(),
renderbuffer->GetExternalIndex());
return nullptr;
@@ -457,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
&imageFormatProperties);
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
static_cast<Int>(format),
static_cast<Int>(sampleCount),
renderbuffer->GetExternalIndex());
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto& renderbuffer = rbAtt.GetRenderbuffer();
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
continue;
@@ -1105,7 +1105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_W("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
"using LOAD_OP_DONT_CARE",
texture->GetExternalIndex());
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
@@ -1161,7 +1161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
if (hasDistinctDepthAndStencilAttachments) {
MGLOG_E("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
fbo.GetExternalIndex());
}
if (selectedDepthStencilAttachment != nullptr) {
@@ -1223,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
MGLOG_W("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
"and partial/no clear; using DONT_CARE for uncleared aspects",
depthAttachmentId);
}
@@ -975,13 +975,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return resource->sampledView;
}
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
MGLOG_E_ONCE("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
@@ -1001,7 +1001,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
MGLOG_E_ONCE("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
"for textureId=%d (available=0x%x)",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
@@ -1015,7 +1015,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
MGLOG_E_ONCE("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
@@ -1043,14 +1043,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
format = resource->format;
}
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
MGLOG_E_ONCE("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if (format != resource->format &&
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
@@ -1070,7 +1070,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewType = VK_IMAGE_VIEW_TYPE_2D;
break;
case VK_IMAGE_VIEW_TYPE_3D:
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
MGLOG_E_ONCE("%s: non-layered 3D storage views are unsupported for textureId=%d",
__func__, texture.GetExternalIndex());
return VK_NULL_HANDLE;
default:
@@ -1079,7 +1079,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (viewType != resource->viewType) {
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
MGLOG_E_ONCE("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
return VK_NULL_HANDLE;
}
@@ -1114,7 +1114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
MGLOG_E_ONCE("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
"(available=0x%x)",
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
@@ -1125,7 +1125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
VK_IMAGE_USAGE_STORAGE_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
MGLOG_E_ONCE("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
@@ -1223,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_W("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
MGLOG_W_ONCE("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
texture.GetExternalIndex());
}
@@ -1574,7 +1574,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
// assertion would take the whole process down instead.
MGLOG_W("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
MGLOG_W_ONCE("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
"mipLevels=%u vkViewType=%d",
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
@@ -1803,7 +1803,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Losing reinterpreted views only degrades the formatless-image feature for
// this texture; failing creation would lose the texture entirely, so retry
// as a plain immutable-format image.
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
MGLOG_W_ONCE("%s: mutable image format=%d is unsupported for textureId=%d; creating "
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
"will be unavailable for it)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
@@ -1821,7 +1821,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
// format; failing creation would lose the texture entirely. Remembered so later syncs
// neither reprobe nor flag-mismatch against this image and recreate it.
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
"unavailable for it)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
@@ -1853,7 +1853,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult createImageResult =
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
if (createImageResult != VK_SUCCESS) {
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
// E_ONCE, not F: the comment above says it - this is a soft failure the caller
// recovers from, and it re-fires on every sync of every texture the driver refuses.
MGLOG_E_ONCE("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
"mips=%u samples=%d format=%d",
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
@@ -2426,7 +2428,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
if (!srcIsD24S8 && !srcIsD32FS8) {
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
MGLOG_E_ONCE("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
for (const auto& item : uploadItems) {
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
@@ -15,7 +15,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
MGLOG_W_ONCE("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
return false;
}
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& poolState : m_pools) {
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
if (result != VK_SUCCESS) {
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
Shutdown();
return false;
}
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& poolState = m_pools[frameIndex];
if (poolState.cursor >= m_slotsPerPool) {
if (!poolState.exhaustionWarned) {
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
MGLOG_W_ONCE("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
"this frame fall back to the frontend path",
frameIndex, m_slotsPerPool);
poolState.exhaustionWarned = true;
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (result != VK_SUCCESS && result != VK_NOT_READY) {
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
return false;
}
if (resultWithAvailability[1] == 0) {
File diff suppressed because it is too large Load Diff
@@ -74,6 +74,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
// call: appending its format to the base format while its arguments precede the base
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
//
// MGLOG_F and deliberately NOT latched. VK_VERIFY is the invariant-check macro: a Vulkan call
// MobileGL believes it has already made legal came back non-success, which is a
// should-never-happen state, not an expected failure mode a user hits. Those fast-fail loudly
// and keep saying so - the log-quietness rules that latch W/E cover expected failures (driver
// capability gaps, app misuse), not broken internal invariants. MOBILEGL_ASSERT below traps in
// a DEBUG build; MGLOG_F is what makes the same condition visible in an INFO test run, where
// the assert is compiled out by contract.
//
// A soft, recoverable failure must therefore NOT be routed through VK_VERIFY. Check the
// VkResult directly and report it with MGLOG_E_ONCE - see VkTextureManager::SyncTextureResource,
// where a driver legitimately refuses an image the format pre-check accepted.
#define VK_VERIFY(expr, ...) \
do { \
VkResult _vk_verify_result = (expr); \
+11 -11
View File
@@ -21,7 +21,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLStateContext* GetState() {
if (!MG_State::pEGLContext) {
MGLOG_E("pEGLContext is null. MG_State may not be initialized.");
MGLOG_E_ONCE("pEGLContext is null. MG_State may not be initialized.");
}
return MG_State::pEGLContext.get();
}
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE;
}
@@ -172,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
state->SetError(EGL_BAD_SURFACE);
return EGL_FALSE;
}
@@ -211,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
@@ -265,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (releaseCurrentRequest) {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
MGLOG_E_ONCE("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
state->SetError(EGL_BAD_ACCESS);
return EGL_FALSE;
@@ -277,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
return EGL_FALSE;
}
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
dpy, draw, read, ctx);
state->SetError(EGL_BAD_ACCESS);
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE;
}
@@ -726,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
width = std::max<EGLint>(width, 1);
@@ -764,7 +764,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name);
if (!proc) {
MGLOG_W("Failed to get function: %s", name);
MGLOG_D("Failed to get function: %s", name);
return nullptr;
}
return (__eglMustCastToProperFunctionPointerType)proc;
+1 -1
View File
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// quietly writing a differently-sized pattern.
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
if (sourceSize != elementSize) {
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
MGLOG_W_ONCE("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
"converting between them is not implemented",
GetBufferOpName(op), sourceSize, internalformat, elementSize);
}
@@ -25,12 +25,12 @@
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
return (type)1; \
}
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
}
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
@@ -2585,7 +2585,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLui
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE;
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
@@ -3181,5 +3181,5 @@ MOBILEGL_GL_API void glVertexAttribDivisorARB(GLuint index, GLuint divisor) {
}
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
}
@@ -547,7 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
if (!blitNamedFramebuffer) {
MGLOG_E("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
return;
}
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
@@ -558,7 +558,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
if (!clearNamedFramebufferfv) {
MGLOG_E("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
return;
}
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
@@ -568,7 +568,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
if (!clearNamedFramebufferfi) {
MGLOG_E("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
return;
}
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
@@ -578,7 +578,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLint* value) {
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
if (!clearNamedFramebufferiv) {
MGLOG_E("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
return;
}
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
@@ -588,7 +588,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLuint* value) {
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
if (!clearNamedFramebufferuiv) {
MGLOG_E("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
return;
}
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
+4 -4
View File
@@ -383,7 +383,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
if (!activeBackendObject) {
MGLOG_E("activeBackendObject is not initialized!");
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return (GLubyte*)"Unknown";
}
@@ -442,7 +442,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MGLOG_E("activeBackendObject is not initialized!");
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return (GLubyte*)"Unknown";
}
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
@@ -1954,7 +1954,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MGLOG_E("activeBackendObject is not initialized!");
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return;
}
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
@@ -2248,7 +2248,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
break;
default:
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
MGLOG_D("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
std::format("Invalid enum: 0x{:X}", pname)));
@@ -908,7 +908,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT span = UniformStorageSpanInBytes(ttype, size);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + span > programObject->GetUBOSize()) {
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
program, location);
return;
}
@@ -962,7 +962,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT span = UniformStorageSpanInBytes(ttype, size);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + span > programObject->GetUBOSize()) {
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
program, location);
return;
}
@@ -1062,7 +1062,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!initialized) {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MGLOG_E("activeBackendObject is not initialized!");
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return;
}
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
@@ -1152,7 +1152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
SizeT writeSize = ItemCount * sizeof(T);
if (size < writeSize) {
// Metadata bug: degrade to a clamped copy instead of killing the process.
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
MGLOG_E_ONCE("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
"bytes; clamping",
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
writeSize = size;
@@ -1173,7 +1173,7 @@ namespace MobileGL::MG_Impl::GLImpl {
offset + byteOffsetInsideUniform + writeSize > uboSize) {
// Should not happen: linking gives every settable uniform backing
// storage. Log and drop the write instead of faulting.
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
MGLOG_E_ONCE("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
"uboSize=%zu); dropping write",
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
writeSize, uboSize);
@@ -1807,7 +1807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
}
default:
MGLOG_E("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
MGLOG_D("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
+150 -19
View File
@@ -621,7 +621,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// the process down, which is never an acceptable answer to a query - see the same reasoning
// above for the compressed-format path.
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
MGLOG_I("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
"storage; recording GL_INVALID_OPERATION instead of terminating",
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError(
@@ -870,7 +870,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (readBytesPerTexel != bytesPerTexel) {
MGLOG_I("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
MGLOG_W_ONCE("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
bytesPerTexel, readBytesPerTexel);
return false;
}
@@ -1490,7 +1490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions");
MGLOG_E_ONCE("TexSubImage3D_State: Specified region exceeds texture level dimensions");
free(processedPixels);
return;
}
@@ -1599,7 +1599,7 @@ namespace MobileGL::MG_Impl::GLImpl {
{width, height, 1}, false, inputSize);
if (!processedPixels || inputSize == 0) {
MGLOG_E("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
MGLOG_E_ONCE("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
height);
if (processedPixels) free(processedPixels);
return;
@@ -1613,7 +1613,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
yoffset + height > static_cast<GLsizei>(texelSize.y())) {
MGLOG_E("TexSubImage2D_State: Specified region exceeds texture dimensions");
MGLOG_E_ONCE("TexSubImage2D_State: Specified region exceeds texture dimensions");
free(processedPixels);
return;
}
@@ -2164,7 +2164,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (processedPixels && imageSize > 0) {
if (imageSize != internalBytes) {
MGLOG_W("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
MGLOG_W_ONCE("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
"This may indicate an alignment or processing issue.",
__func__, imageSize, internalBytes);
}
@@ -2310,7 +2310,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (processedPixels && imageSize > 0) {
if (imageSize != internalBytes) {
MGLOG_W("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
MGLOG_W_ONCE("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
"This may indicate an alignment or processing issue.",
imageSize, internalBytes);
}
@@ -3382,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl {
dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
namespace {
// The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE
// enums and every PROXY enum all convert to a TextureTarget this frontend recognises,
// so ValidateTextureTarget lets them through; here they are INVALID_ENUM.
Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) {
switch (target) {
case GL_RENDERBUFFER:
case GL_TEXTURE_1D:
case GL_TEXTURE_1D_ARRAY:
case GL_TEXTURE_2D:
case GL_TEXTURE_2D_ARRAY:
case GL_TEXTURE_2D_MULTISAMPLE:
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
case GL_TEXTURE_3D:
case GL_TEXTURE_CUBE_MAP:
case GL_TEXTURE_CUBE_MAP_ARRAY:
case GL_TEXTURE_RECTANGLE:
return true;
default:
break;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
std::format("{} is not a target glCopyImageSubData accepts as the {}.",
MG_Util::ConvertGLEnumToString(target), endpointName)));
return false;
}
IntVec3 GetCopyImageLevelSize(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level) {
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (!mipmapTexture) return textureObject->GetBaseSize();
return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
}
// glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2
// spells the failure INVALID_VALUE - "if either name does not correspond to a valid
// object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for
// the ~30 entry points that reach it through a BOUND object (where the name was never
// in question and the fault is the binding), so this is a local rule rather than a
// change to the helper.
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* endpointName) {
if (textureObject) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
std::format("The {} name does not correspond to an existing image object.", endpointName)));
return false;
}
// Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that
// does not match the object INVALID_ENUM, where the shared uniformity helper records
// INVALID_OPERATION for the upload paths that share it.
Bool ValidateCopyImageTargetMatchesObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureTarget target, const char* endpointName) {
if (!textureObject || textureObject->GetTarget() == target) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
std::format("The {} target {} does not match the target the object was created with ({}).",
endpointName, MG_Util::ConvertTextureTargetToString(target),
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
return false;
}
} // namespace
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) {
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
return false;
}
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
@@ -3396,14 +3468,30 @@ namespace MobileGL::MG_Impl::GLImpl {
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
return false;
}
if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) ||
!TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) {
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
// 18.3.2 does not accept them here - only the eleven whole-image targets do.
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
return false;
}
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
return false;
}
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
!TextureImpl::ValidateTextureLevelNumber(dstLevel)) {
return false;
}
// ValidateTextureLevelNumber only bounds the index by GL_MAX_TEXTURE_SIZE; it cannot
// see that this particular texture stops at level 0. Both backends turn <level> into an
// image subresource with no further checking (DirectVulkan builds a VkImageCopy from it,
// DirectGLES forwards it to the ES copy), so a level the texture never had reached the
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
return false;
}
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -3414,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl {
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
return false;
}
if (!TextureImpl::ValidateBaseInternalFormatMatch(srcTexture->GetFormat(), dstTexture->GetFormat())) {
// A multisample image can only be copied to one with the same sample count, and a
// single-sample image reports zero - so this one comparison is also what rejects
// copying between a multisample target and a non-multisample one.
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("The two images have different sample counts ({} vs. {}).",
srcTexture->GetSamples(), dstTexture->GetSamples())));
return false;
}
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
// and no defined storage to copy into.
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
srcTexture->IsComplete(), dstTexture->IsComplete())));
return false;
}
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
return false;
}
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
srcLevelSize.x(), srcLevelSize.y(), "source") ||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
return false;
}
return true;
@@ -3643,10 +3768,11 @@ namespace MobileGL::MG_Impl::GLImpl {
// an application has every right to expect from it. Before this existed the call answered
// GL_INVALID_ENUM, which was wrong but at least visible; a silent success that leaves the
// sampled texels untouched is the kind of thing that costs a day to find from the other
// end. MGLOG_I, not _W: warnings are compiled out at the level everything ships at.
// end. MGLOG_W is the right level and now survives at INFO; it sat at MGLOG_I only
// while the Log.h ordering compiled warnings out of the builds that ship.
static std::atomic<Bool> announcedNoCodec{false};
if (!announcedNoCodec.exchange(true)) {
MGLOG_I("%s: the compressed blocks are stored verbatim and returned by "
MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
"glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
"reach the texels this level SAMPLES as. Upload through glTexSubImage2D for "
"that.",
@@ -5588,10 +5714,15 @@ namespace MobileGL::MG_Impl::GLImpl {
void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
auto srcTexture = GetTextureObjectByName(srcName, __func__);
auto dstTexture = GetTextureObjectByName(dstName, __func__);
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel,
srcWidth, srcHeight, srcDepth)) {
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
// SharedPtr, and let the validator record the error this entry point owes.
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
MG_State::pGLContext->GetTextureObject(srcName);
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
MG_State::pGLContext->GetTextureObject(dstName);
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
return;
}
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
+130 -12
View File
@@ -15,6 +15,7 @@
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Metrics/TextureMetrics.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTextureTarget(TextureTarget target) {
@@ -353,6 +354,63 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
const char* caller) {
// A null object is somebody else's error to report - ValidateTextureObject runs
// first at every call site and has already recorded it.
if (!textureObject) return false;
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (mipmapTexture == nullptr) {
// The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is
// not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target
// set). Declining here is not the error code the spec asks for - that would be
// INVALID_ENUM from a target check this validator is not - but it does keep a
// texture with no image levels whatsoever from reaching a backend that would
// dereference a backend texture it never created.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture has no mipmap levels to address."));
return false;
}
// What this number is, exactly, because two other things are almost it and neither is
// safe to assume: it is the number of level SLOTS the shadow has allocated - holes
// included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap.
// For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union.
//
// The guarantee that matters is one-sided: this count is always >= the level count the
// backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level
// with a non-positive extent, so it can only be shorter). That is the safe direction -
// no copy to a level the texture genuinely has is ever rejected here. It is NOT an
// exact match, so the backends keep their own range guard for the band in between: a
// chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and
// declined by the backend, which is a silent no-op rather than a copy. That band is a
// backend storage limitation, not a validation one - rejecting it here with
// INVALID_VALUE would be refusing a copy the spec permits.
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
if (levelCount == 0) {
// No image has ever been defined on this texture, so the fault is the texture,
// not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a
// copy names is an incomplete texture.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture has no image defined at any level."));
return false;
}
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture level does not exist in this texture."));
return false;
}
return true;
}
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
if (!textureObject) {
MG_State::pGLContext->RecordError(
@@ -458,26 +516,86 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
}
} // namespace
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
if (unsizedFormat1 != unsizedFormat2) {
// The 3-argument GenericErrorInfo constructor used to be spelled as a single
// std::format() call whose format string was the component name, so every
// diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then
// hand over component/function/message separately.
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
CopyImageTexelBlock block{};
if (compressedFormat != GL_NONE) {
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
if (info.blockByteSize != 0) {
block.byteSize = info.blockByteSize;
block.blockWidth = info.blockWidth;
block.blockHeight = info.blockHeight;
block.compressed = true;
return block;
}
}
// The size MobileGL actually stores a texel of this format in, which for every format GL
// gives a required size is that required size. The handful of legacy formats GL leaves
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
// copy between them would in fact preserve the bytes.
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
return block;
}
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
const CopyImageTexelBlock& dstBlock) {
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
"A copied image has no storage whose texel size is known."));
return false;
}
if (srcBlock.byteSize != dstBlock.byteSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
std::format("The base internal format of the two formats do not match ({} vs. {})",
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
"formats are not copy-compatible.",
srcBlock.byteSize, dstBlock.byteSize)));
return false;
}
// Two compressed images additionally have to agree on the SHAPE of the block, not only
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
if (srcBlock.compressed && dstBlock.compressed &&
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
dstBlock.blockHeight)));
return false;
}
return true;
}
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
Int imageWidth, Int imageHeight, const char* endpointName) {
if (!block.compressed) return true;
const Int blockWidth = static_cast<Int>(block.blockWidth);
const Int blockHeight = static_cast<Int>(block.blockHeight);
if (blockWidth <= 1 && blockHeight <= 1) return true;
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
// whole even when the last block is partial.
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
if (originAligned && widthOk && heightOk) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
"grid of a {} x {} image.",
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
return false;
}
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
+36 -2
View File
@@ -30,6 +30,16 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TextureInternalFormat internalFormat,
TexturePixelDataType type);
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
// "Is <level> a level this texture actually has?", which ValidateTextureLevelNumber above
// does NOT answer - that one only bounds the index by GL_MAX_TEXTURE_SIZE and knows nothing
// about the object. Entry points that resolve a level straight into a backend image
// subresource need this one: a level the texture never had is GL_INVALID_VALUE (GL 4.6 core
// 18.3.2), and passing it through instead reaches the driver as an out-of-range subresource.
// Note the error split is per-entry-point, so this is not universally reusable:
// glClearTexImage owes INVALID_OPERATION for the same out-of-range level and spells its own
// copy of this predicate in GL_Texture.cpp (GetClearTextureObject).
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
const char* caller);
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
@@ -40,8 +50,32 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TextureTarget target);
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
// The texel block of one glCopyImageSubData endpoint, resolved to the two things the
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
// a blocked image" from "uncompressed, and so it is measured in texels".
struct CopyImageTexelBlock {
SizeT byteSize = 0;
Uint blockWidth = 1;
Uint blockHeight = 1;
Bool compressed = false;
};
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
// alone can no longer tell a BPTC image from the RGBA8 backing it.
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
// pairs with an uncompressed one whose texel is as big as the compressed block.
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
const CopyImageTexelBlock& dstBlock);
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
// the edge of the image.
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
Int imageWidth, Int imageHeight, const char* endpointName);
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
// the requested internalformat asks for, but may supply more.
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
@@ -527,7 +527,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_Backend::pActiveBackendObject ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
"backend has no double-precision vertex attribute support - see the "
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
attribindex);
@@ -166,32 +166,32 @@ MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
// Legacy entry points some loaders probe for; harmless no-op stubs.
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
MGLOG_W("glx: glXCopyContext is not supported");
MGLOG_W_ONCE("glx: glXCopyContext is not supported");
}
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
MGLOG_W_ONCE("glx: glXCreateGLXPixmap is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
MGLOG_W("glx: glXCreatePixmap is not supported");
MGLOG_W_ONCE("glx: glXCreatePixmap is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
MGLOG_W("glx: glXCreatePbuffer is not supported");
MGLOG_W_ONCE("glx: glXCreatePbuffer is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
MGLOG_W("glx: glXUseXFont is not supported");
MGLOG_W_ONCE("glx: glXUseXFont is not supported");
}
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
+9 -9
View File
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
}
if (!fns->Valid()) {
MGLOG_E("glx: failed to load libX11 entry points");
MGLOG_E_ONCE("glx: failed to load libX11 entry points");
}
return fns;
}();
@@ -314,7 +314,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
Uint32 width = 0;
Uint32 height = 0;
if (!QueryDrawableSize(dpy, drawable, width, height)) {
MGLOG_E("glx: XGetGeometry failed for drawable 0x%lx", drawable);
MGLOG_E_ONCE("glx: XGetGeometry failed for drawable 0x%lx", drawable);
return nullptr;
}
@@ -326,7 +326,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
if (surface == EGL_NO_SURFACE) {
MGLOG_E("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
width, height);
return nullptr;
}
@@ -347,7 +347,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
MGLOG_E("glx: no EGL display");
MGLOG_E_ONCE("glx: no EGL display");
return nullptr;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
@@ -376,13 +376,13 @@ namespace MobileGL::MG_Impl::GLXImpl {
EGLint configCount = 0;
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
configCount <= 0) {
MGLOG_E("glx: eglChooseConfig failed");
MGLOG_E_ONCE("glx: eglChooseConfig failed");
return nullptr;
}
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
MGLOG_E("glx: eglCreateContext failed");
MGLOG_E_ONCE("glx: eglCreateContext failed");
return nullptr;
}
@@ -931,7 +931,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
object->Context)) {
MGLOG_E("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
return 0;
}
t_current = {dpy, drawable, drawable, context};
@@ -943,7 +943,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
if (context && draw != read) {
// MobileGL's backends reject split draw/read surfaces; bind the draw
// drawable for both, which is what every real caller here needs.
MGLOG_W("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
MGLOG_W_ONCE("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
read);
}
const int result = MakeCurrent(dpy, draw, context);
@@ -958,7 +958,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
auto& surfaces = DrawableSurfaces();
auto it = surfaces.find(drawable);
if (it == surfaces.end()) {
MGLOG_W("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
MGLOG_W_ONCE("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
return;
}
SyncSurfaceSize(dpy, drawable, it->second);
+1 -1
View File
@@ -31,7 +31,7 @@ namespace MG_Impl::GLXImpl {
#endif
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
if (!proc) {
MGLOG_W("Failed to get function: %s", (const char*)name);
MGLOG_D("Failed to get function: %s", (const char*)name);
return nullptr;
}
+1 -1
View File
@@ -1403,7 +1403,7 @@ namespace MobileGL::MG_Impl {
GETPROC(glFramebufferTextureMultiviewOVR, name);
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
MGLOG_W("GetProcAddress(%s) = nullptr!", name);
MGLOG_D("GetProcAddress(%s) = nullptr!", name);
return nullptr;
}
} // namespace MobileGL::MG_Impl
@@ -269,7 +269,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
}
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
if (!metalLayerClass) {
MGLOG_E("NSOpenGLImpl: CAMetalLayer class not found");
MGLOG_E_ONCE("NSOpenGLImpl: CAMetalLayer class not found");
return nil;
}
@@ -310,7 +310,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
static_cast<GLint>(geometry.DrawableSize.width),
static_cast<GLint>(geometry.DrawableSize.height));
if (error != kCGLNoError) {
MGLOG_E("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
MGLOG_E_ONCE("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
}
}
@@ -325,7 +325,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
}
const auto error = CGLImpl::SetCurrentContext(context);
if (error != kCGLNoError) {
MGLOG_E("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
MGLOG_E_ONCE("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
}
}
@@ -345,7 +345,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
}
const auto error = CGLImpl::FlushDrawable(context);
if (error != kCGLNoError) {
MGLOG_E("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
MGLOG_E_ONCE("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
}
}
@@ -377,7 +377,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
static_cast<GLint>(geometry.DrawableSize.width),
static_cast<GLint>(geometry.DrawableSize.height));
if (error != kCGLNoError) {
MGLOG_E("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
MGLOG_E_ONCE("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
return;
}
CGLImpl::UpdateContext(context);
@@ -421,7 +421,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
SEL selector = sel_registerName(selectorName);
Method method = class_getInstanceMethod(cls, selector);
if (!method) {
MGLOG_W("NSOpenGLImpl: missing instance method %s", selectorName);
MGLOG_W_ONCE("NSOpenGLImpl: missing instance method %s", selectorName);
return;
}
if (original) {
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
SEL selector = sel_registerName(selectorName);
Method method = class_getClassMethod(cls, selector);
if (!method) {
MGLOG_W("NSOpenGLImpl: missing class method %s", selectorName);
MGLOG_W_ONCE("NSOpenGLImpl: missing class method %s", selectorName);
return;
}
method_setImplementation(method, replacement);
@@ -444,7 +444,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
Class contextClass = objc_getClass("NSOpenGLContext");
if (!pixelFormatClass || !contextClass) {
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
MGLOG_W_ONCE("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
return false;
}
@@ -56,7 +56,7 @@ extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
}
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
MGLOG_W("wglCopyContext is not supported");
MGLOG_W_ONCE("wglCopyContext is not supported");
SetLastError(ERROR_NOT_SUPPORTED);
return FALSE;
}
@@ -132,24 +132,24 @@ extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsA is not supported");
MGLOG_W_ONCE("wglUseFontBitmapsA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsW is not supported");
MGLOG_W_ONCE("wglUseFontBitmapsW is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesA is not supported");
MGLOG_W_ONCE("wglUseFontOutlinesA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesW is not supported");
MGLOG_W_ONCE("wglUseFontOutlinesW is not supported");
return FALSE;
}
+7 -7
View File
@@ -215,7 +215,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
MGLOG_E_ONCE("wgl: GetClientRect failed for HWND %p", hwnd);
return nullptr;
}
@@ -227,7 +227,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
EGLSurface surface =
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
if (surface == EGL_NO_SURFACE) {
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
MGLOG_E_ONCE("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
return nullptr;
}
@@ -244,7 +244,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
MGLOG_E("wgl: no EGL display");
MGLOG_E_ONCE("wgl: no EGL display");
return nullptr;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Impl::WGLImpl {
EGLConfig config = nullptr;
EGLint configCount = 0;
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
MGLOG_E("wgl: eglChooseConfig failed");
MGLOG_E_ONCE("wgl: eglChooseConfig failed");
return nullptr;
}
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
MGLOG_E("wgl: eglCreateContext failed");
MGLOG_E_ONCE("wgl: eglCreateContext failed");
return nullptr;
}
@@ -612,7 +612,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it == surfaces.end()) {
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
MGLOG_W_ONCE("wglSwapBuffers: no surface for HWND %p", hwnd);
return FALSE;
}
SyncSurfaceSize(hwnd, it->second);
@@ -685,7 +685,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
}
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
MGLOG_E_ONCE("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
return FALSE;
}
t_current = {hdc, hglrc};
@@ -70,6 +70,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/FragmentOutputArrayIndexScenario.cpp
@@ -77,6 +78,8 @@ add_executable(MobileGLIntegrationTest
Scenarios/VertexAttribBindingScenario.cpp
Scenarios/XfbCaptureBufferReuseScenario.cpp
Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -0,0 +1,331 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.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
//
// Scenario - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
//
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
//
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
//
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
// and extent.depth counts them; the layer range must stay (0, 1).
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
// them and layerCount counts them; offset.z stays 0.
//
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
// scenario per pair is the coverage that matters here.
//
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
// slices-landed check and fail this one.
//
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
// slice through the same call, so the two texture kinds are read back identically.
//
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
#include <array>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kWidth = 4;
constexpr int kHeight = 4;
// Six is enough for a copy that starts and ends away from both edges of both endpoints
// while still leaving untouched slices on either side to assert against.
constexpr int kSlices = 6;
struct Rgba8 {
GLubyte r = 0, g = 0, b = 0, a = 0;
bool operator==(const Rgba8& other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
};
std::string Describe(const Rgba8& color) {
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
", " + std::to_string(color.a) + ")";
}
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
// in which SLICE the copy addresses, and a value that also varied within the slice would
// make the assertions depend on the framebuffer row order as well.
Rgba8 SourceColor(int slice) {
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
static_cast<GLubyte>(200 - slice * 15), 255};
}
Rgba8 DestinationFill(int slice) {
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
static_cast<GLubyte>(120 + slice * 5), 255};
}
class CopyImageLayeredScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (!CopyImageSubDataUsable()) {
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
}
}
void TearDown() override {
if (!Ready()) return;
for (const GLuint texture : m_textures) {
glDeleteTextures(1, &texture);
}
m_textures.clear();
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
}
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
// on any of the behaviour under test, so a driver (or a backend function table) that
// simply does not have the call skips instead of failing every case below.
bool CopyImageSubDataUsable() {
GLuint probe[2] = {0, 0};
glGenTextures(2, probe);
for (const GLuint texture : probe) {
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
}
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
while (glGetError() != GL_NO_ERROR) {
}
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
0, 1, 1, 1);
const bool usable = glGetError() == GL_NO_ERROR;
glDeleteTextures(2, probe);
return usable;
}
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
// names, so the level's own extent - a 3D level's depth included - has to be resolved
// rather than assumed to be the image's.
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(target, texture);
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// Fill every level, so nothing below can pass by reading a level that was never
// written and happened to hold the expected bytes.
for (int level = 0; level < levels; ++level) {
const int levelWidth = kWidth << (levels - 1 - level);
const int levelHeight = kHeight << (levels - 1 - level);
const int levelSlices =
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
for (int slice = 0; slice < levelSlices; ++slice) {
const Rgba8 color = colorForSlice(slice % kSlices);
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
GL_UNSIGNED_BYTE, texels.data());
}
}
glBindTexture(target, 0);
return texture;
}
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
// array layer and a 3D slice through the same argument, so both targets read back the
// same way.
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
if (m_fbo == 0) {
glGenFramebuffers(1, &m_fbo);
}
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "slice " << slice << " of level " << level << " is not attachable";
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// The fill is uniform within a slice, so any disagreement between texels is itself
// a failure - reported here rather than silently reduced to pixels[0].
for (size_t i = 1; i < pixels.size(); ++i) {
EXPECT_TRUE(pixels[i] == pixels[0])
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
}
return pixels[0];
}
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
// source slice they were fed, and every slice outside it still holds its own fill.
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
int dstZ, int depth, const char* what) {
for (int slice = 0; slice < sliceCount; ++slice) {
const bool inRange = slice >= dstZ && slice < dstZ + depth;
const Rgba8 expected =
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
EXPECT_TRUE(actual == expected)
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
<< " is " << Describe(actual) << ", expected " << Describe(expected);
}
}
std::vector<GLuint> m_textures;
GLuint m_fbo = 0;
};
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
// counts carry the depth and extent.depth must stay 1.
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
kWidth, kHeight, kSlices);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
}
// The same pair with the layer ranges offset differently on the two sides: the shape that
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
// the source range but wrote from layer 0 (or vice versa) passes the case above.
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 3;
constexpr int kDstZ = 1;
constexpr int kDepth = 2;
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
kDstZ, kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
"array->array, offset layer ranges");
}
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 1;
constexpr int kDstZ = 3;
constexpr int kDepth = 3;
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
}
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
// this is the only case where the slice count the copy may name is not the image's own -
// the bound a layered endpoint is checked against has to come from the level.
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kLevel = 1;
constexpr int kSrcZ = 2;
constexpr int kDstZ = 0;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
kDstZ, kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
"3d->3d at mip level 1");
}
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 2;
constexpr int kDstZ = 1;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
}
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
// while the source states it as extent.depth from a z offset.
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 1;
constexpr int kDstZ = 2;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,209 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLevelRangeScenario.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
//
// KHR-GL43.copy_image.non_existent_mipmap, and what it cost.
//
// The CTS case is a pure negative test: two 16x16 textures that have level 0 and
// nothing else, and a glCopyImageSubData naming level 1. The answer is
// GL_INVALID_VALUE (GL 4.6 core 18.3.2 / ARB_copy_image: "srcLevel/dstLevel is not
// a valid level"). MobileGL's frontend only checked the level against
// GL_MAX_TEXTURE_SIZE, so level 1 sailed through into the backends, DirectVulkan
// resolved it into a VkImageCopy subresource on a VkImage that was created with
// exactly one mip level, and the Adreno driver dereferenced the level it was
// promised - SIGSEGV inside vkCmdCopyImage, taking the whole glcts process down
// mid-run. A negative case must never do that.
//
// So the level-1-on-a-one-level-texture rejection is the regression proper, and the
// rest of this file is what keeps the fix honest. A validator that answered
// GL_INVALID_VALUE to every level would satisfy the regression tests alone, so the
// scenarios below pin the BOUNDARY rather than the symptom:
//
// * a texture that really does have two levels must accept a copy at level 1,
// * the same texture must still reject level 2,
// * and a plain level-0 copy must move pixels, which is checked by reading the
// destination back rather than by trusting glGetError.
//
// Both backends are covered because the fix is in the shared frontend: DirectGLES
// forwards to the ES glCopyImageSubData (whose own error lands in the ES context,
// not in MobileGL's, so it never reached the application either) and DirectVulkan
// records the copy itself.
#include <array>
#include <cstring>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr GLsizei kSize = 16;
struct Rgba8 {
GLubyte r, g, b, a;
bool operator==(const Rgba8& other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
};
std::vector<Rgba8> SolidImage(GLsizei width, GLsizei height, Rgba8 color) {
return std::vector<Rgba8>(static_cast<std::size_t>(width) * static_cast<std::size_t>(height), color);
}
class CopyImageLevelRangeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
DeleteTextures();
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
DrainErrors();
ScenarioTest::TearDown();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
void DeleteTextures() {
if (m_src != 0) glDeleteTextures(1, &m_src);
if (m_dst != 0) glDeleteTextures(1, &m_dst);
m_src = 0;
m_dst = 0;
}
// One 16x16 RGBA8 texture with `levelCount` levels defined through
// glTexImage2D - the same way the CTS case builds its textures, and
// deliberately NOT glTexStorage2D: an immutable allocation would define the
// whole chain up front and could not express "level 1 does not exist".
GLuint MakeTexture(int levelCount, Rgba8 baseColor) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
for (int level = 0; level < levelCount; ++level) {
const GLsizei extent = kSize >> level;
const std::vector<Rgba8> pixels = SolidImage(extent, extent, baseColor);
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, extent, extent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
pixels.data());
}
// What Utils::makeTextureComplete does in the CTS case: the texture is
// complete for the levels it actually has, not for a chain it does not.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levelCount - 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
void MakePair(int levelCount) {
DeleteTextures();
m_src = MakeTexture(levelCount, Rgba8{11, 22, 33, 255});
m_dst = MakeTexture(levelCount, Rgba8{200, 100, 50, 255});
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "texture setup with " << levelCount << " level(s)";
}
// The call under test, at whatever levels the caller wants, over a 1x1
// region so the region check can never be what rejects it.
GLenum CopyAt(GLint srcLevel, GLint dstLevel, GLsizei extent = 1) {
DrainErrors();
glCopyImageSubData(m_src, GL_TEXTURE_2D, srcLevel, 0, 0, 0, m_dst, GL_TEXTURE_2D, dstLevel, 0, 0, 0,
extent, extent, 1);
const GLenum error = glGetError();
// A second pending error would mean the entry point queued more than one,
// and the extra would be handed out at an unrelated call site later.
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "the copy recorded more than one error";
return error;
}
Rgba8 ReadBackDestinationLevel0() {
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_dst, 0);
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
ADD_FAILURE() << "readback framebuffer incomplete: " << status;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return Rgba8{0, 0, 0, 0};
}
Rgba8 texel{0, 0, 0, 0};
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &texel);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return texel;
}
GLuint m_src = 0;
GLuint m_dst = 0;
GLuint m_fbo = 0;
};
// The regression. Level 1 of a texture that has only level 0 is not a level, and
// saying so is the whole job: before the fix this reached DirectVulkan, which
// handed mipLevel=1 to vkCmdCopyImage on a one-level VkImage and died inside the
// Adreno driver.
TEST_F(CopyImageLevelRangeScenario, LevelOneOfASingleLevelTextureIsRejected) {
if (!Ready()) GTEST_SKIP();
MakePair(1);
EXPECT_EQ(CopyAt(1, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 1";
EXPECT_EQ(CopyAt(0, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 1";
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "both levels 1";
}
// The negative control that makes the test above falsifiable: the same level
// index, on textures that genuinely have it, must be accepted. A validator that
// rejected every non-zero level would pass the regression test and fail here.
TEST_F(CopyImageLevelRangeScenario, LevelOneOfATwoLevelTextureIsAccepted) {
if (!Ready()) GTEST_SKIP();
MakePair(2);
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_NO_ERROR));
}
// And the boundary from the other side: two levels means 0 and 1, not 2.
TEST_F(CopyImageLevelRangeScenario, LevelTwoOfATwoLevelTextureIsRejected) {
if (!Ready()) GTEST_SKIP();
MakePair(2);
EXPECT_EQ(CopyAt(2, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 2";
EXPECT_EQ(CopyAt(0, 2), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 2";
}
// Errors alone cannot tell an accepted copy from a silently dropped one, so the
// ordinary case is checked by reading the destination back: the copy has to move
// the source's texel, not merely decline to complain.
TEST_F(CopyImageLevelRangeScenario, AValidLevelZeroCopyStillMovesPixels) {
if (!Ready()) GTEST_SKIP();
MakePair(1);
ASSERT_EQ(ReadBackDestinationLevel0(), (Rgba8{200, 100, 50, 255})) << "destination before the copy";
EXPECT_EQ(CopyAt(0, 0, kSize), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_EQ(ReadBackDestinationLevel0(), (Rgba8{11, 22, 33, 255})) << "destination after the copy";
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,314 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageFormatQualifierScenario.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
//
// Scenario - AN IMAGE UNIFORM THAT DECLARES NO FORMAT.
//
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier:
//
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
//
// GLSL ES has no such relaxation; every image uniform must carry one, and Adreno says so as "all
// images have to define layout format", which fails the whole program. That is what took the
// compute half of KHR-GL4x.packed_depth_stencil.stencil_texturing.
//
// The only qualifier that is CORRECT to substitute is whatever glBindImageTexture named for the
// unit that uniform addresses - GL requires the qualifier, the bind format and the texture's
// internal format to belong to one format class - so the format is not knowable when the shader
// is compiled, only when it is drawn with. Espryt therefore BAKES it into the program it
// generates and keys that program on the (unit, format) pairs it baked
// (BackendProgramObjectImpl::ImageUnitFormatsStillMatch, MG_Backend/DirectGLES).
//
// Three separate things follow from "the program is built against live binding state", and each
// one is a case below:
//
// 1. the format reaches the shader at all, so the store lands where the texture is (Writes);
// 2. binding a DIFFERENT format to the same unit rebuilds the program, rather than reusing one
// compiled against the old format (RebindToADifferentFormatRebuilds);
// 3. an image bound for the FIRST time after the link works, i.e. the program built against
// "nothing bound yet" is not the one the dispatch runs (FirstBindAfterLinkRebuilds).
//
// Magma needs none of this - Vulkan takes an Unknown-format storage image given
// shaderStorageImageWriteWithoutFormat, and the view format is resolved from the same bind state
// at descriptor time - so every case here runs on both backends and must agree, which is what
// makes the ES-only machinery falsifiable rather than merely exercised.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 4;
// The image unit is deliberately NOT 0 and the uniform declares no binding, so the unit
// has to travel through glUniform1i and be baked into the ESSL alongside the format -
// the two bakes share a rebuild key and a bug in either shows up as the wrong texel.
constexpr GLint kImageUnit = 1;
// KHR-GL4x.packed_depth_stencil.stencil_texturing's own image declaration, verbatim.
const char* kStoreSource = R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
writeonly uniform uimage2D uni_image;
void main()
{
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
}
)";
class ImageFormatQualifierScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
for (GLuint t : m_textures) glDeleteTextures(1, &t);
m_programs.clear();
m_textures.clear();
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
}
while (glGetError() != GL_NO_ERROR) {
}
}
bool ImagesAreUsable() const {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
}
GLuint MakeComputeProgram(const std::string& source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
m_programs.push_back(program);
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log;
return 0;
}
return program;
}
GLuint MakeTexture(GLenum internalFormat) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
return 0;
}
// Seeded to a value no dispatch writes, so "the store never happened" and "the
// store wrote the right thing" cannot be confused.
const std::vector<GLuint> zeros(static_cast<std::size_t>(kExtent) * kExtent * 4u, 0u);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent,
internalFormat == GL_RGBA32UI ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
zeros.data());
while (glGetError() != GL_NO_ERROR) {
}
return texture;
}
// Texel (x, 0) of the texture's red channel, read back through the GL frontend rather
// than through a second image uniform: a defect in the format bake would be shared by
// a reader declared the same way and could cancel itself out.
GLuint ReadRedTexel(GLuint texture, GLenum internalFormat, int x) {
const bool rgba = internalFormat == GL_RGBA32UI;
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * (rgba ? 4u : 1u),
0xFFFFFFFFu);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, rgba ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
texels.data());
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
return 0xFFFFFFFFu;
}
return texels[static_cast<std::size_t>(x) * (rgba ? 4u : 1u)];
}
void DispatchStore(GLuint program, GLuint texture, GLenum internalFormat) {
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_FALSE, 0, GL_WRITE_ONLY,
internalFormat);
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
glUseProgram(program);
const GLint location = glGetUniformLocation(program, "uni_image");
ASSERT_GE(location, 0) << "the image uniform was not reflected";
glUniform1i(location, kImageUnit);
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image unit errored";
glDispatchCompute(kExtent, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
glUseProgram(0);
}
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
};
// The defect itself. Without the bake the ES driver refuses the program outright and the
// texture keeps its seed - which is also exactly what a silently no-op dispatch looks
// like, and why the seed is a value no store writes.
TEST_F(ImageFormatQualifierScenario, AFormatlessWriteonlyImageWrites) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
const GLuint texture = MakeTexture(GL_R32UI);
if (program == 0 || texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << " of a format-less writeonly image did not take the store";
}
}
// The rebuild key. The SAME program is dispatched twice with a different format bound to
// its unit; a build keyed only on the link (or only on the image UNIT) would reuse the
// r32ui program for the rgba32ui texture, and the second half would come back seeded.
//
// What the SOFTWARE lanes cannot falsify: with the key disabled this case still passes on
// Mesa, because the reused r32ui declaration writes the red channel of an RGBA32UI image
// anyway - a format-class mismatch GL leaves undefined and that driver happens to absorb.
// FirstBindAfterLinkRebuilds below is the case that fails there, because the reused
// program was built with no format at all and never compiled. Both are kept: this one is
// the shape a strict driver is entitled to reject, and it is the shape the device runs.
TEST_F(ImageFormatQualifierScenario, RebindToADifferentFormatRebuilds) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
const GLuint first = MakeTexture(GL_R32UI);
const GLuint second = MakeTexture(GL_RGBA32UI);
if (program == 0 || first == 0 || second == 0) return;
DispatchStore(program, first, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
ASSERT_EQ(ReadRedTexel(first, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "the first format must work before the rebind can be blamed for anything";
}
DispatchStore(program, second, GL_RGBA32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(second, GL_RGBA32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": the program was not rebuilt for the newly bound format";
}
// ...and back, so the rebuild is not a one-way door: returning to a format the
// program was once built against must build for it again, not resurrect a cache row.
const GLuint third = MakeTexture(GL_R32UI);
if (third == 0) return;
DispatchStore(program, third, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(third, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": going back to the first format did not rebuild";
}
}
// Nothing is bound to the unit when the program links, so whatever the first build sees
// is not the format the dispatch needs. glBindImageTexture must not itself trigger a
// build - it is an entry point, and building there is the constraint
// glShaderStorageBlockBinding is held to as well - so the rebuild has to happen at the
// next dispatch preparation instead. This case fails either way round: no rebuild, or a
// build attempted from the entry point before the state settles.
TEST_F(ImageFormatQualifierScenario, FirstBindAfterLinkRebuilds) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
if (program == 0) return;
// Use it once with NOTHING bound to the unit, which is what makes the backend build
// against an empty binding. The dispatch writes nowhere and must not error.
glUseProgram(program);
const GLint location = glGetUniformLocation(program, "uni_image");
ASSERT_GE(location, 0);
glUniform1i(location, kImageUnit);
glDispatchCompute(kExtent, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "dispatching with an unbound image unit must not error";
glUseProgram(0);
const GLuint texture = MakeTexture(GL_R32UI);
if (texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": the first bind after the link did not reach the shader";
}
}
// A DECLARED format is authoritative and the bake must never touch it - including when
// the texture behind the unit has a different (but class-compatible) internal format,
// which GL explicitly allows. If the bake ever overrode a declaration, this is the case
// that would go wrong while every other one stayed green.
TEST_F(ImageFormatQualifierScenario, ADeclaredFormatStillWins) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r32ui) writeonly uniform uimage2D uni_image;
void main()
{
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
}
)");
const GLuint texture = MakeTexture(GL_R32UI);
if (program == 0 || texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": a declared format stopped working";
}
}
} // namespace
} // namespace MGITest
+4 -4
View File
@@ -196,7 +196,7 @@ namespace MobileGL::MG_State {
// (which expands to nothing outside debug builds).
void GLContext::SetCurrentVertexAttributeFloat(Uint index, const Array<Float, 4>& value) {
if (index >= m_currentVertexAttributes.size()) {
MGLOG_E("SetCurrentVertexAttributeFloat: index %u is out of range", index);
MGLOG_E_ONCE("SetCurrentVertexAttributeFloat: index %u is out of range", index);
return;
}
@@ -210,7 +210,7 @@ namespace MobileGL::MG_State {
void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array<Int32, 4>& value) {
if (index >= m_currentVertexAttributes.size()) {
MGLOG_E("SetCurrentVertexAttributeInt: index %u is out of range", index);
MGLOG_E_ONCE("SetCurrentVertexAttributeInt: index %u is out of range", index);
return;
}
@@ -224,7 +224,7 @@ namespace MobileGL::MG_State {
void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array<Uint32, 4>& value) {
if (index >= m_currentVertexAttributes.size()) {
MGLOG_E("SetCurrentVertexAttributeUint: index %u is out of range", index);
MGLOG_E_ONCE("SetCurrentVertexAttributeUint: index %u is out of range", index);
return;
}
@@ -239,7 +239,7 @@ namespace MobileGL::MG_State {
const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const {
static const CurrentVertexAttributeValue defaultValue{};
if (index >= m_currentVertexAttributes.size()) {
MGLOG_E("GetCurrentVertexAttribute: index %u is out of range", index);
MGLOG_E_ONCE("GetCurrentVertexAttribute: index %u is out of range", index);
return defaultValue;
}
return m_currentVertexAttributes[index];
@@ -14,10 +14,10 @@
namespace MobileGL::MG_State::GLState {
void ErrorState::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
if (code == ErrorCode::NoError) {
MGLOG_E("Recording Non-OpenGL error:\n%s", info->toString().c_str());
MGLOG_D("Recording Non-OpenGL error:\n%s", info->toString().c_str());
m_nonGLErrors.push_back(MakeUnique<Error>(code, Move(info)));
} else {
MGLOG_E("Recording OpenGL error (%s):\n%s",
MGLOG_D("Recording OpenGL error (%s):\n%s",
MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(),
info->toString().c_str());
// GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple
@@ -255,7 +255,7 @@ namespace MobileGL::MG_State::GLState {
static_cast<SizeT>(offset) + write.byteOffsetInUniform + write.byteSize > uboSize) {
// Same verdict the live write path reaches for a uniform without backing
// storage: log and drop, rather than fault.
MGLOG_E("ProgramObject %u: buffered uniform write at location %u has no backing storage "
MGLOG_E_ONCE("ProgramObject %u: buffered uniform write at location %u has no backing storage "
"(offset=%u size=%u uboSize=%zu); dropping write",
m_externalIndex, write.location, offset, write.byteSize, uboSize);
continue;
@@ -436,7 +436,7 @@ namespace MobileGL::MG_State::GLState {
defaultFS->Compile(); // TODO: use a global default FS object.
auto status = defaultFS->GetCompileStatus();
if (!status) {
MGLOG_E("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
MGLOG_E_ONCE("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
defaultFS->GetInfoLog().c_str());
return;
}
@@ -16,9 +16,11 @@
#include <MG_Backend/DirectGLES/Utils.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
namespace {
@@ -435,3 +437,116 @@ void main() { tes_gs_coord = tcs_tes_coord[0]; }
EXPECT_TRUE(Contains(out, "layout(location = 1) out vec2 tes_gs_coord;")) << out;
EXPECT_EQ(CountOf(out, "flat"), 0u) << out;
}
// --- image format qualifier completion ---------------------------------------------------------
//
// GLSL ES requires a format layout qualifier on every image; desktop GLSL lets a writeonly
// declaration omit one. The format is normally written into the SPIR-V before SPIRV-Cross runs
// (BakeImageFormatsPass), but SPIRV-Cross THROWS rather than printing the formats it calls
// desktop-only for ESSL - r8ui among them - so those are completed here, on the emitted text.
// The KHR-GL4x.packed_depth_stencil.stencil_texturing stencil half: `writeonly uniform uimage2D`
// with GL_R8UI bound to its unit.
TEST(BakeImageFormatQualifiersTest, AFormatlessDeclarationGetsTheBoundFormat) {
const String source = R"(#version 320 es
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;")) << out;
}
// A declaration with NO layout at all still has to end up with one, or the driver rejects it for
// exactly the reason this pass exists.
TEST(BakeImageFormatQualifiersTest, ADeclarationWithNoLayoutGetsOne) {
const String source = R"(#version 320 es
uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r16i"}});
EXPECT_TRUE(Contains(out, "layout(r16i) uniform writeonly highp uimage2D uni_image;")) << out;
}
// A DECLARED format is authoritative and must survive, whatever the map says - the frontend never
// puts a declared image in the map, and the pass must not depend on that being true.
TEST(BakeImageFormatQualifiersTest, ADeclaredFormatIsNeverOverwritten) {
const String source = R"(#version 320 es
layout(binding = 1, rgba8ui) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
EXPECT_EQ(out, source) << out;
}
// Only the named uniform. A second image in the same shader - format-less because the pass
// declined it, or because its unit holds nothing - must be left exactly as it is.
TEST(BakeImageFormatQualifiersTest, OnlyTheNamedUniformIsTouched) {
const String source = R"(#version 320 es
layout(binding = 0) uniform writeonly highp uimage2D named;
layout(binding = 1) uniform writeonly highp uimage2D other;
void main() { imageStore(named, ivec2(0), uvec4(1u)); imageStore(other, ivec2(0), uvec4(2u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"named", "r8ui"}});
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 0) uniform writeonly highp uimage2D named;")) << out;
EXPECT_TRUE(Contains(out, "layout(binding = 1) uniform writeonly highp uimage2D other;")) << out;
}
// The format the pass writes has to survive the two passes that run after it, or nothing was
// gained: the read+write split copies declarations, and the binding strip edits layout qualifiers.
TEST(BakeImageFormatQualifiersTest, TheWrittenFormatSurvivesTheLaterImagePasses) {
const String source = R"(#version 320 es
layout(binding = 3) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
out = SplitReadWriteImageUniforms(out);
out = RemoveLayoutBinding(out);
EXPECT_TRUE(Contains(out, "r8ui")) << out;
EXPECT_TRUE(Contains(out, "binding = 3")) << out;
}
TEST(BakeImageFormatQualifiersTest, AnEmptyMapOrAnImagelessShaderIsANoOp) {
const String withImage = R"(#version 320 es
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
EXPECT_EQ(BakeImageFormatQualifiers(withImage, {}), withImage);
const String withoutImage = R"(#version 320 es
layout(location = 0) out highp vec4 mg_FragColor;
void main() { mg_FragColor = vec4(1.0); }
)";
EXPECT_EQ(BakeImageFormatQualifiers(withoutImage, {{"uni_image", "r8ui"}}), withoutImage);
}
// --- GL_NV_image_formats directive --------------------------------------------------------------
TEST(RequestExtendedImageFormatsTest, TheDirectiveGoesRightAfterTheVersionLine) {
const String source = R"(#version 320 es
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = RequestExtendedImageFormats(source, true);
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_NV_image_formats : require\n")) << out;
}
// Never speculatively: `#extension` naming an extension the driver does not advertise is itself a
// compile error, so the caller's "not needed" answer has to be honoured exactly.
TEST(RequestExtendedImageFormatsTest, NotNeededMeansNotEmitted) {
const String source = R"(#version 320 es
layout(rgba8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
EXPECT_EQ(RequestExtendedImageFormats(source, false), source);
}
TEST(RequestExtendedImageFormatsTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
const String source = R"(#version 320 es
#extension GL_NV_image_formats : require
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = RequestExtendedImageFormats(source, true);
EXPECT_EQ(out, source);
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
}
+1
View File
@@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.14)
add_executable(
PipelineQuirkTest
PipelineQuirkTest.cpp
PassthroughTessControlTest.cpp
)
target_include_directories(PipelineQuirkTest PRIVATE
@@ -0,0 +1,247 @@
// MobileGL - MobileGL/MG_Test/Pipeline/PassthroughTessControlTest.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 <gtest/gtest.h>
#include "Includes.h"
#include "Init.h"
#include <map>
#include <set>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
// A test-side SPIR-V walker, deliberately independent of the production reflection: the
// generator's contract with the evaluation stage is "declare this many output vertices and
// write these built-ins", and that has to be readable off the module itself.
constexpr Uint32 kSpirvHeaderWordCount = 5;
constexpr Uint32 kOpExecutionMode = 16;
constexpr Uint32 kOpDecorate = 71;
constexpr Uint32 kOpMemberDecorate = 72;
constexpr Uint32 kExecutionModeOutputVertices = 26;
constexpr Uint32 kDecorationBuiltIn = 11;
// SpvBuiltIn values used below.
constexpr Uint32 kBuiltInPosition = 0;
constexpr Uint32 kBuiltInInvocationId = 8;
constexpr Uint32 kBuiltInTessLevelOuter = 11;
constexpr Uint32 kBuiltInTessLevelInner = 12;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
const Uint32 wordCount = spirv[i] >> 16;
const Uint32 opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
visit(opcode, &spirv[i], wordCount);
i += wordCount;
}
}
// -1 when the module declares no OutputVertices mode at all, which is itself a failure the
// tests want to see named rather than silently compared against a wrong number.
Int DeclaredOutputVertices(const Vector<Uint32>& spirv) {
Int declared = -1;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpExecutionMode && wordCount >= 4 && words[2] == kExecutionModeOutputVertices) {
declared = static_cast<Int>(words[3]);
}
});
return declared;
}
// The built-in members of every block in the module, keyed by the struct's result id, in
// member order. A gl_PerVertex is exactly such a struct, and its member list IS the shape the
// neighbouring stage has to agree with.
constexpr Uint32 kOpTypeStruct = 30;
std::map<Uint32, Vector<Uint32>> BuiltInBlockShapes(const Vector<Uint32>& spirv) {
std::map<Uint32, Vector<Uint32>> shapes;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[3] == kDecorationBuiltIn) {
shapes[words[1]].push_back(words[4]);
}
});
return shapes;
}
// Member count of a struct type, so a shape comparison can also catch a block that grew a
// NON-built-in member (which the decoration walk above would not see).
Uint32 StructMemberCount(const Vector<Uint32>& spirv, Uint32 structId) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpTypeStruct && wordCount >= 2 && words[1] == structId) {
count = wordCount - 2;
}
});
return count;
}
std::set<Uint32> DeclaredBuiltIns(const Vector<Uint32>& spirv) {
std::set<Uint32> builtIns;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpDecorate && wordCount >= 4 && words[2] == kDecorationBuiltIn) {
builtIns.insert(words[3]);
}
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[3] == kDecorationBuiltIn) {
builtIns.insert(words[4]);
}
});
return builtIns;
}
Vector<Uint32> CompileGeneratedSource(Uint32 patchVertices) {
using namespace MG_Util::ShaderTranspiler;
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices);
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log) << "\n" << source;
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER},
.program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
} // namespace
class PassthroughTessControlTest : public ::testing::Test {
protected:
void SetUp() override { MobileGL::Initialize(); }
};
// The whole reason this stage is generated per patch size rather than once: GL takes the output
// patch size from PATCH_VERTICES, which is draw state. A program that links at the default 3 and
// draws at 4 - which is exactly what
// KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation does - must get a stage built
// for 4, or its evaluation stage reads gl_in[3] out of a three-element array.
TEST_F(PassthroughTessControlTest, DeclaresTheRequestedPatchSize) {
for (const Uint32 patchVertices : {1u, 2u, 3u, 4u, 16u, 32u}) {
const Vector<Uint32> spirv = CompileGeneratedSource(patchVertices);
ASSERT_FALSE(spirv.empty()) << "patchVertices=" << patchVertices;
EXPECT_EQ(DeclaredOutputVertices(spirv), static_cast<Int>(patchVertices))
<< "patchVertices=" << patchVertices;
}
}
// gl_Position in, gl_Position out, and both tessellation level arrays written: the four facts the
// evaluation stage downstream of this depends on. Position appearing at all is what makes the
// pass-through a pass-through; the levels are what GL's PATCH_DEFAULT_*_LEVEL state supplies when
// there is no control shader, and without them the tessellator produces nothing.
TEST_F(PassthroughTessControlTest, ForwardsPositionAndWritesBothLevelArrays) {
const Vector<Uint32> spirv = CompileGeneratedSource(4);
ASSERT_FALSE(spirv.empty());
const std::set<Uint32> builtIns = DeclaredBuiltIns(spirv);
EXPECT_TRUE(builtIns.contains(kBuiltInPosition));
EXPECT_TRUE(builtIns.contains(kBuiltInInvocationId));
EXPECT_TRUE(builtIns.contains(kBuiltInTessLevelOuter));
EXPECT_TRUE(builtIns.contains(kBuiltInTessLevelInner));
}
// The generated source carries nothing but gl_Position across the interface. If that ever grows a
// user-defined varying, ReflectPassthroughTessControlNeed's "built-ins only" refusal stops being
// the right gate and both have to move together.
TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
const String source = ProgramFactory::BuildPassthroughTessControlSource(4);
EXPECT_EQ(source.find("layout(location"), String::npos) << source;
EXPECT_NE(source.find("layout(vertices = 4) out;"), String::npos) << source;
}
// THE load-bearing test. Vulkan matches built-in interface blocks by their whole shape, and this
// stage is compiled ON ITS OWN - it never goes through the glslang link that gives a real program
// its gl_PerVertex. So the shape it declares has to equal the shape a linked vertex+evaluation
// program carries, and nothing at runtime says otherwise: a mismatch renders a black frame, no
// error, no validation message. That is exactly how the first cut of this shipped-and-failed
// (gl_Position only, three members short), and how the second did (glslang's default block for a
// standalone control stage, which appends gl_CullDistance where a linked program has no such
// member). This links the shader pair the motivating CTS case uses and compares the two shapes
// directly.
TEST_F(PassthroughTessControlTest, MatchesTheFrontendPerVertexBlock) {
using namespace MG_Util::ShaderTranspiler;
// Deliberately the shape of KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation:
// a vertex stage feeding an evaluation stage with no control stage in between.
static const char* kVs = R"(#version 430 core
layout(location = 0) in vec4 g_in_position;
void main() { gl_Position = g_in_position; }
)";
static const char* kTes = R"(#version 430 core
layout(quads) in;
void main() {
vec4 p0 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
vec4 p1 = mix(gl_in[3].gl_Position, gl_in[2].gl_Position, gl_TessCoord.x);
gl_Position = mix(p0, p1, gl_TessCoord.y);
}
)";
static const char* kFs = R"(#version 430 core
layout(location = 0) out vec4 g_fs_out;
void main() { g_fs_out = vec4(0, 1, 0, 1); }
)";
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER};
const Vector<const char*> sources{kVs, kTes, kFs};
Vector<SharedPtr<glslang::TShader>> shaders;
for (SizeT i = 0; i < types.size(); ++i) {
ShaderAttrib attrib{.shaderType = types[i], .sourceStr = sources[i]};
auto compiled = ShaderCompiler::CompileShader(attrib);
ASSERT_TRUE(compiled) << compiled.error().log;
shaders.push_back(compiled.value());
}
ProgramAttrib programAttrib{.shaders = shaders};
auto linked = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(linked) << linked.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *linked.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binary);
ASSERT_EQ(binary->size(), types.size());
// The evaluation stage's gl_in is the block the pass-through has to feed. It is the only
// built-in block that stage declares as an input, so the module holds exactly one such shape
// besides its own gl_PerVertex output - and both are the same shape, which is the point.
const auto tesShapes = BuiltInBlockShapes((*binary)[1]);
ASSERT_FALSE(tesShapes.empty());
const Vector<Uint32> frontendShape = tesShapes.begin()->second;
const Uint32 frontendMembers = StructMemberCount((*binary)[1], tesShapes.begin()->first);
for (const auto& [structId, shape] : tesShapes) {
EXPECT_EQ(shape, frontendShape) << "the evaluation stage's own built-in blocks disagree";
EXPECT_EQ(StructMemberCount((*binary)[1], structId), frontendMembers);
}
const Vector<Uint32> passthrough = CompileGeneratedSource(4);
ASSERT_FALSE(passthrough.empty());
const auto passthroughShapes = BuiltInBlockShapes(passthrough);
ASSERT_FALSE(passthroughShapes.empty());
Uint32 perVertexBlocksChecked = 0;
for (const auto& [structId, shape] : passthroughShapes) {
// gl_TessLevelOuter/Inner are decorated on plain variables, not on a block, so every
// struct that reaches here is a gl_PerVertex - gl_in's and gl_out's.
EXPECT_EQ(shape, frontendShape)
<< "the pass-through control stage's gl_PerVertex no longer matches the one the "
"frontend gives a linked vertex+evaluation program";
EXPECT_EQ(StructMemberCount(passthrough, structId), frontendMembers)
<< "the pass-through control stage's gl_PerVertex has a different member count";
++perVertexBlocksChecked;
}
EXPECT_EQ(perVertexBlocksChecked, 2u) << "expected both gl_in and gl_out to be gl_PerVertex blocks";
}
@@ -3689,3 +3689,316 @@ void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 1u)
<< "declining means the 1D-array type is still there for the driver to reject";
}
// --- image format qualifier bake (BakeImageFormatsPass) ---------------------------------------
//
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier; GLSL ES
// requires one of every image, and Adreno says so as "all images have to define layout format",
// losing the whole program. The only correct qualifier to substitute is the format the
// application passed to glBindImageTexture for that unit, so the transpile bakes it in.
namespace {
Uint CountSpirvOpcode(const String& disassembly, const String& opcode) {
Uint count = 0;
SizeT offset = 0;
const String needle = opcode + " ";
while ((offset = disassembly.find(needle, offset)) != String::npos) {
count += 1;
offset += needle.size();
}
return count;
}
constexpr Uint kGlR32ui = 0x8236;
constexpr Uint kGlRgba32ui = 0x8D70;
constexpr Uint kGlR8ui = 0x8232;
constexpr Uint kGlR32f = 0x822E;
} // namespace
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
// writeonly image, and a bind of a concrete format to the unit it addresses. (The DEPTH half of
// that case binds GL_R32F; the stencil half's GL_R8UI is one SPIRV-Cross will not print and takes
// the text route instead - see the test below.)
TEST_F(ProgramUtilTest, BakeImageFormatsGivesAFormatlessImageTheFormatBoundToItsUnit) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u, 0u, 0u, 0u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
<< DisassembleSpirv(spirv);
// Precondition: SPIRV-Cross prints no format for it, which is the ESSL the driver refuses.
EXPECT_EQ(DecompileToEssl(spirv).find("r32ui"), String::npos);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the baked module must stay validator-clean:\n"
<< DisassembleSpirv(baked);
const String essl = DecompileToEssl(baked);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("r32ui"), String::npos)
<< "the bound format must reach the declaration as a layout qualifier:\n" << essl;
EXPECT_NE(essl.find("writeonly"), String::npos)
<< "the access qualifier the declaration already had must survive:\n" << essl;
}
// SPIRV-Cross THROWS rather than printing the formats it calls desktop-only when it targets ESSL
// (Compiler::is_desktop_only_format), and a throw loses the whole stage - so baking one of those
// into the module would trade a missing qualifier for a missing shader. They are left format-less
// here and completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). r8ui,
// which the stencil half of the packed_depth_stencil case binds, is one of them.
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesTheFormatsSpirvCrossRefusesToPrint) {
using namespace MG_Util::ShaderTranspiler;
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
<< "if SPIRV-Cross ever learns to print r8ui for ES, the text completion can go";
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked));
EXPECT_EQ(baked, spirv) << "a format SPIRV-Cross cannot print must leave the module untouched";
// ...and the stage still transpiles, which is the whole point of declining.
EXPECT_FALSE(DecompileToEssl(baked).empty());
}
// A DECLARED format is authoritative: GL requires the qualifier, the bind format and the
// texture's internal format to be in the same class, but the qualifier is what the shader is
// specified to read the memory as, and a bake that overrode it would change what the shader does.
TEST_F(ProgramUtilTest, BakeImageFormatsNeverOverridesADeclaredFormat) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
layout (binding = 0, rgba32ui) writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
Vector<Uint32> baked;
// Even asked to, with a format of the right component class.
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a module with nothing format-less must pass through byte for byte";
EXPECT_NE(DecompileToEssl(baked).find("rgba32ui"), String::npos);
}
// Review finding. Every use has to be one the retype can carry end to end, and the decision has
// to be made BEFORE anything is mutated - a half-retyped module is not something a later decline
// could undo. An image handed to a FUNCTION is the shape that reaches SPIRV-Cross intact (nothing
// in the ESSL chain inlines), and its OpFunctionCall is a use this pass does not follow.
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAnImagePassedToAFunction) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void writeIt(writeonly uimage2D img) { imageStore(img, ivec2(0), uvec4(1u)); }
void main() { writeIt(uni_image); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a shape the retype cannot follow must leave the module untouched, "
"not partly rewritten:\n"
<< DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
}
// spirv-val requires the Image Format's component class to agree with the OpTypeImage's Sampled
// Type. Binding a uint format to a float image is an application error GL leaves undefined;
// baking it would turn that into an INVALID module, which is strictly worse than the compile
// error the shader already has, so the image is left format-less.
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAFormatOfTheWrongComponentClass) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform image2D uni_image;
void main() { imageStore(uni_image, ivec2(0), vec4(1.0)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
// ...and the same image with a float bind format is baked, so the decline above is about the
// class and not about the pass refusing float images.
Vector<Uint32> bakedFloat;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat));
EXPECT_NE(DecompileToEssl(bakedFloat).find("r32f"), String::npos) << DisassembleSpirv(bakedFloat);
}
// Two format-less images of the same type share ONE OpTypeImage. Giving them different formats
// therefore cannot be an in-place edit of that type - each needs its own declaration, and the
// variable, the loads and (for arrays) the access chains all have to follow.
TEST_F(ProgramUtilTest, BakeImageFormatsSplitsATypeTwoImagesShareWhenTheirFormatsDiffer) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D imgA;
writeonly uniform uimage2D imgB;
void main() {
imageStore(imgA, ivec2(0), uvec4(1u));
imageStore(imgB, ivec2(0), uvec4(2u));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 1u)
<< "the fixture must have the two images sharing one type:\n" << DisassembleSpirv(spirv);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "splitting the shared type must not leave a dangling or duplicate declaration:\n"
<< DisassembleSpirv(baked);
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked));
const String essl = DecompileToEssl(baked);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("r32ui"), String::npos) << essl;
EXPECT_NE(essl.find("rgba32ui"), String::npos) << essl;
}
// The mirror of the split: when the module ALREADY declares the type the bake wants, the two must
// be JOINED, not duplicated. SPIR-V forbids two identical non-aggregate type declarations, and
// that is exactly the defect an earlier image pass shipped and a reviewer caught.
TEST_F(ProgramUtilTest, BakeImageFormatsJoinsATypeTheModuleAlreadyDeclares) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D formatless;
layout (binding = 1, r32ui) writeonly uniform uimage2D declared;
void main() {
imageStore(formatless, ivec2(0), uvec4(1u));
imageStore(declared, ivec2(0), uvec4(2u));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 2u)
<< "the fixture needs one Unknown-format and one r32ui image type:\n" << DisassembleSpirv(spirv);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the baked image collided with the module's own r32ui image and left a duplicate type:\n"
<< DisassembleSpirv(baked);
EXPECT_EQ(CountSpirvOpcode(DisassembleSpirv(baked), "OpTypeImage"), 1u)
<< "the two identical image types must be the same declaration:\n" << DisassembleSpirv(baked);
}
// An ARRAY of format-less images: the variable's type is a pointer to an array, every use goes
// through an OpAccessChain, and all three levels have to be rebuilt for the load to still type-check.
TEST_F(ProgramUtilTest, BakeImageFormatsRetypesAnArrayOfFormatlessImagesThroughItsAccessChains) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D imgs[2];
void main() {
for (int i = 0; i < 2; ++i) imageStore(imgs[i], ivec2(0), uvec4(uint(i)));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the array and pointer types above the image must have been rebuilt too:\n"
<< DisassembleSpirv(baked);
EXPECT_NE(DecompileToEssl(baked).find("r32ui"), String::npos);
}
// A SAMPLED image's format operand is Unknown in every GLSL dialect and has no qualifier to bake;
// only storage images (Sampled == 2) are in scope.
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesSampledImagesAlone) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
uniform usampler2D uni_sampler;
out uvec4 fragColor;
in vec2 vUv;
void main() { fragColor = texture(uni_sampler, vUv); }
)",
GL_FRAGMENT_SHADER);
ASSERT_FALSE(spirv.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
<< "a sampled image must not read as a format-less STORAGE image:\n" << DisassembleSpirv(spirv);
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a sampled image must pass through byte for byte";
}
// The core/extended split the emitted ESSL depends on: GLSL ES has thirteen image formats, and a
// bind format outside them only compiles with GL_NV_image_formats - which the backend must not
// request on a driver that does not advertise it.
TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
using namespace MG_Util::ShaderTranspiler;
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32ui));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlRgba32ui));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32f));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8058 /*GL_RGBA8*/));
// The stencil half of KHR-GL4x.packed_depth_stencil.stencil_texturing binds this one, and it
// is NOT core - the whole reason the directive machinery exists.
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR8ui));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x822D /*GL_R16F*/));
// Not an image format at all.
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
}
+584
View File
@@ -1651,6 +1651,100 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
namespace {
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
// level, NOT glTexStorage2D, because an immutable allocation defines the whole chain
// up front and so cannot express "level 1 does not exist".
GLuint MakeCopyImageTexture(int levelCount) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
for (int level = 0; level < levelCount; ++level) {
const GLsizei extent = 16 >> level;
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, extent, extent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
}
return texture;
}
} // namespace
// KHR-GL43.copy_image.non_existent_mipmap. Level 1 of a texture that has only level 0 is
// not a level: GL 4.6 core 18.3.2 asks for GL_INVALID_VALUE. Until this check existed the
// level travelled all the way into the backends, and DirectVulkan built a VkImageCopy
// naming mip 1 of a VkImage created with one mip - which Adreno answered with a SIGSEGV
// inside vkCmdCopyImage, killing the glcts process in the middle of a negative test.
TEST_F(TextureTest, CopyImageSubDataRejectsALevelTheTextureDoesNotHave) {
const GLuint src = MakeCopyImageTexture(1);
const GLuint dst = MakeCopyImageTexture(1);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(src, GL_TEXTURE_2D, 1, 0, 0, 0, dst, GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::CopyImageSubData(src, GL_TEXTURE_2D, 0, 0, 0, 0, dst, GL_TEXTURE_2D, 1, 0, 0, 0, 1, 1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::CopyImageSubData(src, GL_TEXTURE_2D, 1, 0, 0, 0, dst, GL_TEXTURE_2D, 1, 0, 0, 0, 1, 1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// The negative control, and the reason the pair below asks for a zero-sized copy: a
// validator that answered GL_INVALID_VALUE to every non-zero level would satisfy the test
// above. The two calls here are IDENTICAL except for how many levels the textures have,
// and a zero extent makes the validator decline the copy without an error just after the
// level check - so the level count is the only thing either assertion can be reading, and
// no backend (there is none in this binary) is ever reached.
TEST_F(TextureTest, CopyImageSubDataAcceptsALevelTheTextureDoesHave) {
const GLuint oneLevelSrc = MakeCopyImageTexture(1);
const GLuint oneLevelDst = MakeCopyImageTexture(1);
const GLuint twoLevelSrc = MakeCopyImageTexture(2);
const GLuint twoLevelDst = MakeCopyImageTexture(2);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(oneLevelSrc, GL_TEXTURE_2D, 1, 0, 0, 0, oneLevelDst, GL_TEXTURE_2D, 1, 0, 0, 0,
0, 0, 0);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::CopyImageSubData(twoLevelSrc, GL_TEXTURE_2D, 1, 0, 0, 0, twoLevelDst, GL_TEXTURE_2D, 1, 0, 0, 0,
0, 0, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "level 1 of a two-level texture is a level";
// And the boundary from the other side: two levels means 0 and 1, not 2.
MG_Impl::GLImpl::CopyImageSubData(twoLevelSrc, GL_TEXTURE_2D, 2, 0, 0, 0, twoLevelDst, GL_TEXTURE_2D, 0, 0, 0, 0,
0, 0, 0);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// A texture that has never been given an image is a different fault from a level out of
// range, and the spec spells it differently: an incomplete object named by a copy is
// GL_INVALID_OPERATION. Worth pinning because the natural implementation of the check
// above - level >= levelCount - reports INVALID_VALUE for level 0 of a texture whose level
// count is zero, which is the wrong answer to the wrong question.
//
// BOTH textures are imageless on purpose, and that is the whole point rather than symmetry
// for its own sake. With one imageless and one RGBA8 texture the format comparison further
// down already rejected the call, so the case proved nothing about this check. With both
// imageless the formats are Unknown == Unknown, they MATCH, and every validator downstream
// waves the call through - which is how the second crash in this entry point was found: the
// call reached DirectVulkan, SyncTextureAndGetDescriptor returned nothing for a texture with
// no image, and the release build (where the guarding MOBILEGL_ASSERT expands to nothing)
// dereferenced it. Reproduced deterministically on lavapipe by
// KHR-GL43.copy_image.functional_src_target_texture_2d_array_..._dst_format_rgb9_e5.
TEST_F(TextureTest, CopyImageSubDataRejectsTwoTexturesWithNoImageAtAll) {
GLuint firstEmpty = 0;
GLuint secondEmpty = 0;
MG_Impl::GLImpl::GenTextures(1, &firstEmpty);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, firstEmpty);
MG_Impl::GLImpl::GenTextures(1, &secondEmpty);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, secondEmpty);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(firstEmpty, GL_TEXTURE_2D, 0, 0, 0, 0, secondEmpty, GL_TEXTURE_2D, 0, 0, 0, 0,
1, 1, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// GL_DEPTH_STENCIL_TEXTURE_MODE used to be a pure frontend shadow: stored, answered by
// glGetTexParameter, and never shown to a backend. Sampling therefore always read the depth
// aspect however the mode was set, which is the whole of
@@ -3085,6 +3179,193 @@ TEST_F(TextureTest, DecodeShadowDataToWideRGBACoversComponentAndPackedLayouts) {
}
}
// ---- GL_RGB9_E5 raw-preserving transfer --------------------------------------------------------
// RGB9_E5 packs three 9-bit mantissas against one shared 5-bit exponent, so a value has several
// legal encodings (shift the exponent up, shift every mantissa down). The spec's encode algorithm
// (GL 4.6 8.5.2) always emits the canonical one, which makes decode-to-float / re-encode
// value-preserving but NOT bit-preserving. glTexImage followed by glGetTexImage has to hand the
// application its own bits back, so a client (format, type) whose word already IS the storage word
// must move verbatim. GL CTS KHR-GL43.copy_image caught the round trip turning the uploaded
// 0xf8fc0000 into 0xe7e00000 ("CopyImageSubData modified contents of source image") and a copied-in
// 0x60000000 into 0x00000000 ("CopyImageSubData stored invalid data in copied region").
namespace {
Uint32 RoundTripSharedExponentWord(Uint32 word) {
Float rgb[3];
MG_Util::DecodeSharedExponentRGB9E5(word, rgb);
return MG_Util::EncodeSharedExponentRGB9E5(rgb);
}
} // namespace
TEST(SharedExponentRGB9E5Test, EncodeReproducesCanonicalWordsExactly) {
// Canonical encodings - the ones the spec algorithm emits - must survive a decode/encode round
// trip untouched, or every conversion INTO RGB9_E5 would be off as well.
const Uint32 canonical[] = {
0x00000000u, // all zero
0x0FFFFFFFu, // exponent 1, every mantissa saturated (smallest normalized exponent in use)
0x000003FFu, // exponent 0: the denormal range, mantissas 511 / 1 / 0
0x81010100u, // (1.0, 0.5, 0.25)
0xE7E00000u, // (0, 0, 8064) - what the CTS round trip produced
0xFFFFFFFFu, // exponent 31 with saturated mantissas = the largest representable texel
};
for (const Uint32 word : canonical) {
EXPECT_EQ(RoundTripSharedExponentWord(word), word) << "word 0x" << std::hex << word;
// Encoding is idempotent: a second pass may not drift either.
EXPECT_EQ(RoundTripSharedExponentWord(RoundTripSharedExponentWord(word)), word);
}
}
TEST(SharedExponentRGB9E5Test, EncodeCanonicalizesRedundantWords) {
// The exact QPA signatures. Both pairs hold the same value, so the encoder is not wrong - which
// is why the fix has to be a raw path rather than an encoder change.
Float observed[3];
MG_Util::DecodeSharedExponentRGB9E5(0xF8FC0000u, observed);
Float canonical[3];
MG_Util::DecodeSharedExponentRGB9E5(0xE7E00000u, canonical);
EXPECT_EQ(observed[2], 8064.0f);
EXPECT_EQ(canonical[2], 8064.0f);
EXPECT_EQ(RoundTripSharedExponentWord(0xF8FC0000u), 0xE7E00000u);
// Exponent 12 with all-zero mantissas is still the value zero, and canonicalizes to the
// all-zero word.
EXPECT_EQ(RoundTripSharedExponentWord(0x60000000u), 0x00000000u);
// Mantissa 1 at exponent 1 renormalizes down into the denormal range.
EXPECT_EQ(RoundTripSharedExponentWord(0x08000001u), 0x00000002u);
}
TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts) {
using MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer;
// The four pairs whose client word is bit-identical to the packed storage word.
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::R11FG11FB10F, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt101111Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2UI, TextureInputFormat::RGBAInteger,
TexturePixelDataType::UnsignedInt2101010Rev));
// A different packed float layout of the same width is still a conversion.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt101111Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::R11FG11FB10F, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
// So is a component client type, or the same word against a component internal format.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::Float));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB8, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGBA32F, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
// Integerness has to line up too: the normalized and integer 10/10/10/2 words are not the
// same client layout even though they are the same bit field.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2, TextureInputFormat::RGBAInteger,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2UI, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::Unknown, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
}
TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) {
// Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client
// words untouched, including the redundant encodings the CTS generates.
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint32 words[] = {0xF8FC0000u, 0x60000000u, 0x08000001u, 0x0FFFFFFFu};
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 4, 1, 0, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, words);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
Uint32 readBack[4] = {};
std::memcpy(readBack, stored, sizeof(readBack));
for (Int i = 0; i < 4; ++i) {
EXPECT_EQ(readBack[i], words[i]) << "texel " << i;
}
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, TexImage2DRGB9E5FromOtherPackedFloatTypeStillConverts) {
// Negative control for the raw path: a genuinely different client layout keeps the
// decode-to-float / re-encode conversion.
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
// 10F_11F_11F_REV word holding (1.0, 0.5, 0.25) - see the packed readback encode tests.
const Uint32 packedFloatWord = 0x681C03C0u;
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 1, 1, 0, GL_RGB, GL_UNSIGNED_INT_10F_11F_11F_REV,
&packedFloatWord);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
Uint32 word = 0;
std::memcpy(&word, stored, sizeof(word));
const Float rgb[3] = {1.0f, 0.5f, 0.25f};
EXPECT_EQ(word, MG_Util::EncodeSharedExponentRGB9E5(rgb));
EXPECT_NE(word, packedFloatWord) << "the raw path must not swallow a real conversion";
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams) {
// Readback direction: the raw store copies the words bit-for-bit while still honoring the
// client-side PACK addressing (alignment, skip rows/pixels) and GL_PACK_SWAP_BYTES.
namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl;
const Uint32 source[] = {0xF8FC0000u, 0x60000000u, 0x08000001u, // row 0
0x0FFFFFFFu, 0xFFFFFFFFu, 0x00000000u}; // row 1
constexpr Uint32 kFill = 0xDEADBEEFu;
Uint32 destination[16];
std::fill(std::begin(destination), std::end(destination), kFill);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 8); // rows of 3 words (12 B) pad to 16 B
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 1);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 1);
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
/*sliceHeight=*/2, /*sliceCount=*/1,
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
/*applyPackImageParams=*/false));
// Row 0 lands at SKIP_ROWS * 16 + SKIP_PIXELS * 4 = 20 bytes = word 5; row 1 one 16-byte
// stride further along, at word 9.
for (Int i = 0; i < 3; ++i) {
EXPECT_EQ(destination[5 + i], source[i]) << "row 0 texel " << i;
EXPECT_EQ(destination[9 + i], source[3 + i]) << "row 1 texel " << i;
}
// The skipped region and the row padding stay untouched.
EXPECT_EQ(destination[0], kFill);
EXPECT_EQ(destination[4], kFill);
EXPECT_EQ(destination[8], kFill);
EXPECT_EQ(destination[12], kFill);
// GL_PACK_SWAP_BYTES reverses each 4-byte word.
std::fill(std::begin(destination), std::end(destination), kFill);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 0);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 0);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 1);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE);
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
/*sliceHeight=*/1, /*sliceCount=*/1,
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
/*applyPackImageParams=*/false));
EXPECT_EQ(destination[0], 0x0000FCF8u); // byte-reversed 0xF8FC0000
EXPECT_EQ(destination[1], 0x00000060u);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 4);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core table 23.18: GL_TEXTURE_COMPARE_FUNC takes the whole eight-function depth-compare
// range. The validator used to start it at GL_LEQUAL, which sits in the middle of the contiguous
// GL_NEVER..GL_ALWAYS block, so NEVER/LESS/EQUAL were rejected while GREATER/NOTEQUAL/GEQUAL only
@@ -3752,3 +4033,306 @@ TEST_F(TextureTest, TexStorage2DLeavesAGenericCompressedFormatUncompressed) {
EXPECT_EQ(compressed, GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// ===================== glCopyImageSubData validation (KHR-GL43.copy_image) =====================
//
// Every case below is a mechanism the conformance group caught in the field, and each one is
// pinned here because the backend cannot: a wrongly ACCEPTED copy shows up only as wrong pixels
// on a device, and a wrongly REJECTED one shows up only as a conformance failure.
namespace {
struct CopyImageSubDataCall {
Bool Called = false;
GLenum SrcTarget = GL_NONE;
GLenum DstTarget = GL_NONE;
GLint SrcZ = -1;
GLint DstZ = -1;
GLsizei Depth = -1;
} g_copyImageSubDataCall;
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
GLsizei srcHeight, GLsizei srcDepth) {
(void)srcTexture;
(void)srcLevel;
(void)srcX;
(void)srcY;
(void)dstTexture;
(void)dstLevel;
(void)dstX;
(void)dstY;
(void)srcWidth;
(void)srcHeight;
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth};
}
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
// call in every respect except the one the test is about.
void MakeCopyImagePair(GLenum srcFormat, GLenum dstFormat, GLuint& srcTexture, GLuint& dstTexture,
GLsizei levels = 1, GLsizei extent = 8) {
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &srcTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &dstTexture);
MG_Impl::GLImpl::TextureStorage2D(srcTexture, levels, srcFormat, extent, extent);
MG_Impl::GLImpl::TextureStorage2D(dstTexture, levels, dstFormat, extent, extent);
}
} // namespace
// GL 4.6 core 18.3.2 compatibility is texel-block SIZE, not base internal format. RGB10_A2 and
// R11F_G11F_B10F are both 32-bit and their bases differ (RGBA vs RGB); the old exact-base-format
// predicate rejected the pair, which is what took down the whole cross-format half of the
// conformance matrix on both backends.
TEST_F(TextureTest, CopyImageSubDataAcceptsEqualTexelSizeAcrossDifferentBaseFormats) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGB10_A2, GL_R11F_G11F_B10F, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The other half of the same rule: equal base format is not sufficient either. RGBA8 and RGBA32F
// are both RGBA and 32 vs 128 bits, so the copy is illegal.
TEST_F(TextureTest, CopyImageSubDataRejectsDifferentTexelSizesWithTheSameBaseFormat) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA8, GL_RGBA32F, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
4, 4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// ...and the pairing that is legal purely because the sizes agree, across integer-ness too.
TEST_F(TextureTest, CopyImageSubDataAcceptsIntegerAndFloatOfTheSameTexelSize) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA32UI, GL_RGBA32F, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// 18.3.2 spells a name that is not an object INVALID_VALUE. The shared texture-object validator
// says INVALID_OPERATION, which is right for the entry points that reach an object through a
// BINDING - hence a rule local to this entry point rather than a change to the helper.
TEST_F(TextureTest, CopyImageSubDataNonExistentNameIsInvalidValue) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(4242, GL_TEXTURE_2D, 0, 0, 0, 0, 4243, GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// A target that disagrees with the object it names is INVALID_ENUM, not the INVALID_OPERATION the
// shared target-uniformity validator records for the upload paths.
TEST_F(TextureTest, CopyImageSubDataTargetNotMatchingTheObjectIsInvalidEnum) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D_ARRAY, 0, 0,
0, 0, 1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_ENUM);
}
// The eleven whole-image targets only: a cube FACE converts to a target the frontend knows, so the
// generic target validator lets it through, but 18.3.2 does not accept it here.
TEST_F(TextureTest, CopyImageSubDataRejectsTargetsOutsideTheSpecList) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0, 0, 0, dstTexture,
GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_ENUM);
}
// A level the image does not have is INVALID_VALUE; a single-level texture asked for level 1 used
// to reach the backend with whatever the storage layer answered for that level.
TEST_F(TextureTest, CopyImageSubDataRejectsLevelTheImageDoesNotHave) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 1, 0, 0, 0,
1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// Sample counts must match. A single-sample image reports zero, so this same comparison is also
// what refuses a copy between a multisample target and a non-multisample one.
TEST_F(TextureTest, CopyImageSubDataRejectsSampleCountMismatch) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
// Two DIFFERENT counts are the whole point, so the case needs a context that can actually
// create multisample storage - which this unit-test binary, with no backend behind the
// renderable-format and sample-count queries, may not be able to. The precondition is
// checked on the state objects rather than assumed, so this can only ever skip or test the
// real rule; it can never pass vacuously.
GLint maxSamples = 1;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &maxSamples);
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstTexture);
MG_Impl::GLImpl::TextureStorage2DMultisample(srcTexture, 1, GL_RGBA8, 8, 8, GL_FALSE);
MG_Impl::GLImpl::TextureStorage2DMultisample(dstTexture, std::max(maxSamples, 2), GL_RGBA8, 8, 8, GL_FALSE);
DrainPendingGlErrors();
const Int srcSamples = MG_State::pGLContext->GetTextureObject(srcTexture)->GetSamples();
const Int dstSamples = MG_State::pGLContext->GetTextureObject(dstTexture)->GetSamples();
if (srcSamples == dstSamples) {
GTEST_SKIP() << "this context could not give the two textures different sample counts (both " << srcSamples
<< "); nothing for the rule to reject";
}
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstTexture,
GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// The layer range has to survive the frontend intact. Both backends used to drop it - DirectVulkan
// pinned baseArrayLayer/layerCount at 0/1 - so a 12-layer copy moved one layer and said nothing;
// this pins the frontend half of that contract.
TEST_F(TextureTest, CopyImageSubDataForwardsTheWholeLayerRangeToTheBackend) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &srcTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &dstTexture);
MG_Impl::GLImpl::TextureStorage3D(srcTexture, 1, GL_RGBA8, 8, 8, 12);
MG_Impl::GLImpl::TextureStorage3D(dstTexture, 1, GL_RGBA8, 8, 8, 12);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, dstTexture, GL_TEXTURE_2D_ARRAY,
0, 0, 0, 5, 4, 4, 7);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(g_copyImageSubDataCall.SrcZ, 2);
EXPECT_EQ(g_copyImageSubDataCall.DstZ, 5);
EXPECT_EQ(g_copyImageSubDataCall.Depth, 7);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The shape KHR-GL43.copy_image.invalid_object ends on once the invalid-name cases are answered
// correctly: two ordinary glTexImage2D textures, no storage object, one texel copied from the
// origin. Nothing about it is exotic, which is exactly why it is worth a case of its own - every
// rule added to this validator is a new way to reject it.
TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MG_Impl::GLImpl::GenTextures(1, &srcTexture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, srcTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
MG_Impl::GLImpl::GenTextures(1, &dstTexture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, dstTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
1, 1, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// ...and again after the names have been through a delete/regenerate cycle, which is what the
// conformance case does between its sub-cases: it deletes an object to make it invalid, then
// builds the next pair from names the allocator hands straight back.
MG_Impl::GLImpl::DeleteTextures(1, &srcTexture);
MG_Impl::GLImpl::DeleteTextures(1, &dstTexture);
DrainPendingGlErrors();
g_copyImageSubDataCall = {};
GLuint reusedSrc = 0;
GLuint reusedDst = 0;
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedSrc);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::GenTextures(1, &reusedDst);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedDst);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
1, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// A rectangle target reaches the backend as itself. The translation to the GL_TEXTURE_2D the ES
// driver actually stores it in belongs to DirectGLES, not here - and putting it here would break
// DirectVulkan, which needs the real target to tell an array copy from a flat one.
TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstTexture);
MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 8, 8);
MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstTexture, GL_TEXTURE_RECTANGLE,
0, 0, 0, 0, 4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(g_copyImageSubDataCall.SrcTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
+20
View File
@@ -16,5 +16,25 @@ target_link_libraries(
${LINK_LIBRARIES}
)
# The log-severity ordering and the MOBILEGL_ASSERT gate keyed to it. Links gtest
# (not gtest_main): the suite needs its own main() to point MOBILEGL_LOG_FILE_PATH at a
# temp file before anything in the process logs and latches the sink's FILE*.
add_executable(
LogLevelTest
LogLevelTest.cpp
)
target_include_directories(LogLevelTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
LogLevelTest PRIVATE
GTest::gtest
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(LogLevelTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+204
View File
@@ -0,0 +1,204 @@
// MobileGL - MobileGL/MG_Test/Util/LogLevelTest.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
// Guards the log-severity ordering and the MOBILEGL_ASSERT gate that hangs off it.
//
// Until 2026-08-13 the numeric order was DEBUG < WARN < ERROR < INFO < FATAL, so the
// production gate `#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_X` compiled
// MGLOG_W and MGLOG_E out of every INFO build. Failures logged with MGLOG_E were
// invisible in exactly the builds that shipped. Nothing in the suite noticed, which is
// why this file exists.
//
// This test is written to be meaningful in BOTH configurations - build it at
// MOBILEGL_LOG_LEVEL_INFO and at MOBILEGL_LOG_LEVEL_DEBUG and it checks the contract
// appropriate to each. It runs headless: no GL context, no device, just the file sink.
#include <gtest/gtest.h>
#include <Defines.h>
#include <MG_Util/Debug/Log.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
namespace {
// ---------------------------------------------------------------------------
// Compile-time contract
// ---------------------------------------------------------------------------
// The ordering itself. A renumbering that re-inverts the scale fails here.
static_assert(MOBILEGL_LOG_LEVEL_DEBUG < MOBILEGL_LOG_LEVEL_INFO, "DEBUG must be below INFO");
static_assert(MOBILEGL_LOG_LEVEL_INFO < MOBILEGL_LOG_LEVEL_WARN, "INFO must be below WARN");
static_assert(MOBILEGL_LOG_LEVEL_WARN < MOBILEGL_LOG_LEVEL_ERROR, "WARN must be below ERROR");
static_assert(MOBILEGL_LOG_LEVEL_ERROR < MOBILEGL_LOG_LEVEL_FATAL, "ERROR must be below FATAL");
// DEBUG must stay the floor: the MOBILEGL_ASSERT gate in Defines.h is spelled
// `ACTIVE <= MOBILEGL_LOG_LEVEL_DEBUG` and means "only in a DEBUG build". That
// reading is only correct while DEBUG is the minimum.
static_assert(MOBILEGL_LOG_LEVEL_DEBUG == 0, "DEBUG must be the lowest level");
// Defines.h and Log.h each define the five constants. Log.h's copy wins when both
// are included; if the two ever drift, the duplicate-definition warning is not
// guaranteed to be an error, so pin the values a second time from this TU's view.
static_assert(MOBILEGL_LOG_LEVEL_INFO == 1, "INFO must be 1 in both Defines.h and Log.h");
static_assert(MOBILEGL_LOG_LEVEL_WARN == 2, "WARN must be 2 in both Defines.h and Log.h");
static_assert(MOBILEGL_LOG_LEVEL_ERROR == 3, "ERROR must be 3 in both Defines.h and Log.h");
static_assert(MOBILEGL_LOG_LEVEL_FATAL == 4, "FATAL must be 4 in both Defines.h and Log.h");
// Whether this translation unit was compiled with asserts live. This is a literal
// copy of the Defines.h gate - the point of the test is to prove it agrees with
// MGLOG_D's liveness, observed at runtime below.
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
constexpr bool kAssertsLive = true;
#else
constexpr bool kAssertsLive = false;
#endif
// Whether the build is the production INFO configuration.
constexpr bool kBuiltAtInfo = (MOBILEGL_LOG_ACTIVE_LEVEL == MOBILEGL_LOG_LEVEL_INFO);
// ---------------------------------------------------------------------------
// Runtime observation of the sink
// ---------------------------------------------------------------------------
// The log file path is latched by MG_Util::Debug::InitFile() on the first write and
// never reopened, so the whole process gets one file. main() below points
// MOBILEGL_LOG_FILE_PATH at a temp file before gtest runs; this fixture emits one
// line per level and then reads the file back.
std::string g_logPath;
struct Emitted {
bool debug = false;
bool info = false;
bool warn = false;
bool error = false;
bool fatal = false;
};
Emitted EmitAndRead() {
// Distinctive markers so a substring search cannot collide with unrelated output.
MGLOG_D("MGLOGTEST_MARKER_DEBUG_5f3a");
MGLOG_I("MGLOGTEST_MARKER_INFO_5f3a");
MGLOG_W("MGLOGTEST_MARKER_WARN_5f3a");
MGLOG_E("MGLOGTEST_MARKER_ERROR_5f3a");
MGLOG_F("MGLOGTEST_MARKER_FATAL_5f3a");
std::ifstream in(g_logPath, std::ios::binary);
std::ostringstream ss;
ss << in.rdbuf();
const std::string text = ss.str();
Emitted e;
e.debug = text.find("MGLOGTEST_MARKER_DEBUG_5f3a") != std::string::npos;
e.info = text.find("MGLOGTEST_MARKER_INFO_5f3a") != std::string::npos;
e.warn = text.find("MGLOGTEST_MARKER_WARN_5f3a") != std::string::npos;
e.error = text.find("MGLOGTEST_MARKER_ERROR_5f3a") != std::string::npos;
e.fatal = text.find("MGLOGTEST_MARKER_FATAL_5f3a") != std::string::npos;
return e;
}
TEST(LogLevel, SinkIsReachableAtAll) {
// Guards the test itself: if the file sink were disabled or the path override
// ignored, every "level X is suppressed" assertion below would pass vacuously.
ASSERT_FALSE(g_logPath.empty()) << "test harness did not set MOBILEGL_LOG_FILE_PATH";
const Emitted e = EmitAndRead();
EXPECT_TRUE(e.fatal) << "FATAL is compiled in at every level; an empty log means the "
"file sink never opened and this suite proves nothing";
}
TEST(LogLevel, ProductionBuildKeepsErrorAndWarn) {
if constexpr (!kBuiltAtInfo) {
GTEST_SKIP() << "only meaningful when built at MOBILEGL_LOG_LEVEL_INFO";
} else {
const Emitted e = EmitAndRead();
// The regression this file exists for.
EXPECT_TRUE(e.error) << "MGLOG_E must be live in an INFO build";
EXPECT_TRUE(e.warn) << "MGLOG_W must be live in an INFO build";
EXPECT_TRUE(e.info) << "MGLOG_I must be live in an INFO build";
EXPECT_TRUE(e.fatal) << "MGLOG_F must be live in an INFO build";
// ...and the other half: D must still be compiled out, or production pays
// for every dev-only diagnostic in the tree.
EXPECT_FALSE(e.debug) << "MGLOG_D must be compiled out of an INFO build";
}
}
TEST(LogLevel, DebugBuildKeepsEverything) {
if constexpr (MOBILEGL_LOG_ACTIVE_LEVEL != MOBILEGL_LOG_LEVEL_DEBUG) {
GTEST_SKIP() << "only meaningful when built at MOBILEGL_LOG_LEVEL_DEBUG";
} else {
const Emitted e = EmitAndRead();
EXPECT_TRUE(e.debug);
EXPECT_TRUE(e.info);
EXPECT_TRUE(e.warn);
EXPECT_TRUE(e.error);
EXPECT_TRUE(e.fatal);
}
}
// ---------------------------------------------------------------------------
// The assert contract
// ---------------------------------------------------------------------------
TEST(LogLevel, AssertGateTracksDebugLiveness) {
// The contract: "INFO builds: asserts OFF; DEBUG builds: asserts ON". Stated
// without naming a level, that is exactly "asserts are live iff MGLOG_D is
// live" - which is checkable in whichever configuration this was built in,
// and is what makes the renumbering safe.
const Emitted e = EmitAndRead();
EXPECT_EQ(kAssertsLive, e.debug)
<< "MOBILEGL_ASSERT liveness (" << kAssertsLive << ") disagrees with MGLOG_D liveness ("
<< e.debug << "). The Defines.h assert gate and the Log.h MGLOG_D gate have drifted.";
}
TEST(LogLevel, AssertIsCompiledOutOfProductionBuilds) {
if constexpr (kAssertsLive) {
GTEST_SKIP() << "asserts are live in this configuration; see AssertIsLiveInDebugBuilds";
} else {
// If MOBILEGL_ASSERT were live here this would TRAP and take the process
// down, which is the behavioural half of the contract.
MOBILEGL_ASSERT(false, "this assert must be compiled out at %s", "INFO");
SUCCEED();
}
}
TEST(LogLevel, AssertIsLiveInDebugBuilds) {
if constexpr (!kAssertsLive) {
GTEST_SKIP() << "asserts are compiled out in this configuration";
} else {
// A satisfied assert must be a no-op rather than a trap; that it expands to
// real code at all is what kAssertsLive already established.
MOBILEGL_ASSERT(true, "a satisfied assert must not trap");
SUCCEED();
}
}
} // namespace
int main(int argc, char** argv) {
// Must happen before anything logs: MG_Util::Debug::InitFile() reads the variable
// once, on the first write, and caches the FILE*.
namespace fs = std::filesystem;
const fs::path path = fs::temp_directory_path() / "mobilegl-loglevel-test.log";
std::error_code ec;
fs::remove(path, ec);
g_logPath = path.string();
#if defined(_WIN32)
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
#else
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
#endif
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+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;
@@ -912,6 +913,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
caps.SupportsNoperspectiveInterpolation = true;
}
if (std::strcmp(extension, "GL_NV_image_formats") == 0) {
caps.SupportsExtendedImageFormats = true;
}
if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) {
caps.SupportsShaderMultisampleInterpolation = true;
}
@@ -983,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);
@@ -1138,6 +1138,14 @@ namespace MobileGL {
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
Bool SupportsNoperspectiveInterpolation = false;
// GL_NV_image_formats is present: the driver accepts the image format qualifiers GL
// has and GLSL ES core does not (the one- and two-channel formats, the 16-bit and
// snorm ones - r8ui, rg16f, rgba16 and the rest of GL table 8.26). GLSL ES core has
// only thirteen, so without this an image whose bound format is outside that set has
// no legal spelling in the generated ESSL at all, and the directive must not be
// emitted either - `#extension` on a name the driver does not advertise is itself a
// compile error.
Bool SupportsExtendedImageFormats = false;
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
// interpolateAtOffset and the three fragment-offset limit queries.
Bool SupportsShaderMultisampleInterpolation = false;
@@ -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;
}
@@ -27,6 +27,7 @@
#include "SpirvPasses/ZeroBaseVertexPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/Lower1DArrayImagesPass.h"
#include "SpirvPasses/BakeImageFormatsPass.h"
#include "SpirvPasses/PrivateToEntryLocalPass.h"
#include "SpirvPasses/StripUniformLocationsPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
@@ -462,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);
@@ -485,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()));
@@ -686,6 +686,35 @@ namespace MobileGL {
outputBinary);
}
bool ShaderCompiler::BakeImageFormatsForEssl(const Vector<Uint32>& inputBinary,
const UnorderedMap<String, Uint>& glFormatByName,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
if (glFormatByName.empty()) return false;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(BakeImageFormatsPass::CreateBakeImageFormatsPass(glFormatByName));
return RunOptimizerChecked("BakeImageFormatsForEssl", optimizer, inputBinary, outputBinary);
}
bool ShaderCompiler::DeclaresFormatlessStorageImage(const Vector<Uint32>& binary) {
return BakeImageFormatsPass::DeclaresFormatlessStorageImage(binary);
}
bool ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(Uint glInternalFormat) {
return BakeImageFormatsPass::IsCoreEsslImageFormat(
BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat));
}
String ShaderCompiler::EsslImageFormatSpelling(Uint glInternalFormat) {
return BakeImageFormatsPass::EsslSpellingOfGLInternalFormat(glInternalFormat);
}
bool ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat) {
return BakeImageFormatsPass::IsSpirvCrossEsslPrintableFormat(
BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat));
}
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
@@ -807,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;
@@ -836,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
@@ -849,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;
@@ -97,6 +97,32 @@ namespace MobileGL {
// but a handful. See Lower1DArrayImagesPass for what it declines and why.
static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Gives each format-less storage image the format bound to its image unit, so
// the emitted ESSL can carry the format layout qualifier GLSL ES requires of
// every image and desktop GLSL lets a writeonly declaration omit. `glFormatByName`
// maps uniform name to the glBindImageTexture format of the unit it addresses.
// DirectGLES transpile path only - Vulkan takes an Unknown-format storage image
// natively. See BakeImageFormatsPass for what it declines and why.
static bool BakeImageFormatsForEssl(const Vector<Uint32>& inputBinary,
const UnorderedMap<String, Uint>& glFormatByName,
Vector<uint32_t>& outputBinary);
// Whether the module declares a storage image with no format qualifier at all,
// i.e. whether BakeImageFormatsForEssl could change anything. One module parse,
// so the ~every shader that declares none pays no optimizer run.
static bool DeclaresFormatlessStorageImage(const Vector<Uint32>& binary);
// Whether the GL internal format's image-format spelling is one GLSL ES has in
// core. False both for a format ES only reaches through GL_NV_image_formats and
// for one with no image-format spelling at all, so a caller that has to decide
// whether to emit the extension directive can ask this one question.
static bool GLInternalFormatIsCoreEsslImageFormat(Uint glInternalFormat);
// The ESSL layout-qualifier spelling of a GL internal format ("r8ui", "rgba32f"),
// empty when the format has no image-format spelling at all.
static String EsslImageFormatSpelling(Uint glInternalFormat);
// Whether SPIRV-Cross will print that format when it targets ESSL. It throws on
// the ones it calls desktop-only - taking the whole stage with it - so a caller
// must not ask BakeImageFormatsForEssl for those, and completes them in the
// emitted text instead.
static bool SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Builds the non-indexed-draw variant of a vertex shader: every gl_BaseVertex
@@ -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)" : "");
}
}
@@ -0,0 +1,706 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.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 "BakeImageFormatsPass.h"
#include "spirv.hpp"
#include "source/opt/build_module.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <map>
#include <memory>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
// 5 Sampled, 6 Format, (7 Access Qualifier - Kernel only, never present here).
constexpr uint32_t kImageSampledTypeOperand = 0;
constexpr uint32_t kImageSampledOperand = 5;
constexpr uint32_t kImageFormatOperand = 6;
// A storage image, i.e. one reached through imageLoad/imageStore rather than a
// sampler. The only kind that has a format qualifier in any GLSL dialect.
constexpr uint32_t kSampledStorageImage = 2;
// OpTypePointer in-operands: 0 storage class, 1 pointee.
constexpr uint32_t kPointerStorageClassOperand = 0;
constexpr uint32_t kPointerPointeeOperand = 1;
// OpTypeArray in-operands: 0 element type, 1 length.
constexpr uint32_t kArrayElementOperand = 0;
bool IsFormatlessStorageImageType(const Instruction* type) {
return type != nullptr && type->opcode() == spv::Op::OpTypeImage &&
type->GetSingleWordInOperand(kImageSampledOperand) == kSampledStorageImage &&
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand)) ==
spv::ImageFormat::Unknown;
}
// The three component classes a format layout qualifier can have. spirv-val
// requires the Image Format's class to agree with the OpTypeImage's Sampled
// Type, so a bake that disagrees would produce an invalid module rather than a
// merely wrong one.
enum class ComponentClass { Float, SignedInt, UnsignedInt, None };
ComponentClass ClassOfImageFormat(spv::ImageFormat format) {
switch (format) {
case spv::ImageFormat::Rgba32f:
case spv::ImageFormat::Rgba16f:
case spv::ImageFormat::R32f:
case spv::ImageFormat::Rgba8:
case spv::ImageFormat::Rgba8Snorm:
case spv::ImageFormat::Rg32f:
case spv::ImageFormat::Rg16f:
case spv::ImageFormat::R11fG11fB10f:
case spv::ImageFormat::R16f:
case spv::ImageFormat::Rgba16:
case spv::ImageFormat::Rgb10A2:
case spv::ImageFormat::Rg16:
case spv::ImageFormat::Rg8:
case spv::ImageFormat::R16:
case spv::ImageFormat::R8:
case spv::ImageFormat::Rgba16Snorm:
case spv::ImageFormat::Rg16Snorm:
case spv::ImageFormat::Rg8Snorm:
case spv::ImageFormat::R16Snorm:
case spv::ImageFormat::R8Snorm:
return ComponentClass::Float;
case spv::ImageFormat::Rgba32i:
case spv::ImageFormat::Rgba16i:
case spv::ImageFormat::Rgba8i:
case spv::ImageFormat::R32i:
case spv::ImageFormat::Rg32i:
case spv::ImageFormat::Rg16i:
case spv::ImageFormat::Rg8i:
case spv::ImageFormat::R16i:
case spv::ImageFormat::R8i:
return ComponentClass::SignedInt;
case spv::ImageFormat::Rgba32ui:
case spv::ImageFormat::Rgba16ui:
case spv::ImageFormat::Rgba8ui:
case spv::ImageFormat::R32ui:
case spv::ImageFormat::Rgb10a2ui:
case spv::ImageFormat::Rg32ui:
case spv::ImageFormat::Rg16ui:
case spv::ImageFormat::Rg8ui:
case spv::ImageFormat::R16ui:
case spv::ImageFormat::R8ui:
return ComponentClass::UnsignedInt;
default:
return ComponentClass::None;
}
}
ComponentClass ClassOfSampledType(IRContext* context, uint32_t sampledTypeId) {
const Instruction* sampledType = context->get_def_use_mgr()->GetDef(sampledTypeId);
if (sampledType == nullptr) return ComponentClass::None;
if (sampledType->opcode() == spv::Op::OpTypeFloat) return ComponentClass::Float;
if (sampledType->opcode() == spv::Op::OpTypeInt) {
// OpTypeInt in-operands: 0 width, 1 signedness.
return sampledType->GetSingleWordInOperand(1) != 0 ? ComponentClass::SignedInt
: ComponentClass::UnsignedInt;
}
return ComponentClass::None;
}
// The image type at the end of a UniformConstant variable's type chain, plus the
// links along the way. Anything that is not `pointer -> [array ->] image` comes
// back with a null image and is left alone.
struct ImageTypeChain {
Instruction* pointerType = nullptr; // the variable's own result type
Instruction* arrayType = nullptr; // null when the variable is a single image
Instruction* imageType = nullptr;
};
ImageTypeChain ResolveImageTypeChain(IRContext* context, const Instruction& variable) {
ImageTypeChain chain;
auto* defUseMgr = context->get_def_use_mgr();
Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) {
return chain;
}
chain.pointerType = pointerType;
Instruction* pointee = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(kPointerPointeeOperand));
if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeArray) {
chain.arrayType = pointee;
pointee = defUseMgr->GetDef(pointee->GetSingleWordInOperand(kArrayElementOperand));
}
if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeImage) {
chain.imageType = pointee;
}
return chain;
}
// Instructions that consume an image VALUE (the result of an OpLoad) and need no
// result-type change of their own: the texel type they yield is independent of
// the format operand.
bool ConsumesImageValueWithoutRetyping(spv::Op opcode) {
switch (opcode) {
case spv::Op::OpImageWrite:
case spv::Op::OpImageRead:
case spv::Op::OpImageSparseRead:
case spv::Op::OpImageQuerySize:
case spv::Op::OpImageQuerySizeLod:
case spv::Op::OpImageQuerySamples:
case spv::Op::OpImageQueryLevels:
case spv::Op::OpImageQueryFormat:
case spv::Op::OpImageQueryOrder:
return true;
default:
return false;
}
}
// Debug/annotation instructions name an id without depending on its type.
bool IsTypeAgnosticReference(spv::Op opcode) {
switch (opcode) {
case spv::Op::OpName:
case spv::Op::OpMemberName:
case spv::Op::OpDecorate:
case spv::Op::OpDecorateId:
case spv::Op::OpDecorateString:
case spv::Op::OpMemberDecorate:
case spv::Op::OpEntryPoint:
return true;
default:
return false;
}
}
} // namespace
Uint32 BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(Uint glInternalFormat) {
switch (glInternalFormat) {
// The GL 4.2 image format table (core spec table 8.26), in its order. Written as
// literals rather than through the GL headers because this lives in MG_Util,
// which the GL frontend's enums do not reach.
case 0x8814: /*GL_RGBA32F*/ return static_cast<Uint32>(spv::ImageFormat::Rgba32f);
case 0x881A: /*GL_RGBA16F*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16f);
case 0x8230: /*GL_RG32F*/ return static_cast<Uint32>(spv::ImageFormat::Rg32f);
case 0x822F: /*GL_RG16F*/ return static_cast<Uint32>(spv::ImageFormat::Rg16f);
case 0x8C3A: /*GL_R11F_G11F_B10F*/ return static_cast<Uint32>(spv::ImageFormat::R11fG11fB10f);
case 0x822E: /*GL_R32F*/ return static_cast<Uint32>(spv::ImageFormat::R32f);
case 0x822D: /*GL_R16F*/ return static_cast<Uint32>(spv::ImageFormat::R16f);
case 0x8D70: /*GL_RGBA32UI*/ return static_cast<Uint32>(spv::ImageFormat::Rgba32ui);
case 0x8D76: /*GL_RGBA16UI*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16ui);
case 0x8D7C: /*GL_RGBA8UI*/ return static_cast<Uint32>(spv::ImageFormat::Rgba8ui);
case 0x906F: /*GL_RGB10_A2UI*/ return static_cast<Uint32>(spv::ImageFormat::Rgb10a2ui);
case 0x823C: /*GL_RG32UI*/ return static_cast<Uint32>(spv::ImageFormat::Rg32ui);
case 0x823A: /*GL_RG16UI*/ return static_cast<Uint32>(spv::ImageFormat::Rg16ui);
case 0x8238: /*GL_RG8UI*/ return static_cast<Uint32>(spv::ImageFormat::Rg8ui);
case 0x8236: /*GL_R32UI*/ return static_cast<Uint32>(spv::ImageFormat::R32ui);
case 0x8234: /*GL_R16UI*/ return static_cast<Uint32>(spv::ImageFormat::R16ui);
case 0x8232: /*GL_R8UI*/ return static_cast<Uint32>(spv::ImageFormat::R8ui);
case 0x8D82: /*GL_RGBA32I*/ return static_cast<Uint32>(spv::ImageFormat::Rgba32i);
case 0x8D88: /*GL_RGBA16I*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16i);
case 0x8D8E: /*GL_RGBA8I*/ return static_cast<Uint32>(spv::ImageFormat::Rgba8i);
case 0x823B: /*GL_RG32I*/ return static_cast<Uint32>(spv::ImageFormat::Rg32i);
case 0x8239: /*GL_RG16I*/ return static_cast<Uint32>(spv::ImageFormat::Rg16i);
case 0x8237: /*GL_RG8I*/ return static_cast<Uint32>(spv::ImageFormat::Rg8i);
case 0x8235: /*GL_R32I*/ return static_cast<Uint32>(spv::ImageFormat::R32i);
case 0x8233: /*GL_R16I*/ return static_cast<Uint32>(spv::ImageFormat::R16i);
case 0x8231: /*GL_R8I*/ return static_cast<Uint32>(spv::ImageFormat::R8i);
case 0x8058: /*GL_RGBA8*/ return static_cast<Uint32>(spv::ImageFormat::Rgba8);
case 0x805B: /*GL_RGBA16*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16);
case 0x8059: /*GL_RGB10_A2*/ return static_cast<Uint32>(spv::ImageFormat::Rgb10A2);
case 0x822B: /*GL_RG8*/ return static_cast<Uint32>(spv::ImageFormat::Rg8);
case 0x822C: /*GL_RG16*/ return static_cast<Uint32>(spv::ImageFormat::Rg16);
case 0x8229: /*GL_R8*/ return static_cast<Uint32>(spv::ImageFormat::R8);
case 0x822A: /*GL_R16*/ return static_cast<Uint32>(spv::ImageFormat::R16);
case 0x8F97: /*GL_RGBA8_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::Rgba8Snorm);
case 0x8F9B: /*GL_RGBA16_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16Snorm);
case 0x8F95: /*GL_RG8_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::Rg8Snorm);
case 0x8F99: /*GL_RG16_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::Rg16Snorm);
case 0x8F94: /*GL_R8_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::R8Snorm);
case 0x8F98: /*GL_R16_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::R16Snorm);
default:
return static_cast<Uint32>(spv::ImageFormat::Unknown);
}
}
bool BakeImageFormatsPass::IsCoreEsslImageFormat(Uint32 spirvImageFormat) {
// GLSL ES 3.1 / 3.2, table "Image Formats". Everything else in the GL table
// exists on ES only through GL_NV_image_formats.
switch (static_cast<spv::ImageFormat>(spirvImageFormat)) {
case spv::ImageFormat::Rgba32f:
case spv::ImageFormat::Rgba16f:
case spv::ImageFormat::R32f:
case spv::ImageFormat::Rgba8:
case spv::ImageFormat::Rgba8Snorm:
case spv::ImageFormat::Rgba32i:
case spv::ImageFormat::Rgba16i:
case spv::ImageFormat::Rgba8i:
case spv::ImageFormat::R32i:
case spv::ImageFormat::Rgba32ui:
case spv::ImageFormat::Rgba16ui:
case spv::ImageFormat::Rgba8ui:
case spv::ImageFormat::R32ui:
return true;
default:
return false;
}
}
bool BakeImageFormatsPass::IsSpirvCrossEsslPrintableFormat(Uint32 spirvImageFormat) {
// Mirrors SPIRV-Cross's Compiler::is_desktop_only_format (spirv_cross.cpp), which
// CompilerGLSL::format_to_glsl consults before printing: for ESSL output it throws
// on these instead of emitting a token, and the throw takes the whole stage with
// it. NOT the same set as "outside GLSL ES core" - SPIRV-Cross is happy to print
// rg32f, rg16f and the rest of the two-channel 16/32-bit formats for ES, which
// core ES does not have either. Kept as its own list for that reason: the
// question here is what the emitter will do, not what the language allows.
switch (static_cast<spv::ImageFormat>(spirvImageFormat)) {
case spv::ImageFormat::R11fG11fB10f:
case spv::ImageFormat::R16f:
case spv::ImageFormat::Rgb10A2:
case spv::ImageFormat::R8:
case spv::ImageFormat::Rg8:
case spv::ImageFormat::R16:
case spv::ImageFormat::Rg16:
case spv::ImageFormat::Rgba16:
case spv::ImageFormat::R16Snorm:
case spv::ImageFormat::Rg16Snorm:
case spv::ImageFormat::Rgba16Snorm:
case spv::ImageFormat::R8Snorm:
case spv::ImageFormat::Rg8Snorm:
case spv::ImageFormat::R8ui:
case spv::ImageFormat::Rg8ui:
case spv::ImageFormat::R16ui:
case spv::ImageFormat::Rgb10a2ui:
case spv::ImageFormat::R8i:
case spv::ImageFormat::Rg8i:
case spv::ImageFormat::R16i:
return false;
case spv::ImageFormat::Unknown:
return false;
default:
return true;
}
}
String BakeImageFormatsPass::EsslSpellingOfGLInternalFormat(Uint glInternalFormat) {
switch (static_cast<spv::ImageFormat>(SpirvImageFormatFromGLInternalFormat(glInternalFormat))) {
case spv::ImageFormat::Rgba32f: return "rgba32f";
case spv::ImageFormat::Rgba16f: return "rgba16f";
case spv::ImageFormat::R32f: return "r32f";
case spv::ImageFormat::Rgba8: return "rgba8";
case spv::ImageFormat::Rgba8Snorm: return "rgba8_snorm";
case spv::ImageFormat::Rg32f: return "rg32f";
case spv::ImageFormat::Rg16f: return "rg16f";
case spv::ImageFormat::R11fG11fB10f: return "r11f_g11f_b10f";
case spv::ImageFormat::R16f: return "r16f";
case spv::ImageFormat::Rgba16: return "rgba16";
case spv::ImageFormat::Rgb10A2: return "rgb10_a2";
case spv::ImageFormat::Rg16: return "rg16";
case spv::ImageFormat::Rg8: return "rg8";
case spv::ImageFormat::R16: return "r16";
case spv::ImageFormat::R8: return "r8";
case spv::ImageFormat::Rgba16Snorm: return "rgba16_snorm";
case spv::ImageFormat::Rg16Snorm: return "rg16_snorm";
case spv::ImageFormat::Rg8Snorm: return "rg8_snorm";
case spv::ImageFormat::R16Snorm: return "r16_snorm";
case spv::ImageFormat::R8Snorm: return "r8_snorm";
case spv::ImageFormat::Rgba32i: return "rgba32i";
case spv::ImageFormat::Rgba16i: return "rgba16i";
case spv::ImageFormat::Rgba8i: return "rgba8i";
case spv::ImageFormat::R32i: return "r32i";
case spv::ImageFormat::Rg32i: return "rg32i";
case spv::ImageFormat::Rg16i: return "rg16i";
case spv::ImageFormat::Rg8i: return "rg8i";
case spv::ImageFormat::R16i: return "r16i";
case spv::ImageFormat::R8i: return "r8i";
case spv::ImageFormat::Rgba32ui: return "rgba32ui";
case spv::ImageFormat::Rgba16ui: return "rgba16ui";
case spv::ImageFormat::Rgba8ui: return "rgba8ui";
case spv::ImageFormat::R32ui: return "r32ui";
case spv::ImageFormat::Rgb10a2ui: return "rgb10_a2ui";
case spv::ImageFormat::Rg32ui: return "rg32ui";
case spv::ImageFormat::Rg16ui: return "rg16ui";
case spv::ImageFormat::Rg8ui: return "rg8ui";
case spv::ImageFormat::R16ui: return "r16ui";
case spv::ImageFormat::R8ui: return "r8ui";
default:
return {};
}
}
bool BakeImageFormatsPass::DeclaresFormatlessStorageImage(const Vector<Uint32>& binary) {
std::unique_ptr<IRContext> context = spvtools::BuildModule(
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
binary.data(), binary.size());
if (!context) {
return false;
}
for (const Instruction& type : context->module()->types_values()) {
if (IsFormatlessStorageImageType(&type)) {
return true;
}
}
return false;
}
spvtools::opt::Pass::Status BakeImageFormatsPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
// Cheap gate first: no format-less storage image type, nothing this pass can do,
// and the module is handed back byte-identical.
bool hasFormatlessType = false;
for (const Instruction& type : irContext->types_values()) {
if (IsFormatlessStorageImageType(&type)) {
hasFormatlessType = true;
break;
}
}
if (!hasFormatlessType || m_glFormatByName.empty()) {
return Status::SuccessWithoutChange;
}
// Variable name -> OpName target, built once. OpName is how the frontend's
// reflection and this module agree on which uniform is which; SPIRV-Cross
// preserves it, which is also why the emitted ESSL can be matched by name later.
std::map<uint32_t, const Instruction*> nameById;
for (const Instruction& debugInst : irContext->debugs2()) {
if (debugInst.opcode() == spv::Op::OpName) {
nameById.emplace(debugInst.GetSingleWordInOperand(0), &debugInst);
}
}
struct Candidate {
Instruction* variable = nullptr;
ImageTypeChain chain;
spv::ImageFormat format = spv::ImageFormat::Unknown;
// The access chains and loads that reach the image through this variable,
// collected while validating so the mutation half never has to re-walk.
std::vector<Instruction*> accessChains;
std::vector<Instruction*> loads;
};
std::vector<Candidate> candidates;
for (Instruction& global : irContext->types_values()) {
if (global.opcode() != spv::Op::OpVariable) continue;
if (static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) !=
spv::StorageClass::UniformConstant) {
continue;
}
// An initializer would be a second operand, and the variable is moved behind
// its new type below - which would put it in front of that initializer.
// GLSL never gives a UniformConstant image one; refuse rather than reason.
if (global.NumInOperands() > 1) continue;
const ImageTypeChain chain = ResolveImageTypeChain(irContext, global);
if (!IsFormatlessStorageImageType(chain.imageType)) continue;
const auto nameIt = nameById.find(global.result_id());
if (nameIt == nameById.end()) continue;
const String uniformName = nameIt->second->GetInOperand(1).AsString();
const auto formatIt = m_glFormatByName.find(uniformName);
if (formatIt == m_glFormatByName.end()) continue;
const auto format =
static_cast<spv::ImageFormat>(SpirvImageFormatFromGLInternalFormat(formatIt->second));
if (format == spv::ImageFormat::Unknown) continue;
// SPIRV-Cross THROWS rather than prints for the formats it calls
// desktop-only when targeting ESSL, and a throw loses the whole stage - so
// baking one of those would trade a missing qualifier for a missing shader.
// Those formats are completed in the emitted text instead (see
// PrgramImpl::BakeImageFormatQualifiers); the module is left format-less for
// them, which is exactly the state that pass looks for.
if (!IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format))) continue;
// spirv-val: "Expected Image Format to match Sampled Type". A bind format
// whose class disagrees with the declaration is an application error GL
// leaves undefined; baking it would turn that into an invalid module, so it
// is declined and the image stays format-less.
if (ClassOfImageFormat(format) !=
ClassOfSampledType(irContext,
chain.imageType->GetSingleWordInOperand(kImageSampledTypeOperand))) {
continue;
}
Candidate candidate;
candidate.variable = &global;
candidate.chain = chain;
candidate.format = format;
// Validate every use BEFORE anything is mutated: a shape this pass cannot
// retype end to end has to leave the variable exactly as it found it, and a
// half-retyped module is not something a later decline could undo.
bool rewritable = true;
defUseMgr->ForEachUser(&global, [&](Instruction* user) {
if (!rewritable) return;
if (IsTypeAgnosticReference(user->opcode())) return;
if (user->opcode() == spv::Op::OpAccessChain ||
user->opcode() == spv::Op::OpInBoundsAccessChain) {
// Only an access chain that lands ON the image - i.e. whose result is
// a pointer to the image type this pass is about to replace.
const Instruction* resultType = defUseMgr->GetDef(user->type_id());
if (resultType == nullptr || resultType->opcode() != spv::Op::OpTypePointer ||
resultType->GetSingleWordInOperand(kPointerPointeeOperand) !=
chain.imageType->result_id()) {
rewritable = false;
return;
}
candidate.accessChains.push_back(user);
return;
}
if (user->opcode() == spv::Op::OpLoad) {
candidate.loads.push_back(user);
return;
}
// OpImageTexelPointer is DECLINED, not allowed through. Its result type
// does not depend on the format, but spirv-val requires the image behind
// an atomic to be r32i/r32ui/r32f, so baking any other format here would
// turn a module the validator accepts (Unknown is exempt) into one it
// rejects. GLSL cannot express an atomic on a format-less image anyway -
// the format qualifier is what makes an image atomic legal - so nothing
// reachable is being given up.
rewritable = false;
});
if (!rewritable) continue;
for (SizeT i = 0; i < candidate.accessChains.size() && rewritable; ++i) {
Instruction* accessChain = candidate.accessChains[i];
defUseMgr->ForEachUser(accessChain, [&](Instruction* user) {
if (!rewritable) return;
if (IsTypeAgnosticReference(user->opcode())) return;
if (user->opcode() == spv::Op::OpLoad) {
candidate.loads.push_back(user);
return;
}
rewritable = false;
});
}
if (!rewritable) continue;
for (SizeT i = 0; i < candidate.loads.size() && rewritable; ++i) {
Instruction* load = candidate.loads[i];
if (load->type_id() != chain.imageType->result_id()) {
rewritable = false;
break;
}
defUseMgr->ForEachUser(load, [&](Instruction* user) {
if (!rewritable) return;
if (IsTypeAgnosticReference(user->opcode())) return;
if (ConsumesImageValueWithoutRetyping(user->opcode())) return;
// Everything else - an image handed to a function, copied into a
// local, put in a composite - would need the type change carried
// further than this pass reasons about.
rewritable = false;
});
}
if (!rewritable) continue;
candidates.push_back(Move(candidate));
}
if (candidates.empty()) {
return Status::SuccessWithoutChange;
}
// Type cloning. A new OpTypeImage is built by hand rather than through the type
// manager because the manager always writes the OpTypeImage Access Qualifier
// operand, which is a Kernel-capability operand: emitting it into a Shader module
// is what spirv-val rejects, not what the format change needed.
//
// Each clone is inserted immediately AFTER the instruction it was cloned from.
// That is what keeps the module free of forward references: everything the
// original depended on is already defined above it, and every user of the
// original - the OpVariable among them - is below it.
std::map<std::pair<uint32_t, uint32_t>, uint32_t> cloneCache; // (original type, key) -> id
auto findIdenticalType = [&](const Instruction& candidateType) -> uint32_t {
for (const Instruction& type : irContext->types_values()) {
if (type.opcode() != candidateType.opcode()) continue;
if (type.NumInOperands() != candidateType.NumInOperands()) continue;
bool same = true;
for (uint32_t i = 0; i < type.NumInOperands(); ++i) {
if (type.GetInOperand(i).words != candidateType.GetInOperand(i).words) {
same = false;
break;
}
}
if (same) return type.result_id();
}
return 0;
};
// The later of two instructions in the globals section. Both a new type's
// ORIGINAL and the definition of the operand it was given have to precede it, and
// the second of those can be a type the module declared further down (a join, see
// below) - so the two are compared rather than assumed.
auto laterInGlobals = [&](Instruction* a, Instruction* b) -> Instruction* {
if (a == nullptr) return b;
if (b == nullptr) return a;
Instruction* last = nullptr;
for (Instruction& global : irContext->types_values()) {
if (&global == a || &global == b) last = &global;
}
return last != nullptr ? last : a;
};
// Clones `original`, replacing in-operand `operandIndex` with `value`. Returns an
// EXISTING type id when the module already declares the result: two identical
// type declarations are invalid SPIR-V, and a module that already spells the
// wanted image (say a second uniform declared `layout(r32ui)`) must be JOINED to
// that declaration, not given a second one.
//
// Placement is the other half of staying valid. SPIR-V allows no forward
// reference among types, so a clone goes after whichever of its original and its
// new operand's definition comes last - the join case is exactly where those two
// differ, and putting the clone after the original alone is what left an
// OpVariable naming a pointer type declared below it.
auto cloneTypeWithOperand = [&](Instruction* original, uint32_t operandIndex, uint32_t value,
bool valueIsId) -> uint32_t {
const auto cacheKey = std::make_pair(original->result_id(), value);
const auto cached = cloneCache.find(cacheKey);
if (cached != cloneCache.end()) return cached->second;
std::vector<Operand> operands;
operands.reserve(original->NumInOperands());
for (uint32_t i = 0; i < original->NumInOperands(); ++i) {
operands.push_back(original->GetInOperand(i));
}
auto clone = spvtools::MakeUnique<Instruction>(irContext, original->opcode(), 0,
irContext->TakeNextId(), operands);
if (clone->result_id() == 0) return 0;
clone->SetInOperand(operandIndex, {value});
const uint32_t existing = findIdenticalType(*clone);
if (existing != 0) {
cloneCache.emplace(cacheKey, existing);
return existing;
}
const uint32_t newId = clone->result_id();
// Only an ID operand names a definition the clone has to sit behind. The
// format operand is a LITERAL, and looking it up would resolve some unrelated
// instruction that happens to carry that number as its result id.
Instruction* anchor =
valueIsId ? laterInGlobals(original, defUseMgr->GetDef(value)) : original;
if (anchor == nullptr) return 0;
Instruction* inserted = clone.release();
inserted->InsertAfter(anchor);
defUseMgr->AnalyzeInstDefUse(inserted);
cloneCache.emplace(cacheKey, newId);
return newId;
};
// Types the retype leaves behind. Killed at the end when nothing references them
// any more: a stranded Unknown-format image type is legal SPIR-V but is exactly
// the thing a later reader (this pass's own probe among them) would take for a
// shader that still needs baking.
std::vector<Instruction*> possiblyOrphanedTypes;
bool changed = false;
for (Candidate& candidate : candidates) {
const uint32_t newImageId = cloneTypeWithOperand(candidate.chain.imageType, kImageFormatOperand,
static_cast<uint32_t>(candidate.format),
/*valueIsId=*/false);
if (newImageId == 0) return Status::Failure;
// The pointer-to-image type every access chain and every single-image
// variable resolves through.
uint32_t newPointeeId = newImageId;
if (candidate.chain.arrayType != nullptr) {
newPointeeId = cloneTypeWithOperand(candidate.chain.arrayType, kArrayElementOperand,
newImageId, /*valueIsId=*/true);
if (newPointeeId == 0) return Status::Failure;
}
const uint32_t newVariablePointerId = cloneTypeWithOperand(
candidate.chain.pointerType, kPointerPointeeOperand, newPointeeId, /*valueIsId=*/true);
if (newVariablePointerId == 0) return Status::Failure;
candidate.variable->SetResultType(newVariablePointerId);
defUseMgr->AnalyzeInstUse(candidate.variable);
// ...and move it behind that type, for the same no-forward-reference reason.
// A joined type can live anywhere in the globals section, including below the
// variable that now names it. Safe unconditionally because the only operand a
// UniformConstant OpVariable can have besides its storage class is an
// initializer, and a candidate carrying one was refused above.
if (Instruction* pointerTypeInst = defUseMgr->GetDef(newVariablePointerId);
pointerTypeInst != nullptr) {
candidate.variable->RemoveFromList();
candidate.variable->InsertAfter(pointerTypeInst);
}
for (Instruction* accessChain : candidate.accessChains) {
Instruction* oldResultType = defUseMgr->GetDef(accessChain->type_id());
if (oldResultType == nullptr) return Status::Failure;
const uint32_t newResultType =
cloneTypeWithOperand(oldResultType, kPointerPointeeOperand, newImageId, /*valueIsId=*/true);
if (newResultType == 0) return Status::Failure;
accessChain->SetResultType(newResultType);
defUseMgr->AnalyzeInstUse(accessChain);
possiblyOrphanedTypes.push_back(oldResultType);
}
for (Instruction* load : candidate.loads) {
load->SetResultType(newImageId);
defUseMgr->AnalyzeInstUse(load);
}
// Innermost last, so the sweep below - which only kills what nothing
// references - can free a whole chain in one walk.
possiblyOrphanedTypes.push_back(candidate.chain.pointerType);
if (candidate.chain.arrayType != nullptr) {
possiblyOrphanedTypes.push_back(candidate.chain.arrayType);
}
possiblyOrphanedTypes.push_back(candidate.chain.imageType);
// Every format outside the thirteen the Shader capability covers - the same
// thirteen GLSL ES has in core, which is not a coincidence: both lists are
// the formats Vulkan requires without an optional feature. Baking one of the
// rest without declaring the capability produces a module spirv-val rejects
// ("Operand 8 of TypeImage requires ... StorageImageExtendedFormats"), which
// is how the stencil half's r8ui announced itself.
if (!IsCoreEsslImageFormat(static_cast<Uint32>(candidate.format))) {
irContext->AddCapability(spv::Capability::StorageImageExtendedFormats);
}
changed = true;
}
if (!changed) {
return Status::SuccessWithoutChange;
}
// The types the retype stranded. Ordered outermost-first above and swept in that
// order, so a pointer goes before the image it pointed at and the image is
// unreferenced by the time it is reached. Anything still referenced - by another
// uniform this pass declined, or by an OpName - is simply left.
for (Instruction* orphan : possiblyOrphanedTypes) {
if (orphan == nullptr) continue;
if (defUseMgr->NumUsers(orphan) != 0) continue;
// Later entries may name the same instruction (several candidates sharing a
// type); scrub the duplicates before the pointer goes stale.
for (Instruction*& other : possiblyOrphanedTypes) {
if (other == orphan) other = nullptr;
}
irContext->KillInst(orphan);
}
// StorageImageWriteWithoutFormat / StorageImageReadWithoutFormat are deliberately
// left declared. A capability a module no longer exercises is valid SPIR-V, and
// dropping one is only safe after proving no format-less image is left ANYWHERE -
// including the ones this pass declined - which is a stronger claim than the
// rewrite needs to make.
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken BakeImageFormatsPass::CreateBakeImageFormatsPass(
GLFormatByName glFormatByName) {
return spvtools::Optimizer::PassToken(
spvtools::MakeUnique<BakeImageFormatsPass>(Move(glFormatByName)));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,116 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.h
// 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
#pragma once
#include "spirv-tools/optimizer.hpp"
#include "source/opt/pass.h"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Gives every format-less storage image in the module the format the application
// bound to its image unit, so SPIRV-Cross can print a format layout qualifier ESSL
// demands and desktop GLSL does not.
//
// Desktop GLSL 4.2 lets a `writeonly` (or `readonly`) image declaration omit the
// format qualifier - the access is typeless as far as the shader is concerned:
//
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
//
// GLSL ES has no such relaxation. Every image uniform must carry one, and Adreno
// says so in as many words - "all images have to define layout format" - failing the
// whole program, which is how KHR-GL4x.packed_depth_stencil.stencil_texturing's
// compute half lost its only shader.
//
// The one format that is CORRECT to print is the one glBindImageTexture named for
// that unit: GL requires the shader qualifier, the bind format and the texture's own
// internal format to belong to the same format class, so the bind format is exactly
// what the declaration would have said had it been written out. It is not knowable
// at compile time, only at draw time, which is why this is a bake into the generated
// program rather than a translation: the caller keys its build on the (unit, format)
// pairs and rebuilds when a rebind moves one (BackendProgramObjectImpl,
// MG_Backend/DirectGLES).
//
// Where the bake happens is the OpTypeImage's Image Format operand, before
// SPIRV-Cross runs, rather than in the emitted text: SPIRV-Cross prints the operand
// it is given, so setting it is the whole of the change, and the result stays a
// valid module that spirv-val can still check.
//
// Deliberately narrow, on four axes:
//
// * UNKNOWN formats only. A declared format is authoritative - a `layout(r32ui)`
// image must be read as r32ui whatever the texture behind it is - and this pass
// never overrides one. It is also what keeps the rebuild key at zero for the
// overwhelming majority of programs.
// * STORAGE images (Sampled == 2). A sampled image's format operand must stay
// Unknown; it has no format qualifier in any GLSL dialect.
// * MATCHING component class only. spirv-val requires the Image Format's component
// type to agree with the OpTypeImage's Sampled Type, so a bind format that
// disagrees with the declaration (which GL leaves undefined) is DECLINED rather
// than baked into an invalid module.
// * ESSL only. Vulkan takes an Unknown-format storage image natively given
// shaderStorageImageWriteWithoutFormat, and Magma resolves the view format from
// the same bind state at descriptor time (UniformManager), so the module must
// reach that backend unchanged.
//
// A variable whose uses are not the plain access-chain / load / image-op shape - an
// image passed to a function, stored into a local - is DECLINED individually and
// left format-less, rather than half-retyped into a module no driver would accept.
// The decision is made before anything is mutated, so a decline costs nothing.
class BakeImageFormatsPass final : public spvtools::opt::Pass {
public:
// Uniform NAME to the GL internal format bound to the image unit it addresses
// (the `format` argument of glBindImageTexture). Names are the SPIR-V ones, i.e.
// an array is named once, without a subscript. Formats with no image-format
// spelling, and names the module does not declare, are ignored.
using GLFormatByName = UnorderedMap<String, Uint>;
explicit BakeImageFormatsPass(GLFormatByName glFormatByName)
: m_glFormatByName(Move(glFormatByName)) {}
const char* name() const override { return "mobilegl-bake-image-formats"; }
Status Process() override;
// Whether the module declares a storage image with no format at all, i.e.
// whether running this pass could change anything. Answered from a single parse
// so the caller can skip the optimizer run entirely - which is every shader but
// a handful.
static bool DeclaresFormatlessStorageImage(const Vector<Uint32>& binary);
// The GL internal format's SPIR-V ImageFormat, or 0 (Unknown) when the format
// has no image-format spelling. Exposed for the caller's ESSL-side question of
// whether an extension directive is needed for it.
static Uint32 SpirvImageFormatFromGLInternalFormat(Uint glInternalFormat);
// Whether the SPIR-V ImageFormat is one GLSL ES has in core. The rest exist only
// under GL_NV_image_formats, whose directive the emitted ESSL must then carry.
// (It is also exactly the set that needs no StorageImageExtendedFormats
// capability in the module - both lists are the formats Vulkan requires without
// an optional feature.)
static bool IsCoreEsslImageFormat(Uint32 spirvImageFormat);
// Whether SPIRV-Cross will PRINT the format when it targets ESSL. Its
// is_desktop_only_format set throws instead of emitting, which loses the whole
// stage, so those formats are left for the text-level completion in the backend
// and are never baked into a module bound for SPIRV-Cross. A different question
// from IsCoreEsslImageFormat, and a different set.
static bool IsSpirvCrossEsslPrintableFormat(Uint32 spirvImageFormat);
// The ESSL layout-qualifier spelling of a GL internal format, or empty when the
// format has no image-format spelling. For the text-level completion above.
static String EsslSpellingOfGLInternalFormat(Uint glInternalFormat);
static spvtools::Optimizer::PassToken CreateBakeImageFormatsPass(GLFormatByName glFormatByName);
private:
GLFormatByName m_glFormatByName;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -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());
}
@@ -259,6 +259,25 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
}
}
// The one client (format, type) pair whose word is bit-identical to the packed internal
// word, if any. Everything else has to go through the decode/encode conversion.
Bool IsRawPackedPixelPair(PackedInternalKind kind, TextureInputFormat format,
TexturePixelDataType type) {
switch (kind) {
case PackedInternalKind::UNorm2101010Rev:
return format == TextureInputFormat::RGBA && type == TexturePixelDataType::UnsignedInt2101010Rev;
case PackedInternalKind::UInt2101010Rev:
return format == TextureInputFormat::RGBAInteger &&
type == TexturePixelDataType::UnsignedInt2101010Rev;
case PackedInternalKind::FloatR11G11B10:
return format == TextureInputFormat::RGB && type == TexturePixelDataType::UnsignedInt101111Rev;
case PackedInternalKind::FloatRGB9E5:
return format == TextureInputFormat::RGB && type == TexturePixelDataType::UnsignedInt5999Rev;
default:
return false;
}
}
Uint32 EncodePackedInternalWordFloat(PackedInternalKind kind, const Float rgba[4]) {
switch (kind) {
case PackedInternalKind::UNorm2101010Rev: {
@@ -431,10 +450,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
return false;
}
// The client word already equals the packed internal word (memcpy fast path).
if (hasPackedInternal && type == TexturePixelDataType::UnsignedInt2101010Rev &&
(format == TextureInputFormat::RGBA || format == TextureInputFormat::RGBAInteger) &&
(packedInternal.kind == PackedInternalKind::UNorm2101010Rev ||
packedInternal.kind == PackedInternalKind::UInt2101010Rev)) {
if (hasPackedInternal && IsRawPackedPixelPair(packedInternal.kind, format, type)) {
return false;
}
} else {
@@ -458,11 +474,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
// GL_RGB, which the state layer already enforces.
if (mapping.isInteger || mapping.channelCount != 3) return false;
// The client word already equals the packed internal word.
if (hasPackedInternal &&
((packedInternal.kind == PackedInternalKind::FloatRGB9E5 &&
type == TexturePixelDataType::UnsignedInt5999Rev) ||
(packedInternal.kind == PackedInternalKind::FloatR11G11B10 &&
type == TexturePixelDataType::UnsignedInt101111Rev))) {
if (hasPackedInternal && IsRawPackedPixelPair(packedInternal.kind, format, type)) {
return false;
}
break;
@@ -765,6 +777,15 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
}
} // namespace
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
TexturePixelDataType clientType) {
InternalPackedLayout packedInternal{};
if (!GetInternalPackedLayout(internalFormat, packedInternal)) {
return false;
}
return IsRawPackedPixelPair(packedInternal.kind, clientFormat, clientType);
}
// assume 8 bit per channel
// swizzle.size() == channel count
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) {
@@ -23,6 +23,21 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
IntVec3 dimension, Bool isBitmap, SizeT& outSize);
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle);
// True when a packed internal format's 32-bit storage word IS the client (format, type) word,
// so the transfer has to move the words verbatim in both directions.
//
// Decoding such a texel to float and re-encoding it is NOT a no-op: RGB9_E5 stores a shared
// exponent with redundant encodings, and the spec's encode algorithm (GL 4.6 8.5.2) always
// emits the canonical one - 0xf8fc0000 and 0xe7e00000 are the same value 8064, but only the
// latter is canonical. glTexImage followed by glGetTexImage must hand back the bits the
// application uploaded, which is what GL CTS KHR-GL43.copy_image compares
// ("CopyImageSubData modified contents of source image").
//
// Only the pairs whose bit layouts are identical qualify; a genuinely different client format
// or type still needs the decode/encode conversion.
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
TexturePixelDataType clientType);
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
// readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with
// 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set
@@ -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;
+5 -1
View File
@@ -187,7 +187,11 @@ collect_run_diagnostics() {
if [ "${adb_state}" != "device" ]; then
return
fi
"${ADB}" logcat -d -t 2000 > "${diagnostics_dir}/logcat.txt" || true
# Depth matters, not just content: is_infrastructure_failure below decides "the emulator
# broke, retry" by finding a system_server crash in this window, and MobileGL logs into the
# same buffer under its own tag. A tail that is too short lets routine MobileGL output evict
# the crash line and charges an infrastructure fault to the trace under test.
"${ADB}" logcat -d -t 20000 > "${diagnostics_dir}/logcat.txt" || true
adb_device_path shell pidof "${package_name}" > "${diagnostics_dir}/pidof.txt" 2>&1 || true
adb_device_path shell dumpsys activity activities > "${diagnostics_dir}/activity.txt" 2>&1 || true
adb_device_path shell run-as "${package_name}" ls -laR "${app_dir}" > "${diagnostics_dir}/app-files.txt" 2>&1 || true