diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index f4b21ae4..4f333d4a 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1151,6 +1151,16 @@ namespace MobileGL::MG_Backend::DirectGLES { m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations; m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings; + // This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and + // on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather + // than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says + // which). Reporting 0 instead was considered and rejected: MobileGL advertises an OpenGL + // 4.x context, where buffer textures are core and the limit has a spec minimum of 65536, + // so 0 is not a legal answer and applications are not written to survive it. GL offers no + // way to say "this core feature is missing", so the honesty is carried outside the limit: + // FillInGLESCapabilities logs the tier, glTexBuffer and the program build each name the + // missing capability at MGLOG_I, and the driver POST carries a "Buffer textures" row that + // FAILs on this tier. m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize; m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment; m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index a47406b5..0a6b2b95 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -7233,6 +7233,30 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glGetQueryObjectui64vEXT; } + Bool AreBufferTexturesSupported() { + // The tier already folds in the resolved-pointer requirement (see FillInGLESCapabilities), + // but the pointer is re-checked here because the tier is only meaningful once the + // capabilities have been filled in, and callers may run before that. + return g_GLESCapabilities.TextureBufferSupport != + MG_External::GLESCapabilities::TextureBufferTier::None && + g_GLESFuncs.glTexBuffer != nullptr; + } + + const char* GetBufferTextureTierName() { + using Tier = MG_External::GLESCapabilities::TextureBufferTier; + switch (g_GLESCapabilities.TextureBufferSupport) { + case Tier::CoreEs32: + return "core (ES 3.2)"; + case Tier::ExtensionEXT: + return "GL_EXT_texture_buffer"; + case Tier::ExtensionOES: + return "GL_OES_texture_buffer"; + case Tier::None: + default: + return "unsupported"; + } + } + BackendQueryHandle BeginTimeElapsedQuery() { // Query objects can only be created on the thread that owns the ES // context (MC's F3 profiler queries on the render thread, which diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 9207f9a7..7b315002 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -117,6 +117,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // capability read needs no current ES context, and it stays false until // the ES capabilities have been filled in. Bool AreTimerQueriesSupported(); + // True when the host ES driver can back a GL_TEXTURE_BUFFER at all - ES 3.2 core, or + // EXT/OES_texture_buffer, with glTexBuffer resolved. Desktop GL has had buffer textures as + // core since 3.1, so the frontend advertises them unconditionally and an app may call + // glTexBuffer whenever it likes; this is the only thing standing between that call and a + // null entry point. False also means every shader declaring a samplerBuffer is + // uncompilable on this driver, which the program build reports by name. + Bool AreBufferTexturesSupported(); + // Human-readable name of the buffer-texture tier for diagnostics and the driver POST: + // "core (ES 3.2)", "GL_EXT_texture_buffer", "GL_OES_texture_buffer" or "unsupported". + const char* GetBufferTextureTierName(); // GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The // creators return null (the frontend then falls back to an immediately // available zero result) when the calling thread does not own the ES diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 08eb95ac..bba3d1ff 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2779,6 +2779,29 @@ namespace MobileGL::MG_Backend::DirectGLES { &glType, TextureTarget::TextureBuffer); if (needsRegeneration) { + // Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a + // 4.x context, so glTexBuffer is a legal call the app may make on any driver - + // but ES only gained them in 3.2, and g_GLESFuncs.glTexBuffer is simply null + // 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. + if (!AreBufferTexturesSupported()) { + if (m_bufferTextureUnsupportedReported) { + break; + } + m_bufferTextureUnsupportedReported = true; + MGLOG_I("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 " + "OpenGL 4.x context may not report 0.", + stateTextureObject->GetExternalIndex(), GetBufferTextureTierName(), + g_GLESCapabilities.MaxTextureBufferSize); + break; + } MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with " "ID: %u, buffer ID: %u, buffer size: %zu, format: %s", m_backendTextureId, backendId, buffer->GetSize(), @@ -4297,6 +4320,26 @@ namespace MobileGL::MG_Backend::DirectGLES { String source; auto& spirvCode = shaderSpirvs[index]; + // A samplerBuffer is core in the OpenGL 3.1+ context MobileGL advertises but needs + // 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). + // 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 " + "cannot provide (%s). The shader will not compile and the program will " + "not link; every draw using it is a no-op.", + m_backendProgramId, + MG_Util::ConvertGLEnumToString(glShaderType).c_str(), + GetBufferTextureTierName()); + m_backendProgramUsable = false; + g_GLESFuncs.glDeleteShader(backendShaderId); + continue; + } + // ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to // plain globals (mg_*) before handing the module to SPIRV-Cross. Vector loweredSpirv; @@ -4399,6 +4442,14 @@ namespace MobileGL::MG_Backend::DirectGLES { source = result; + // First in the chain because it is the only header-level rewrite: it edits + // #extension directives and never the body, so it is independent of every pass + // below and running it early keeps the directive block correct for + // ForceSupporterOutput, which scans for the last #extension line to decide where + // its precision statements go. + source = RetargetTextureBufferExtension(std::move(source), + g_GLESCapabilities.TextureBufferSupport); + source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject); // Wedged between those two on purpose: // * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index ac13dfc1..d5440906 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -626,6 +626,11 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool m_isInitialized = false; Bool m_imageBindableStorageRequired = false; Bool m_backendStorageImmutable = false; + // Latches the "this driver has no buffer textures" report to once per texture. The + // report is emitted from the respecify path, which bails before recording the state + // it was asked to apply - so without the latch the texture stays permanently dirty + // and every draw of every frame logs the same line. + Bool m_bufferTextureUnsupportedReported = false; StateTextureBasicInfo m_prevTextureInfo; // Frontend content version at the last completed mipmap sync. The per-draw // clean probe compares this before rebuilding shape info and scanning diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 431086cc..ed534f71 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -435,6 +435,69 @@ namespace MobileGL::MG_Backend::DirectGLES { return result; } + String RetargetTextureBufferExtension(String glslCode, + MG_External::GLESCapabilities::TextureBufferTier tier) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // SPIRV-Cross hardcodes the EXT spelling: CompilerGLSL::type_to_glsl emits + // require_extension_internal("GL_EXT_texture_buffer") for any Dim=Buffer image + // whenever it targets ESSL below 320, with no OES alternative and no way to + // configure it. GL_OES_texture_buffer is functionally identical but is a separate + // directive, and `#extension : require` on a name the driver does not + // advertise is a hard compile error - so on an OES-only driver the emitted shader + // fails to compile for the sake of one token. + // + // Deliberately a directive rewrite and nothing more. The alternative - teaching the + // SPIR-V to stop asking for the extension - is not available: the requirement is + // synthesized by SPIRV-Cross from the image type itself, not carried in the module, + // so there is nothing upstream to strip. Everything about the shader body that + // actually uses the buffer texture is identical between the two extensions. + using Tier = MG_External::GLESCapabilities::TextureBufferTier; + if (tier != Tier::ExtensionOES) { + return glslCode; + } + static constexpr const char* kExtName = "GL_EXT_texture_buffer"; + static constexpr const char* kOesName = "GL_OES_texture_buffer"; + constexpr SizeT kExtNameLength = 21; // strlen("GL_EXT_texture_buffer") + static_assert(sizeof("GL_EXT_texture_buffer") - 1 == kExtNameLength, "name length drifted"); + static_assert(sizeof("GL_OES_texture_buffer") - 1 == kExtNameLength, + "the two spellings must be the same length for the in-place replace"); + + // Only rewrite the name where it is the subject of an #extension directive. The same + // token can legitimately appear in a comment SPIRV-Cross carried through, and a + // shader that merely mentions the string must not be edited. + SizeT searchFrom = 0; + while (true) { + const SizeT hit = glslCode.find(kExtName, searchFrom); + if (hit == String::npos) { + break; + } + searchFrom = hit + kExtNameLength; + + // Walk back to the start of the line and require that it is an #extension + // directive, allowing whitespace between '#' and the keyword. + SizeT lineStart = glslCode.rfind('\n', hit); + lineStart = (lineStart == String::npos) ? 0 : lineStart + 1; + SizeT cursor = lineStart; + while (cursor < hit && std::isspace(static_cast(glslCode[cursor]))) { + ++cursor; + } + if (cursor >= hit || glslCode[cursor] != '#') { + continue; + } + ++cursor; + while (cursor < hit && std::isspace(static_cast(glslCode[cursor]))) { + ++cursor; + } + if (glslCode.compare(cursor, 9, "extension") != 0) { + continue; + } + glslCode.replace(hit, kExtNameLength, kOesName); + } + return glslCode; + } + String RemoveLayoutBinding(const String& glslCode) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 2a7473be..04eaf707 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -130,6 +130,13 @@ namespace MobileGL::MG_Backend::DirectGLES { // drawBufferCount <= 1, i.e. for everything but a framebuffer that actually // enables several draw buffers, so the ordinary single-target shader is untouched. String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount); + // SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for every buffer-texture + // sampler when it targets ESSL below 320, and offers no way to ask for the OES spelling. + // On a driver that advertises only GL_OES_texture_buffer that directive is a compile + // error, so the name is retargeted in the emitted source. A no-op on every other tier: + // 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); 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. diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 4fb87798..604c310c 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -67,6 +67,7 @@ add_executable(MobileGLIntegrationTest Scenarios/SsboDeclarationFormScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp Scenarios/FragmentOutputArrayIndexScenario.cpp + Scenarios/BufferTextureScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE diff --git a/MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp new file mode 100644 index 00000000..47d3d3cf --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp @@ -0,0 +1,170 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.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 - A BUFFER TEXTURE IS SAMPLED FROM THE VERTEX STAGE, AND TRACKS ITS BUFFER. +// +// Buffer textures are core in OpenGL 3.1 and MobileGL advertises a 4.x context, so an +// application may build geometry out of one without asking whether the host can. Minecraft +// 26.3 does exactly that: its cloud layer has no vertex attributes at all, only gl_VertexID +// and texelFetch on a GL_R8I buffer texture. Nothing covered that path end to end on either +// backend - the frontend unit tests stop at glTexBuffer's state, and no scenario ever drew +// with the result - which is how DirectGLES came to emit `#extension GL_EXT_texture_buffer : +// require` unconditionally, compile nothing on a host without the extension, and lose the +// whole cloud layer with no diagnostic anywhere. +// +// Two claims, in the order they can break: +// 1. a vertex-stage texelFetch on an R8I buffer texture reads the byte the application put +// in the buffer (the shape of the real workload: no attributes, index from gl_VertexID); +// 2. a later glBufferSubData is visible to the next draw WITHOUT re-specifying the texture. +// glTexBuffer attaches storage, it does not copy: the texture is a live view of the +// buffer, so a backend that only refreshes the view when the texture's own state changes +// must still show the new bytes. DirectGLES' respecify gate is keyed on the texture info +// and deliberately does not include the buffer's contents, so this is the assertion that +// says that is safe rather than merely untested. +// +// NOTE ON A HOST WITHOUT BUFFER TEXTURES: this scenario is expected to FAIL there, and that is +// the honest outcome - MobileGL keeps advertising GL_MAX_TEXTURE_BUFFER_SIZE (an OpenGL 4.x +// context may not answer 0), so there is no capability an application, or this test, could +// branch on. The driver POST's "Buffer textures" row is where that verdict is stated. + +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + // No vertex attributes: the quad's corners come from gl_VertexID, exactly like the + // workload this exists for. The texel is fetched in the VERTEX stage - the stage where + // buffer-texture support is scarcest across ES drivers - and carried flat so every + // fragment of the quad reports the same byte and the readback is exact. + constexpr const char* kVS = R"(#version 330 core +uniform isamplerBuffer uFaces; +flat out int vFace; +void main() { + vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0, + (gl_VertexID & 2) == 0 ? -1.0 : 1.0); + vFace = texelFetch(uFaces, 0).r; + gl_Position = vec4(corner, 0.0, 1.0); +} +)"; + + // 1/255 steps survive an RGBA8 round trip exactly, so the readback byte IS the value + // the vertex shader fetched. + constexpr const char* kFS = R"(#version 330 core +flat in int vFace; +out vec4 o_color; +void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); } +)"; + + class BufferTextureScenario : public ScenarioTest {}; + + // Draws the full-viewport quad and returns the red byte every fragment was painted with, + // or -1 if the quad did not come out uniform (which would mean the flat varying, not the + // fetch, is what this test is measuring). + int PaintedValue(unsigned int program, int width, int height) { + ClearTo(0.0f, 0.0f, 0.0f, 1.0f); + GLuint vao = 0; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + glUseProgram(program); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + glDeleteVertexArrays(1, &vao); + + const Image image = ReadPixels(width, height); + if (image.Empty()) { + return -1; + } + const int first = image.At(0, 0).r; + for (int y = 0; y < image.Height(); ++y) { + for (int x = 0; x < image.Width(); ++x) { + if (image.At(x, y).r != first) { + return -1; + } + } + } + return first; + } + + } // namespace + + TEST_F(BufferTextureScenario, VertexStageTexelFetchReadsTheBufferAndTracksItsUpdates) { + if (!Ready()) return; + HeadlessGL& gl = Gl(); + + std::string error; + const unsigned int program = CompileProgram(kVS, kFS, &error); + ASSERT_NE(program, 0u) << error; + + // GL_R8I is the format the real workload uses. Signed, so the values stay well inside + // [0, 127] to keep the readback arithmetic honest. + constexpr signed char kInitial = 37; + constexpr signed char kUpdated = 91; + std::vector texels(64, 0); + texels[0] = kInitial; + + GLuint buffer = 0; + glGenBuffers(1, &buffer); + glBindBuffer(GL_TEXTURE_BUFFER, buffer); + glBufferData(GL_TEXTURE_BUFFER, static_cast(texels.size()), texels.data(), + GL_DYNAMIC_DRAW); + + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_BUFFER, texture); + glTexBuffer(GL_TEXTURE_BUFFER, GL_R8I, buffer); + ASSERT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_R8I) was refused"; + + ColorFbo target = MakeColorFbo(64, 64); + ASSERT_NE(target.fbo, 0u) << "could not create the render target"; + BindFbo(target); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_BUFFER, texture); + glUseProgram(program); + const GLint location = glGetUniformLocation(program, "uFaces"); + ASSERT_NE(location, -1) << "the buffer sampler was optimized away or never reflected"; + glUniform1i(location, 0); + + EXPECT_EQ(PaintedValue(program, target.width, target.height), static_cast(kInitial)) + << "a vertex-stage texelFetch on an R8I buffer texture did not read the byte the " + "application stored (a uniform -1 here means the quad was not uniform at all)"; + + // The texture is a VIEW of the buffer: no glTexBuffer call follows, and none should be + // needed for the new bytes to be visible. + glBindBuffer(GL_TEXTURE_BUFFER, buffer); + glBufferSubData(GL_TEXTURE_BUFFER, 0, 1, &kUpdated); + ASSERT_EQ(FirstGLError(), 0u) << "glBufferSubData on the texture's buffer was refused"; + + EXPECT_EQ(PaintedValue(program, target.width, target.height), static_cast(kUpdated)) + << "the buffer texture kept showing the old contents after glBufferSubData; the " + "texture must track its buffer without being re-specified"; + + BindDefaultFramebuffer(); + DestroyColorFbo(target); + glUseProgram(0); + glDeleteProgram(program); + glDeleteTextures(1, &texture); + glDeleteBuffers(1, &buffer); + glViewport(0, 0, gl.Width(), gl.Height()); + EXPECT_EQ(FirstGLError(), 0u); + } + +} // namespace MGITest diff --git a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp index f3e4d2f3..a8246cb5 100644 --- a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp +++ b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp @@ -253,3 +253,90 @@ void main() )"; EXPECT_EQ(SplitReadWriteImageUniforms(source), source); } + +// --------------------------------------------------------------------------------------- +// RetargetTextureBufferExtension +// +// Buffer textures are core in the OpenGL 3.1+ context MobileGL advertises, but in ES they +// only became core in 3.2; below that they need EXT_texture_buffer or OES_texture_buffer. +// SPIRV-Cross hardcodes the EXT spelling for every Dim=Buffer image it emits below ESSL 320 +// and offers no way to ask for the other one, so on a driver that advertises only the OES +// name the `: require` is a hard compile error over a single token. +// --------------------------------------------------------------------------------------- + +using Tier = MobileGL::MG_External::GLESCapabilities::TextureBufferTier; +using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RetargetTextureBufferExtension; + +namespace { + // What SPIRV-Cross actually emits for `uniform isamplerBuffer CloudFaces;` at ESSL 310 - + // the shape that empties Minecraft 26.3's cloud layer on a driver without the extension. + const String kBufferTextureShader = R"(#version 310 es +#extension GL_EXT_texture_buffer : require +precision highp float; +uniform highp isamplerBuffer CloudFaces; +layout(location = 0) out highp vec4 mg_FragColor; +void main() +{ + mg_FragColor = vec4(texelFetch(CloudFaces, gl_VertexID).r); +} +)"; +} // namespace + +TEST(RetargetTextureBufferExtensionTest, OesOnlyDriverGetsTheOesDirective) { + const String out = RetargetTextureBufferExtension(kBufferTextureShader, Tier::ExtensionOES); + EXPECT_TRUE(Contains(out, "#extension GL_OES_texture_buffer : require")) + << "the OES driver's own spelling must reach the directive:\n" << out; + EXPECT_FALSE(Contains(out, "GL_EXT_texture_buffer")) + << "the EXT spelling this driver does not advertise must be gone:\n" << out; + // Only the directive changes; the declaration and the fetch are identical between the two + // extensions and must not be touched. + EXPECT_TRUE(Contains(out, "uniform highp isamplerBuffer CloudFaces;")); + EXPECT_TRUE(Contains(out, "texelFetch(CloudFaces, gl_VertexID)")); +} + +TEST(RetargetTextureBufferExtensionTest, ExtDriverKeepsWhatSpirvCrossEmitted) { + EXPECT_EQ(RetargetTextureBufferExtension(kBufferTextureShader, Tier::ExtensionEXT), + kBufferTextureShader); +} + +// ES 3.2 needs no directive at all, and SPIRV-Cross emits none at ESSL 320 - but a shader +// that arrived with one anyway must not be rewritten to a name the pass was not asked for. +TEST(RetargetTextureBufferExtensionTest, CoreAndUnsupportedTiersAreNoOps) { + EXPECT_EQ(RetargetTextureBufferExtension(kBufferTextureShader, Tier::CoreEs32), + kBufferTextureShader); + EXPECT_EQ(RetargetTextureBufferExtension(kBufferTextureShader, Tier::None), + kBufferTextureShader); +} + +// The name is only the subject of a rewrite where it is the subject of an #extension +// directive. A shader that merely mentions it - in a comment SPIRV-Cross carried through, or +// in an identifier - is not an extension request and must come out byte-identical. +TEST(RetargetTextureBufferExtensionTest, OnlyExtensionDirectivesAreRewritten) { + const String source = R"(#version 310 es +// GL_EXT_texture_buffer is what this shader would need +precision highp float; +uniform highp float GL_EXT_texture_buffer_lookalike; +layout(location = 0) out highp vec4 mg_FragColor; +void main() +{ + mg_FragColor = vec4(GL_EXT_texture_buffer_lookalike); +} +)"; + EXPECT_EQ(RetargetTextureBufferExtension(source, Tier::ExtensionOES), source); +} + +// Whitespace between '#' and the keyword is legal in GLSL, and a shader carrying several +// extension directives must have exactly the one retargeted. +TEST(RetargetTextureBufferExtensionTest, SpacedDirectiveIsRewrittenAndNeighboursAreLeftAlone) { + const String source = R"(#version 310 es +# extension GL_EXT_texture_buffer : require +#extension GL_EXT_shader_io_blocks : require +precision highp float; +void main() {} +)"; + const String out = RetargetTextureBufferExtension(source, Tier::ExtensionOES); + EXPECT_TRUE(Contains(out, "# extension GL_OES_texture_buffer : require")) << out; + EXPECT_TRUE(Contains(out, "#extension GL_EXT_shader_io_blocks : require")) + << "an unrelated extension must survive untouched:\n" << out; + EXPECT_EQ(CountOf(out, "GL_OES_texture_buffer"), 1u); +} diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 6994c1db..27b702b2 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -3324,3 +3324,160 @@ void main() { EXPECT_FALSE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw)) << "only fragment outputs carry the constant-index rule:\n" << DisassembleSpirv(raw); } + +// --------------------------------------------------------------------------------------- +// Buffer-texture samplers (samplerBuffer / isamplerBuffer / usamplerBuffer) +// +// Buffer textures are core in OpenGL 3.1 and MobileGL advertises a 4.x context, so an +// application may sample one without asking. On the ES side they only became core in 3.2, +// and SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for any Dim=Buffer +// image it renders below ESSL 320. On a driver with neither EXT_ nor OES_texture_buffer that +// directive - and the isamplerBuffer keyword behind it - fail to compile, the program never +// links, and every draw using it is a silent no-op. DirectGLES asks the detector below so it +// can name that as the missing capability it is, instead of leaving a driver info log the +// shipped INFO build compiles out. +// --------------------------------------------------------------------------------------- + +namespace { + // Compiles `source` for `stage` and returns the module, or fails the calling test. + Vector BuildSpirvForStage(const String& source, GLenum stage) { + using namespace MG_Util::ShaderTranspiler; + ShaderAttrib attrib{.shaderType = stage, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + ADD_FAILURE() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log; + return {}; + } + ProgramAttrib programAttrib{.shaders = {res.value()}}; + auto program_res = ShaderCompiler::LinkProgram(programAttrib); + if (!program_res) { + ADD_FAILURE() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log; + return {}; + } + ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *program_res.value()}; + auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + if (!bin_res) { + ADD_FAILURE() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log; + return {}; + } + if (bin_res.value().size() != 1u) { + ADD_FAILURE() << "expected exactly one module, got " << bin_res.value().size(); + return {}; + } + return bin_res.value()[0]; + } +} // namespace + +// The shape of Minecraft 26.3's cloud vertex shader: no vertex attributes at all, the whole +// geometry read out of a GL_R8I buffer texture indexed by gl_VertexID. +TEST_F(ProgramUtilTest, BufferTextureSamplerIsDetectedInTheModule) { + using namespace MG_Util::ShaderTranspiler; + + String vs = R"(#version 330 core +uniform isamplerBuffer CloudFaces; +out vec4 vColor; +void main() { + int face = texelFetch(CloudFaces, gl_VertexID).r; + vColor = vec4(float(face) / 255.0); + gl_Position = vec4(0.0, 0.0, 0.0, 1.0); +} +)"; + const Vector spirv = BuildSpirvForStage(vs, GL_VERTEX_SHADER); + ASSERT_FALSE(spirv.empty()); + EXPECT_TRUE(ShaderCompiler::ModuleDeclaresBufferTextureSampler(spirv)) + << "an isamplerBuffer must be recognised as a buffer texture"; +} + +// The float and unsigned spellings lower to the same Dim=Buffer image with a different +// sampled type, so all three have to be caught by the same check. +TEST_F(ProgramUtilTest, FloatAndUnsignedBufferSamplersAreDetectedToo) { + using namespace MG_Util::ShaderTranspiler; + + String floatFs = R"(#version 330 core +uniform samplerBuffer Data; +out vec4 fragColor; +void main() { fragColor = texelFetch(Data, 3); } +)"; + const Vector floatSpirv = BuildSpirvForStage(floatFs, GL_FRAGMENT_SHADER); + ASSERT_FALSE(floatSpirv.empty()); + EXPECT_TRUE(ShaderCompiler::ModuleDeclaresBufferTextureSampler(floatSpirv)); + + String uintFs = R"(#version 330 core +uniform usamplerBuffer Data; +out vec4 fragColor; +void main() { fragColor = vec4(texelFetch(Data, 3)); } +)"; + const Vector uintSpirv = BuildSpirvForStage(uintFs, GL_FRAGMENT_SHADER); + ASSERT_FALSE(uintSpirv.empty()); + EXPECT_TRUE(ShaderCompiler::ModuleDeclaresBufferTextureSampler(uintSpirv)); +} + +// The negative control that keeps the detector from turning into "declares any sampler": +// an ordinary sampler2D must not put a program on the unsupported path on a driver that is +// perfectly able to run it. +TEST_F(ProgramUtilTest, OrdinaryTextureSamplersAreNotBufferTextures) { + using namespace MG_Util::ShaderTranspiler; + + String fs = R"(#version 330 core +uniform sampler2D Albedo; +uniform isampler2D Ids; +in vec2 vUv; +out vec4 fragColor; +void main() { fragColor = texture(Albedo, vUv) + vec4(texelFetch(Ids, ivec2(0), 0)); } +)"; + const Vector spirv = BuildSpirvForStage(fs, GL_FRAGMENT_SHADER); + ASSERT_FALSE(spirv.empty()); + EXPECT_FALSE(ShaderCompiler::ModuleDeclaresBufferTextureSampler(spirv)) + << "only Dim=Buffer images are buffer textures"; +} + +// Pins the SPIRV-Cross behaviour the whole defect rests on: below ESSL 320 it synthesizes an +// EXT_texture_buffer requirement from the image type itself. There is nothing in the module +// to strip - which is why the OES driver is served by retargeting the emitted directive +// (RetargetTextureBufferExtension) rather than by rewriting the SPIR-V. +TEST_F(ProgramUtilTest, BufferTextureSamplerEmitsTheExtDirectiveInEssl) { + using namespace MG_Util::ShaderTranspiler; + + String fs = R"(#version 330 core +uniform isamplerBuffer Data; +out vec4 fragColor; +void main() { fragColor = vec4(texelFetch(Data, 3)); } +)"; + const Vector spirv = BuildSpirvForStage(fs, GL_FRAGMENT_SHADER); + ASSERT_FALSE(spirv.empty()); + + // Emitted at ESSL 310 the way DirectGLES does on an ES 3.1 host (ShaderCompiler's own + // DecompileShader helper hardcodes 320, where the question does not arise). This is the + // version the defect lives at: the emulator SDK's ANGLE is ES 3.1 with neither extension. + auto emitAt = [&spirv](unsigned version) -> String { + SpvcSession session(spirv, SessionUsageBit::Transpile); + spvc_compiler_options options; + session.CreateOptions(&options); + spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, version); + spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE); + spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); + session.SetOptions(options); + const char* result = nullptr; + session.Compile(&result); + return result ? String(result) : String(); + }; + + const String essl310 = emitAt(310); + ASSERT_FALSE(essl310.empty()) << "ESSL 310 emission failed outright"; + EXPECT_NE(essl310.find("isamplerBuffer"), String::npos) + << "the buffer sampler must survive to ESSL:\n" << essl310; + EXPECT_NE(essl310.find("GL_EXT_texture_buffer"), String::npos) + << "SPIRV-Cross requires EXT_texture_buffer below ESSL 320, and hardcodes that spelling - " + "which is the whole reason an OES-only driver needs the emitted directive retargeted:\n" + << essl310; + + // At 320 buffer textures are ES core, so there is no directive to get wrong. This half is + // what makes the ES 3.2 tier a Pass with nothing to do rather than a silent dependency. + const String essl320 = emitAt(320); + ASSERT_FALSE(essl320.empty()) << "ESSL 320 emission failed outright"; + EXPECT_NE(essl320.find("isamplerBuffer"), String::npos) << essl320; + EXPECT_EQ(essl320.find("GL_EXT_texture_buffer"), String::npos) + << "ES 3.2 has buffer textures in core; requiring the extension there would be wrong:\n" + << essl320; + +} diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 1ef58b75..30528254 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -841,6 +841,9 @@ namespace MobileGL::MG_Util::BackendLoader { Bool hasMultiDrawIndirectExtension = false; Bool hasDrawElementsBaseVertexExtension = false; Bool hasMultiDrawArraysExtension = false; + // Resolved into caps.TextureBufferSupport below, once the ES version is also known. + Bool hasExtTextureBuffer = false; + Bool hasOesTextureBuffer = false; for (GLint i = 0; i < extCount; ++i) { const char* extension = (const char*)glesFuncs.glGetStringi(GL_EXTENSIONS, i); if (extension) { @@ -874,6 +877,12 @@ namespace MobileGL::MG_Util::BackendLoader { std::strcmp(extension, "GL_OES_texture_cube_map_array") == 0) { caps.SupportsTextureCubeMapArray = true; } + if (std::strcmp(extension, "GL_EXT_texture_buffer") == 0) { + hasExtTextureBuffer = true; + } + if (std::strcmp(extension, "GL_OES_texture_buffer") == 0) { + hasOesTextureBuffer = true; + } if (std::strcmp(extension, "GL_EXT_base_instance") == 0) { caps.SupportsBaseInstance = true; } @@ -1050,7 +1059,12 @@ namespace MobileGL::MG_Util::BackendLoader { glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &maxComputeUniformBlocks); glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &maxComputeWorkGroupInvocations); glesFuncs.glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxShaderStorageBufferBindings); - glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize); + // GL_MAX_TEXTURE_BUFFER_SIZE is deliberately NOT batched here: like + // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT below, the pname only exists once buffer textures do, + // so on a driver without them it raises GL_INVALID_ENUM, leaves the local at MobileGL's own + // floor, and - because nothing drains the queue until the alignment probe far below - lets + // that error be misattributed to any query in between. It is queried in the guarded block + // that resolves caps.TextureBufferSupport instead. glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxUniformBufferBindings); glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize); glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits); @@ -1101,6 +1115,62 @@ namespace MobileGL::MG_Util::BackendLoader { caps.SupportsTextureBorderClamp = true; caps.SupportsTextureCubeMapArray = true; } + // Buffer-texture tier. Core from ES 3.2 on; below that the EXT spelling is preferred over + // the OES one purely because SPIRV-Cross emits GL_EXT_texture_buffer natively, so a driver + // with both needs no directive retargeting. The entry point has to have resolved either + // way - the extension string alone is not support (see the multi-draw note above). + { + const Bool textureBufferIsCore = + caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2); + using Tier = MG_External::GLESCapabilities::TextureBufferTier; + if (glesFuncs.glTexBuffer == nullptr) { + caps.TextureBufferSupport = Tier::None; + } else if (textureBufferIsCore) { + caps.TextureBufferSupport = Tier::CoreEs32; + } else if (hasExtTextureBuffer) { + caps.TextureBufferSupport = Tier::ExtensionEXT; + } else if (hasOesTextureBuffer) { + caps.TextureBufferSupport = Tier::ExtensionOES; + } else { + caps.TextureBufferSupport = Tier::None; + } + + if (caps.TextureBufferSupport != Tier::None) { + // Drain first: an error left by any earlier probe would otherwise read as this + // query having failed, and the value would be discarded as a non-answer. + if (glesFuncs.glGetError) { + while (glesFuncs.glGetError() != GL_NO_ERROR) { + } + } + glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize); + if (glesFuncs.glGetError) { + Bool queryFailed = false; + while (glesFuncs.glGetError() != GL_NO_ERROR) { + queryFailed = true; + } + caps.MaxTextureBufferSizeIsDriverReported = !queryFailed; + } else { + caps.MaxTextureBufferSizeIsDriverReported = true; + } + } + // On the None tier the local keeps MobileGL's floor and + // MaxTextureBufferSizeIsDriverReported stays false. The floor, not 0, is what the + // frontend goes on advertising: MobileGL reports an OpenGL 4.x context, where buffer + // textures are core and GL_MAX_TEXTURE_BUFFER_SIZE has a spec minimum of 65536, so 0 + // would be an illegal answer that no conformant app is prepared to read (several + // divide by it or size an allocation with it). The dishonesty is contained by making + // the missing capability loud instead - at capability init here, at glTexBuffer, at + // program build, and as its own driver POST row - because GL offers no way to say + // "buffer textures exist but cannot work". + if (caps.TextureBufferSupport == Tier::None) { + MGLOG_I(" Buffer textures: UNSUPPORTED (ES %d.%d core needs 3.2, and neither " + "GL_EXT_texture_buffer nor GL_OES_texture_buffer is present). Any shader " + "sampling a samplerBuffer will fail to compile, and MobileGL keeps " + "advertising GL_MAX_TEXTURE_BUFFER_SIZE = %d because a GL 4.x context may " + "not report 0.", + caps.GLESVersion.Major, caps.GLESVersion.Minor, maxTextureBufferSize); + } + } if (caps.SupportsTextureFilterAnisotropy) { GLfloat maxTextureMaxAnisotropy = 1.0f; glesFuncs.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxTextureMaxAnisotropy); @@ -1215,7 +1285,8 @@ namespace MobileGL::MG_Util::BackendLoader { MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks); MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations); MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings); - MGLOG_I(" GL_MAX_TEXTURE_BUFFER_SIZE: %d", caps.MaxTextureBufferSize); + MGLOG_I(" GL_MAX_TEXTURE_BUFFER_SIZE: %d%s", caps.MaxTextureBufferSize, + caps.MaxTextureBufferSizeIsDriverReported ? "" : " (MobileGL floor - the driver has no buffer textures to ask)"); MGLOG_I(" GL_MAX_UNIFORM_BUFFER_BINDINGS: %d", caps.MaxUniformBufferBindings); MGLOG_I(" GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize); MGLOG_I(" GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits); diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index 4c8976ca..c0647271 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1057,6 +1057,27 @@ namespace MobileGL { Bool SupportsTextureBorderClamp = false; // GL_TEXTURE_CUBE_MAP_ARRAY: ES 3.2 core, or EXT/OES_texture_cube_map_array before it. Bool SupportsTextureCubeMapArray = false; + // Which spelling of buffer-texture support the host driver has. Desktop GL makes buffer + // textures core from 3.1 on, so the frontend advertises them unconditionally and an app + // may call glTexBuffer at any time; ES only gained them in 3.2, and before that only + // through EXT/OES_texture_buffer. The two extensions are functionally identical but + // their ESSL directives are NOT interchangeable, and SPIRV-Cross hardcodes the EXT + // spelling whenever it emits ESSL below 320 for a Dim=Buffer image - so a driver that + // ships only the OES spelling needs the emitted directive retargeted, and a driver with + // neither cannot compile such a shader at all. Gate on this, never on the entry point: + // eglGetProcAddress hands back live-looking stubs (see AcquireGLESFunctions). + enum class TextureBufferTier : Uint8 { + None = 0, // no core support and neither extension; glTexBuffer is unusable + CoreEs32, // ES >= 3.2, buffer textures are core and ESSL 320 needs no directive + ExtensionEXT, // GL_EXT_texture_buffer; ESSL below 320 must say GL_EXT_texture_buffer + ExtensionOES, // GL_OES_texture_buffer; ESSL below 320 must say GL_OES_texture_buffer + }; + TextureBufferTier TextureBufferSupport = TextureBufferTier::None; + // GL_MAX_TEXTURE_BUFFER_SIZE actually came back from the driver. False means the value + // below is MobileGL's own floor, not a driver answer: the pname is only legal once + // buffer textures exist, and querying it on a driver without them raises + // GL_INVALID_ENUM and leaves the default untouched. + Bool MaxTextureBufferSizeIsDriverReported = false; // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT of the host driver; only queried when the // extension above is present, and left at 1.0 (no anisotropy) otherwise. Float MaxTextureMaxAnisotropy = 1.0f; diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 5bd3d13a..d8c208b5 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -422,6 +422,57 @@ namespace MobileGL::MG_Util::SelfTest { "map array texture gets no driver storage at all, so sampling one reads nothing " "and rendering to one does not reach the screen"); } + // FAIL, not WARN: buffer textures are CORE in OpenGL 3.1 and MobileGL advertises a 4.x + // context, so an application may use one without asking - and nothing degrades + // gracefully when they are absent. The texture gets no driver storage (glTexBuffer does + // not exist), and, worse, every shader declaring a samplerBuffer fails to compile + // outright, because SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for + // it below ESSL 320. The program then never links and every draw using it silently + // draws nothing - which is how Minecraft 26.3, whose cloud layer is built entirely from + // gl_VertexID plus texelFetch on a GL_R8I buffer texture, loses its clouds. + // The limit is stated on every tier because it is the one number an application can + // read, and on the None tier it is knowingly a fiction (see below). + { + using Tier = MG_External::GLESCapabilities::TextureBufferTier; + const Int advertisedLimit = caps.MaxTextureBufferSize; + switch (caps.TextureBufferSupport) { + case Tier::CoreEs32: + builder.Pass("Buffer textures", + format("core in ES 3.2; GL_MAX_TEXTURE_BUFFER_SIZE = {} is the " + "driver's own answer, and ESSL 320 needs no #extension " + "directive to declare a samplerBuffer", + advertisedLimit)); + break; + case Tier::ExtensionEXT: + builder.Pass("Buffer textures", + format("GL_EXT_texture_buffer; GL_MAX_TEXTURE_BUFFER_SIZE = {} is " + "the driver's own answer, and the directive SPIRV-Cross " + "emits (GL_EXT_texture_buffer) is the one this driver wants", + advertisedLimit)); + break; + case Tier::ExtensionOES: + builder.Pass("Buffer textures", + format("GL_OES_texture_buffer; GL_MAX_TEXTURE_BUFFER_SIZE = {} is " + "the driver's own answer. SPIRV-Cross hardcodes the EXT " + "spelling, so MobileGL retargets the emitted #extension " + "directive to the OES one this driver advertises", + advertisedLimit)); + break; + case Tier::None: + default: + builder.Fail("Buffer textures", + format("not supported (pre-ES 3.2 without GL_EXT/OES_texture_buffer); " + "glTexBuffer does not exist, so a buffer texture gets no storage, " + "and any shader declaring a samplerBuffer fails to compile and " + "leaves its program unlinked - every draw using it is a silent " + "no-op. MobileGL still reports GL_MAX_TEXTURE_BUFFER_SIZE = {}: " + "the value is a floor it cannot honour, kept because an OpenGL " + "4.x context may not answer 0 and GL has no way to say that a " + "core feature is missing", + advertisedLimit)); + break; + } + } // Reported rather than probed: this one cannot come out any other way. OpenGL ES has no // double-precision vertex format and ESSL has no fp64 type, so there is no driver and no // extension that could make it work - the row exists so the loss is named at startup @@ -1728,6 +1779,29 @@ namespace MobileGL::MG_Util::SelfTest { } else { builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw"); } + // The Magma counterpart of the GLES "Buffer textures" row, so the two sections can be + // read side by side. Vulkan has no optional-feature bit here: a uniform texel buffer is + // core, and maxTexelBufferElements has a spec floor of 65536 - exactly the GL 3.1 floor + // for GL_MAX_TEXTURE_BUFFER_SIZE - so this backend can always back a buffer texture and + // the row exists to state the limit MobileGL derives its advertisement from, not to + // report a risk. A driver below the floor would be non-conformant, hence the Warn. + { + const Uint32 maxTexelBufferElements = properties.limits.maxTexelBufferElements; + constexpr Uint32 kGL31MinTextureBufferSize = 65536; + if (maxTexelBufferElements >= kGL31MinTextureBufferSize) { + builder.Pass("maxTexelBufferElements", + format("{}; uniform texel buffers are core in Vulkan, so buffer textures " + "need no extension and MobileGL advertises " + "GL_MAX_TEXTURE_BUFFER_SIZE from this limit", + maxTexelBufferElements)); + } else { + builder.Warn("maxTexelBufferElements", + format("{} (< {}); below the OpenGL 3.1 floor for " + "GL_MAX_TEXTURE_BUFFER_SIZE, so a conformant application may " + "create a buffer texture larger than this driver can view", + maxTexelBufferElements, kGL31MinTextureBufferSize)); + } + } { VkImageFormatProperties sliceProbe{}; const Bool sliceCapable = diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index e9bd54e3..91f4b440 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -30,6 +30,8 @@ #include "SpirvPasses/LegalizeFragmentOutputIndexPass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" +#include "source/opt/build_module.h" +#include "source/opt/ir_context.h" #include "ShaderSourceProcessor.h" #include @@ -538,6 +540,32 @@ namespace MobileGL { return g_spirvValidationFailures.load(std::memory_order_relaxed); } + Bool ShaderCompiler::ModuleDeclaresBufferTextureSampler(const Vector& spirv) { + // Callers gate this on the driver LACKING buffer textures, so the module build + // here only ever happens on a degraded driver that is about to fail the compile + // anyway - it is not on the healthy path. + std::unique_ptr context = spvtools::BuildModule( + SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresBufferTextureSampler"), + spirv.data(), spirv.size()); + if (!context) { + // Unparseable here means unusable downstream too; let the ordinary transpile + // path produce the error rather than inventing a capability verdict from it. + return false; + } + for (const spvtools::opt::Instruction& type : context->types_values()) { + if (type.opcode() != spv::Op::OpTypeImage) { + continue; + } + // OpTypeImage operands: Sampled Type, Dim, Depth, Arrayed, MS, Sampled, Format. + // Dim is operand 1; SpvDimBuffer is what samplerBuffer/isamplerBuffer/ + // usamplerBuffer all lower to, whatever their sampled type. + if (static_cast(type.GetSingleWordInOperand(1)) == spv::Dim::Buffer) { + return true; + } + } + return false; + } + bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector& inputBinary, Vector& outputBinary) { using namespace spvtools; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 85449154..6d7f6c37 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -133,6 +133,16 @@ namespace MobileGL { // total. static Uint64 SpirvValidationFailureCount(); static Uint64 NoteSpirvValidationFailure(); + + // True when the module declares any buffer-texture sampler - a samplerBuffer, + // isamplerBuffer or usamplerBuffer, i.e. an OpTypeImage with Dim = Buffer. + // DirectGLES asks before handing the transpiled ESSL to the driver: buffer + // textures are core in the OpenGL 3.1+ context MobileGL advertises but need + // ES 3.2 or EXT/OES_texture_buffer on the host, and on a driver without them + // SPIRV-Cross's `#extension ... : require` makes the shader uncompilable. The + // check exists so that failure can be reported as the missing capability it is, + // naming the shader, rather than as a driver info log nobody sees. + static Bool ModuleDeclaresBufferTextureSampler(const Vector& spirv); }; } // namespace ShaderTranspiler } // namespace MG_Util