mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
10
Commits
bce34d7fac
...
f9182a5ca3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9182a5ca3 | ||
|
|
f37b511fca | ||
|
|
38027d21f8 | ||
|
|
dd2a62228f | ||
|
|
373aa44dd7 | ||
|
|
96646df12e | ||
|
|
f20b20e643 | ||
|
|
2587814970 | ||
|
|
43398e33e8 | ||
|
|
b7557d6615 |
@@ -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
@@ -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 { \
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5573,6 +5581,16 @@ 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 Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat());
|
||||
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat());
|
||||
@@ -5994,7 +6012,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 +6166,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 +6424,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 +6440,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 +6460,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 +6578,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 +6682,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 +6987,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 +7063,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;
|
||||
}
|
||||
@@ -7228,7 +7246,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 +7267,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 +7292,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 +7325,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 +7357,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 +7377,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 +7413,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 +7441,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 +7509,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 +7521,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 +7618,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 +7654,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 +7690,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 +8077,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 +8188,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 +8226,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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1556,7 +1642,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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");
|
||||
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,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); \
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -3404,6 +3404,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
!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,
|
||||
@@ -3643,10 +3654,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.",
|
||||
|
||||
@@ -353,6 +353,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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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*/));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user