From 01098e9dd71a77ac9d22b74b781f80b70e06f0d5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:45:12 -0400 Subject: [PATCH 1/7] [Fix] (GLImpl): raise the errors ARB_sync specifies for ClientWaitSync, WaitSync and GetSynciv --- MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp | 44 +++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp b/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp index e18b8120..39da96d5 100644 --- a/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp @@ -69,8 +69,25 @@ namespace MobileGL::MG_Impl::GLImpl { } GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) { + // GL 4.6 core 4.1.1: GL_SYNC_FLUSH_COMMANDS_BIT is the only bit this call accepts, and + // any other bit is INVALID_VALUE. Silently ignoring the stray bits used to make a caller + // that passed, say, GL_SYNC_GPU_COMMANDS_COMPLETE by mistake think it had asked for a + // flush it never got. + if ((flags & ~static_cast(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "flags must be zero or GL_SYNC_FLUSH_COMMANDS_BIT.")); + return GL_WAIT_FAILED; + } const auto* syncObject = FindSyncObject(sync); if (!syncObject) { + // The spec pairs the GL_WAIT_FAILED return with a recorded INVALID_VALUE; returning + // the enum alone left glGetError() clean and the failure indistinguishable from a + // genuine wait failure on a live sync. + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object.")); return GL_WAIT_FAILED; } const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync; @@ -95,6 +112,9 @@ namespace MobileGL::MG_Impl::GLImpl { } const auto* syncObject = FindSyncObject(sync); if (!syncObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object.")); return; } const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync; @@ -125,8 +145,22 @@ namespace MobileGL::MG_Impl::GLImpl { } void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) { + // GL 4.6 core 4.1: a negative bufSize is INVALID_VALUE, an unnamed sync is INVALID_VALUE + // and an unrecognised pname is INVALID_ENUM. All three used to leave glGetError() clean + // and write a plausible-looking zero, which is the one failure mode a caller cannot tell + // apart from a real answer - GL_SYNC_STATUS legitimately answers GL_UNSIGNALED (0x9118), + // but a mistyped pname answered a bare 0 that no query ever returns. + if (bufSize < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "bufSize must not be negative.")); + return; + } const auto* syncObject = FindSyncObject(sync); if (!syncObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object.")); if (length) { *length = 0; } @@ -152,7 +186,15 @@ namespace MobileGL::MG_Impl::GLImpl { value = static_cast(syncObject->flags); break; default: - break; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "pname must be GL_OBJECT_TYPE, GL_SYNC_STATUS, GL_SYNC_CONDITION or " + "GL_SYNC_FLAGS.")); + if (length) { + *length = 0; + } + return; } if (length) { From 6a8bf4c03cc2b933adf505f81cc142edf58ca272 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:45:13 -0400 Subject: [PATCH 2/7] [Fix, Test] (DirectVulkan): resolve a lowered atomic-counter block from the atomic-counter binding points --- .../DirectVulkan/Renderer/UniformManager.cpp | 41 +++++++++++++++---- .../Scenarios/AtomicCounterScenario.cpp | 11 ----- MobileGL/MG_Util/ShaderTranspiler/Types.h | 22 ++++++++++ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index f01a29f7..bd0c42d5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -17,6 +17,7 @@ #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Metrics/TextureMetrics.h" +#include "MG_Util/ShaderTranspiler/Types.h" #include #include #include @@ -923,22 +924,46 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Int blockIndex = programObj.storageBlockIndexByBinding[binding]; MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u", binding); + // An atomic counter is not an SSBO the application ever declared: glslang lowers every + // atomic_uint onto a synthesized gl_AtomicCounterBlock_ storage block, where N is the + // GL ATOMIC-COUNTER binding. That block arrives here auto-mapped to an arbitrary + // storage-block slot, so resolving it the SSBO way looked up GL_SHADER_STORAGE_BUFFER + // point N' - which is never where glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, N, ...) put + // the buffer. The counter therefore never reached the shader (KHR-GL43 + // shader_atomic_counters.advanced-usage-*), and when the application also bound an SSBO at + // the colliding slot the descriptor silently aliased it, so the dispatch wrote over the + // application's own buffer. DirectGLES has always taken this branch explicitly + // (SyncAtomicCounterBuffers); this is the same rule in Magma's descriptor resolution. + // + // Only the SOURCE of the handle differs. The per-counter layout(offset=) is already folded + // into the block's SPIR-V member offsets on this path (FlattenAtomicCounterBlockPass is + // DirectGLES-only), so everything below - residency, the glBindBufferRange window, the + // descriptor fill - is target-agnostic and stays exactly as it was. + const String& blockName = programObj.storageBlockNameByBinding[binding]; + const Int atomicCounterBinding = MG_Util::ShaderTranspiler::AtomicCounterBlockGlBinding(blockName); + const Bool isAtomicCounterBlock = atomicCounterBinding >= 0; + const BufferTarget bufferTarget = + isAtomicCounterBlock ? BufferTarget::AtomicCounter : BufferTarget::ShaderStorage; // A block instance array declares one block whose elements take consecutive GL binding // points from the declared one (GL 4.6 core 7.8), and the reflection collapses the whole - // array to that one block - so the element index IS the offset from its binding. + // array to that one block - so the element index IS the offset from its binding. glslang + // synthesizes one counter block per GL binding, so a counter block is never an instance + // array and `element` is always 0 there; the +element rule stays with the SSBO case. const GLuint frontendBinding = - GetShaderStorageBlockBinding(program, static_cast(blockIndex)) + element; + isAtomicCounterBlock + ? static_cast(atomicCounterBinding) + : GetShaderStorageBlockBinding(program, static_cast(blockIndex)) + element; const Uint32 bindingPointCount = - static_cast(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage)); + static_cast(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget)); MOBILEGL_ASSERT(frontendBinding < bindingPointCount, - "ResolveStorageBufferDescriptor: frontend SSBO binding %u out of range for block '%s'", - frontendBinding, programObj.storageBlockNameByBinding[binding].c_str()); + "ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'", + frontendBinding, blockName.c_str()); - auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding); + auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding); const auto& bufferObject = bindingPoint.GetBoundObject(); if (bufferObject == nullptr) { - MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'", - frontendBinding, programObj.storageBlockNameByBinding[binding].c_str()); + MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no buffer bound at frontend binding %u for block '%s'", + frontendBinding, blockName.c_str()); return false; } diff --git a/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp index 55884664..7d0de004 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp @@ -87,11 +87,6 @@ void main() { << " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers << "; this needs 3 and 2"; } - if (!AtomicCountersAreWired()) { - GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName() - << " yet: glslang lowers them onto a storage block and that block's descriptor " - << "is still resolved from the shader-storage binding points"; - } m_program = CompileComputeProgram(kCounterComputeSource); ASSERT_NE(m_program, 0u) << m_buildLog; } @@ -105,12 +100,6 @@ void main() { m_program = 0; } - // Magma binds the lowered block as an ordinary storage-buffer descriptor resolved - // from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The - // frontend half (limits, reflection queries, the link-time offset rules) is - // backend-agnostic and is covered by the unit suites; only the VALUE is scoped here. - bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; } - unsigned int CompileComputeProgram(const char* source) { const GLuint shader = glCreateShader(GL_COMPUTE_SHADER); glShaderSource(shader, 1, &source, nullptr); diff --git a/MobileGL/MG_Util/ShaderTranspiler/Types.h b/MobileGL/MG_Util/ShaderTranspiler/Types.h index 13c628be..0b7dc74b 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/Types.h +++ b/MobileGL/MG_Util/ShaderTranspiler/Types.h @@ -20,6 +20,28 @@ namespace MobileGL { // buffer, and the trailing number is the only place the GL binding survives. inline constexpr const char* ATOMIC_COUNTER_BLOCK_PREFIX = "gl_AtomicCounterBlock"; + // "gl_AtomicCounterBlock_5" -> 5; -1 for any name that is not one of these blocks. + // Recovering N from the NAME is not a shortcut, it is the only way: the block reaches + // a backend auto-mapped to whatever storage-block slot the IO mapper had free, and + // that number has no relation to the GL atomic-counter binding the application asked + // for (see TMglGlslIoResolver). A backend that resolves the block from the + // shader-storage binding points therefore binds the wrong buffer - or, worse, the + // application's own SSBO at the same slot. + inline Int AtomicCounterBlockGlBinding(StringView name) { + const SizeT prefixLength = StringView(ATOMIC_COUNTER_BLOCK_PREFIX).size(); + // Needs the prefix, the '_' and at least one digit. + if (name.size() <= prefixLength + 1) return -1; + if (name.compare(0, prefixLength, ATOMIC_COUNTER_BLOCK_PREFIX) != 0) return -1; + if (name[prefixLength] != '_') return -1; + Int binding = 0; + for (SizeT i = prefixLength + 1; i < name.size(); ++i) { + if (name[i] < '0' || name[i] > '9') return -1; + binding = binding * 10 + (name[i] - '0'); + if (binding > 0x0FFFFFFF) return -1; // absurd suffix; treat as not-a-counter + } + return binding; + } + // Atomic-counter limits, in ONE place because GL 4.6 requires glGetIntegerv and the // shading language's gl_MaxAtomicCounter* constants to report the same numbers // (KHR-GL43.shader_atomic_counters.basic-glsl-built-in compares them directly). From 50d260c84033fa0dc3acd0a03e6d13ed44c58bc5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:45:13 -0400 Subject: [PATCH 3/7] [Fix] (GLImpl): reject memory-barrier bits the spec does not define --- .../MG_Impl/GLImpl/Drawing/GL_Drawing.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 11eea70e..10b6103e 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -713,7 +713,34 @@ namespace MobileGL::MG_Impl::GLImpl { } } + namespace { + // GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the + // barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with + // GL_ALL_BARRIER_BITS - which is 0xFFFFFFFF, not the union of the list - accepted whole. + // Forwarding an undefined bit to the host driver let a caller that had computed its mask + // wrongly (or reused an ES-only bit) get silence instead of the error the spec promises. + constexpr GLbitfield kAllDefinedBarrierBits = + GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT | + GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_COMMAND_BARRIER_BIT | + GL_PIXEL_BUFFER_BARRIER_BIT | GL_TEXTURE_UPDATE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT | + GL_FRAMEBUFFER_BARRIER_BIT | GL_TRANSFORM_FEEDBACK_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT | + GL_SHADER_STORAGE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_QUERY_BUFFER_BARRIER_BIT; + + Bool ValidateMemoryBarrierBits(const char* function, GLbitfield barriers) { + if (barriers == GL_ALL_BARRIER_BITS) return true; + if ((barriers & ~kAllDefinedBarrierBits) != 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", function, + "barriers contains bits that are not defined barrier bits.")); + return false; + } + return true; + } + } // namespace + void MemoryBarrier(GLbitfield barriers) { + if (!ValidateMemoryBarrierBits(__func__, barriers)) return; auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier; if (!memoryBarrier) { MG_State::pGLContext->RecordError( @@ -725,6 +752,7 @@ namespace MobileGL::MG_Impl::GLImpl { } void MemoryBarrierByRegion(GLbitfield barriers) { + if (!ValidateMemoryBarrierBits(__func__, barriers)) return; auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion; if (!memoryBarrierByRegion) { MG_State::pGLContext->RecordError( From 6b25e7a7e323c3c4966fbc3e6cfbb794079001b6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:45:13 -0400 Subject: [PATCH 4/7] [Fix] (GLState): report the storage flags glBufferData implies --- MobileGL/MG_State/GLState/BufferState/BufferObject.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 1ca335b7..276f01fa 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -118,7 +118,13 @@ namespace MobileGL::MG_State::GLState { // record that so backends skip uploading the stale shadow bytes. m_hasDefinedContent = (data != nullptr) || size == 0; m_isImmutableStorage = false; - m_storageFlags = 0; + // GL 4.6 core 6.2 defines glBufferData as glBufferStorage with + // DYNAMIC_STORAGE_BIT | MAP_READ_BIT | MAP_WRITE_BIT, so GL_BUFFER_STORAGE_FLAGS has to + // report those three afterwards. Reporting 0 - the value that belongs to a buffer whose + // store has never been specified - told an application that a perfectly writable + // glBufferData buffer accepted neither glBufferSubData nor a map. Only the IMMUTABLE flag + // distinguishes the two cases, and it is cleared just above. + m_storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT; NotifyRespecify(); } From 0fdcb5d6c470372e651fbcfdd1b6a28a5a73f83a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:45:13 -0400 Subject: [PATCH 5/7] [Feat] (DirectGLES, DirectVulkan): advertise GL_ARB_sync and GL_ARB_shader_atomic_counters --- .../DirectGLES/BackendObject_DirectGLES.cpp | 16 ++++++++++++++++ .../DirectVulkan/BackendObject_DirectVulkan.cpp | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 99164716..7daf5713 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1030,6 +1030,22 @@ namespace MobileGL::MG_Backend::DirectGLES { // was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests // NotSupported on a feature that works. E_GL_ARB_draw_elements_base_vertex, + // The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync, + // glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a + // backend fence (a host GLsync here, a VkFence on DirectVulkan), and glGetInteger64v + // answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason + // ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension + // list, and a caller that finds GL_ARB_sync missing never resolves the entry points - + // then calls through null if it uses fences anyway. Nothing in the CTS gates on this + // string, so it is advertised on the strength of the implementation, not a test unlock. + E_GL_ARB_sync, + // Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole + // GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and SyncAtomicCounterBuffers + // re-issues the counter buffer as an SSBO binding in the range reserved at the top of + // the ES driver's shader-storage points, so a counter dispatch reads and writes the + // buffer the application bound. DirectVulkan reaches the same place through its own + // descriptor resolution, so the string is symmetric. + E_GL_ARB_shader_atomic_counters, // glVertexAttribDivisor, core since 3.3 and real on both backends. Applications // (Better Clouds' GLCompat among them) accept the extension string as an // ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index c0eb108b..0e632946 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -550,6 +550,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { // was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests // NotSupported on a feature that works. E_GL_ARB_draw_elements_base_vertex, + // The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync, + // glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a + // backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v + // answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason + // ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension + // list, and a caller that finds GL_ARB_sync missing never resolves the entry points - + // then calls through null if it uses fences anyway. Nothing in the CTS gates on this + // string, so it is advertised on the strength of the implementation, not a test unlock. + E_GL_ARB_sync, + // Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole + // GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer + // now reaches the shader on BOTH backends - Magma resolves the lowered + // gl_AtomicCounterBlock_ from the atomic-counter binding points rather than the + // shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that + // landed, because the counter silently read whatever was bound as SSBO N instead. + E_GL_ARB_shader_atomic_counters, // glVertexAttribDivisor, core since 3.3 and real on both backends. Applications // (Better Clouds' GLCompat among them) accept the extension string as an // ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is From dc543fa90536d6da3ff6816f78eb0ed4f333e613 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:54:17 -0400 Subject: [PATCH 6/7] [Feat, Test] (DirectGLES, DirectVulkan, SelfTest): advertise the implemented extensions that were never named --- .../DirectGLES/BackendObject_DirectGLES.cpp | 58 ++++++++++++++- .../DirectGLES/BackendObject_DirectGLES.h | 2 +- .../BackendObject_DirectVulkan.cpp | 59 ++++++++++++++- .../DirectVulkan/BackendObject_DirectVulkan.h | 3 +- .../BackendLoader/BackendLoaderTest.cpp | 72 ++++++++++++++++--- .../Program/ParallelShaderCompileTest.cpp | 8 +-- MobileGL/MG_Util/SelfTest/DriverPost.cpp | 7 +- 7 files changed, 186 insertions(+), 23 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 7daf5713..a1e7c356 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -753,7 +753,7 @@ namespace MobileGL::MG_Backend::DirectGLES { .TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version // Baseline advertisement (no runtime capabilities yet); reconciled once // the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions. - .Extensions = BuildAdvertisedExtensions(false, false, false, false, false), + .Extensions = BuildAdvertisedExtensions(false, false, false, false, false, false), .IsCompatibilityProfile = false // Is Compatibility Profile }, .StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability @@ -778,7 +778,7 @@ namespace MobileGL::MG_Backend::DirectGLES { AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy, capabilities.SupportsDrawIndirect, capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance, - capabilities.SupportsTextureView); + capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray); } } // namespace @@ -992,7 +992,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported, Bool drawIndirectSupported, Bool nonZeroIndirectBaseInstanceSupported, - Bool textureViewSupported) { + Bool textureViewSupported, Bool cubeMapArraySupported) { Vector extensions = { // The version tokens have to reach the version the backend actually claims: // TargetGLVersion is {4,3,0}, and a list that stopped at OpenGL40 told an @@ -1055,6 +1055,46 @@ namespace MobileGL::MG_Backend::DirectGLES { // object-label table are MobileGL's own state, not the host driver's - so it is as // available here as it is on DirectVulkan, which has advertised it all along. E_GL_KHR_debug, + // Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has + // existed, and that was simply never named. None of these unlocks a single CTS case - + // the conformance suite reaches all of them through the version - so they are + // advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the + // string set, and an application that gates its ENTRY POINTS on the string rather than + // on the version never resolves them and then calls through null. Each is backed by + // the entry points named beside it. + // + // glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray. + E_GL_ARB_vertex_array_object, + // The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the + // integer-valued Iiv/Iuiv forms. + E_GL_ARB_sampler_objects, + // glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent + // maps are already built on top of. + E_GL_ARB_map_buffer_range, + // glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets. + E_GL_ARB_copy_buffer, + // glCopyImageSubData, wired to a real backend hook on both backends. + E_GL_ARB_copy_image, + // GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which this backend syncs through to the ES + // driver's identical parameters. + E_GL_ARB_texture_swizzle, + // GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus + // the eight glVertexAttribP* entry points. + E_GL_ARB_vertex_type_2_10_10_10_rev, + // The R/RG internal formats. Named separately from the float ones because an + // application may check either. + E_GL_ARB_texture_rg, + // GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8. + E_GL_ARB_depth_buffer_float, + // The floating-point colour formats. Unlike the rest of this block this string DOES + // gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no + // core-version fallback, so eight cases per version list were NotSupported on formats + // the backend has always had. + E_GL_ARB_texture_float, + // glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} / + // glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the + // 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation. + E_GL_ARB_viewport_array, // Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the // extension explicitly permits. It is also the only thing that // exposes glProgramParameteri before GL 4.1. @@ -1103,6 +1143,18 @@ namespace MobileGL::MG_Backend::DirectGLES { if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) { extensions.push_back(E_GL_ARB_timer_query); } + // Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without + // EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage + // and a samplerCubeArray shader does not even compile, which is exactly what the POST + // reports. So the string follows the host capability rather than the version. + // + // It is worth naming even though cube map arrays are core at the version claimed, because + // KHR-GL4*.texture_gather.plain-gather-*-cube-array checks the STRING and has no + // core-version fallback - five cases per version list sat NotSupported on a device that + // supports the feature. + if (cubeMapArraySupported) { + extensions.push_back(E_GL_ARB_texture_cube_map_array); + } // Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core // texture views at any version and no honest emulation exists: a view is a SECOND NAME // over the SAME storage, so that writes through either are visible through the other and diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index 07463120..8f3fd0c6 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -83,7 +83,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported, Bool drawIndirectSupported, Bool nonZeroIndirectBaseInstanceSupported, - Bool textureViewSupported); + Bool textureViewSupported, Bool cubeMapArraySupported); // Format: , OpenGL ES . — the exact string an // initialized backend returns from GetBackendAPIVersionString (and that ends up diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 0e632946..3019ba6f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -504,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { .TargetGLSLVersion = {4, 6, 0}, // Baseline advertisement (no runtime-gated capabilities); a live // backend reconciles its copy in UpdateAdvertisedExtensions. - .Extensions = BuildAdvertisedExtensions(false, false, false, false), + .Extensions = BuildAdvertisedExtensions(false, false, false, false, false), .IsCompatibilityProfile = false}, .StaticBackendCapability = {.AllowVSOnlyPrograms = false}}; return rendererInfo; @@ -512,7 +512,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, Bool anisotropicFilteringSupported, - Bool nonZeroIndirectBaseInstanceSupported) { + Bool nonZeroIndirectBaseInstanceSupported, + Bool cubeMapArraySupported) { Vector extensions = { // The version tokens have to reach the version the backend actually claims: // TargetGLVersion is {4,3,0}, and a list that stopped at OpenGL40 told an @@ -571,6 +572,46 @@ namespace MobileGL::MG_Backend::DirectVulkan { // ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is // available, so withholding it makes MobileGL look less capable than it is. E_GL_ARB_instanced_arrays, + // Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has + // existed, and that was simply never named. None of these unlocks a single CTS case - + // the conformance suite reaches all of them through the version - so they are + // advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the + // string set, and an application that gates its ENTRY POINTS on the string rather than + // on the version never resolves them and then calls through null. Each is backed by + // the entry points named beside it. Kept identical to the DirectGLES block so the two + // backends do not disagree about what MobileGL is. + // + // glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray. + E_GL_ARB_vertex_array_object, + // The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the + // integer-valued Iiv/Iuiv forms. + E_GL_ARB_sampler_objects, + // glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent + // maps are already built on top of. + E_GL_ARB_map_buffer_range, + // glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets. + E_GL_ARB_copy_buffer, + // glCopyImageSubData, wired to a real backend hook on both backends. + E_GL_ARB_copy_image, + // GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle. + E_GL_ARB_texture_swizzle, + // GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus + // the eight glVertexAttribP* entry points. + E_GL_ARB_vertex_type_2_10_10_10_rev, + // The R/RG internal formats. Named separately from the float ones because an + // application may check either. + E_GL_ARB_texture_rg, + // GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8. + E_GL_ARB_depth_buffer_float, + // The floating-point colour formats. Unlike the rest of this block this string DOES + // gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no + // core-version fallback, so eight cases per version list were NotSupported on formats + // the backend has always had. + E_GL_ARB_texture_float, + // glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} / + // glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the + // 16 viewports GL_MAX_VIEWPORTS reports. + E_GL_ARB_viewport_array, // Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the // extension explicitly permits. It is also the only thing that // exposes glProgramParameteri before GL 4.1. @@ -623,6 +664,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { extensions.push_back(E_GL_EXT_texture_filter_anisotropic); extensions.push_back(E_GL_ARB_texture_filter_anisotropic); } + // A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that + // view type cannot be created without the imageCubeArray device feature - so the string + // follows the feature, not the version, exactly as the per-layer attachment bit does. + // + // Worth naming even though cube map arrays are core at the version claimed, because + // KHR-GL4*.texture_gather.plain-gather-*-cube-array checks the STRING and has no + // core-version fallback - five cases per version list sat NotSupported on a device that + // supports the feature. + if (cubeMapArraySupported) { + extensions.push_back(E_GL_ARB_texture_cube_map_array); + } return extensions; } @@ -753,7 +805,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions( subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(), pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(), - pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported()); + pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(), + m_vulkanCaps.SupportsImageCubeArray); } void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() { diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index c64d57cb..91c6e54c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -75,7 +75,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // the detected device support (passing an already-gated value is harmless). Vector BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, Bool anisotropicFilteringSupported, - Bool nonZeroIndirectBaseInstanceSupported); + Bool nonZeroIndirectBaseInstanceSupported, + Bool cubeMapArraySupported); // Format: , Vulkan , Driver — the exact // string an initialized backend returns from GetBackendAPIVersionString (and that diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index 7b5706a9..80046586 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -1087,22 +1087,76 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end(); }; - const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false); + const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false); EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic)); EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic)); - const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false, false); + const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false, false, false); EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic)); EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic)); // Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature. - const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false); + const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false); EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic)); - const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false); + const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false, false); EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic)); EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic)); } +// Cube map arrays are core at the version MobileGL claims, but there is nothing underneath on a +// pre-ES-3.2 driver without EXT/OES_texture_cube_map_array, and no VK_IMAGE_VIEW_TYPE_CUBE_ARRAY +// without the imageCubeArray feature. The string has to follow the capability on both backends - +// and it has to BE there when the capability is, because KHR-GL4*.texture_gather.*-cube-array +// gates on the string with no core-version fallback. +TEST(CubeMapArrayAdvertisement, FollowsTheHostCapabilityOnBothBackends) { + const auto contains = [](const MobileGL::Vector& extensions, + MobileGL::GLExtension wanted) { + return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end(); + }; + + const auto esWithout = + MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false); + EXPECT_FALSE(contains(esWithout, MobileGL::E_GL_ARB_texture_cube_map_array)); + const auto esWith = + MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, true); + EXPECT_TRUE(contains(esWith, MobileGL::E_GL_ARB_texture_cube_map_array)); + + const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, + false); + EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_ARB_texture_cube_map_array)); + const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, true); + EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_cube_map_array)); +} + +// The core-plumbing strings carry no capability gate: they name entry points that have been real +// on both backends for as long as the backends have existed, and an application that gates its +// entry-point resolution on the string (LWJGL does) would otherwise call through null. Pinned +// together so a future edit cannot quietly drop one, and pinned on BOTH backends so the two +// cannot disagree about what MobileGL is. +TEST(CorePlumbingAdvertisement, IsUnconditionalAndIdenticalOnBothBackends) { + const auto contains = [](const MobileGL::Vector& extensions, + MobileGL::GLExtension wanted) { + return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end(); + }; + const MobileGL::GLExtension expected[] = { + MobileGL::E_GL_ARB_sync, MobileGL::E_GL_ARB_shader_atomic_counters, + MobileGL::E_GL_ARB_vertex_array_object, MobileGL::E_GL_ARB_sampler_objects, + MobileGL::E_GL_ARB_map_buffer_range, MobileGL::E_GL_ARB_copy_buffer, + MobileGL::E_GL_ARB_copy_image, MobileGL::E_GL_ARB_texture_swizzle, + MobileGL::E_GL_ARB_vertex_type_2_10_10_10_rev, MobileGL::E_GL_ARB_texture_rg, + MobileGL::E_GL_ARB_depth_buffer_float, MobileGL::E_GL_ARB_texture_float, + MobileGL::E_GL_ARB_viewport_array}; + + // Every gate off: none of these may depend on one. + const auto es = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, + false); + const auto vk = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false); + for (const auto extension : expected) { + EXPECT_TRUE(contains(es, extension)) << "DirectGLES stopped advertising extension " << extension; + EXPECT_TRUE(contains(vk, extension)) << "DirectVulkan stopped advertising extension " << extension; + } +} + // Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised // ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain // indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate. @@ -1113,27 +1167,27 @@ TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) { }; const auto esWithoutIndirect = - MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false); + MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false); EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect)); EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance)); const auto esWithoutBaseInstance = - MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false, false); + MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false, false, false); EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect)); EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance)); const auto esWithBoth = - MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true, false); + MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true, false, false); EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect)); EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance)); const auto vkWithoutBaseInstance = - MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false); + MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false); EXPECT_TRUE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect)); EXPECT_FALSE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance)); const auto vkWithBoth = - MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true); + MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true, false); EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_draw_indirect)); EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_base_instance)); } diff --git a/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp b/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp index 098d9ed4..44b73cc9 100644 --- a/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp +++ b/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp @@ -516,17 +516,17 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) { { const AsyncModeScope async(true); - EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false), + EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false), E_GL_KHR_parallel_shader_compile)); - EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false), + EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false), E_GL_KHR_parallel_shader_compile)); } { const AsyncModeScope async(false); - EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false), + EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false, false), E_GL_KHR_parallel_shader_compile)) << "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading"; - EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false), + EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false, false), E_GL_KHR_parallel_shader_compile)) << "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading"; } diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 7e8ef6c9..fdca43d4 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -1269,7 +1269,7 @@ namespace MobileGL::MG_Util::SelfTest { summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy, summary.caps.SupportsDrawIndirect, summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance, - summary.caps.SupportsTextureView)); + summary.caps.SupportsTextureView, summary.caps.SupportsTextureCubeMapArray)); } AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString, advertisedExtensions); @@ -2009,6 +2009,7 @@ namespace MobileGL::MG_Util::SelfTest { Bool samplerAnisotropySupported = false; Bool drawIndirectFirstInstanceSupported = false; Bool shaderDrawParametersSupported = false; + Bool imageCubeArraySupported = false; }; } // namespace @@ -2300,6 +2301,7 @@ namespace MobileGL::MG_Util::SelfTest { VkPhysicalDeviceFeatures features{}; vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features); summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE; + summary.imageCubeArraySupported = features.imageCubeArray == VK_TRUE; summary.drawIndirectFirstInstanceSupported = features.drawIndirectFirstInstance == VK_TRUE; if (features.multiDrawIndirect == VK_TRUE) { builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands"); @@ -2721,7 +2723,8 @@ namespace MobileGL::MG_Util::SelfTest { summary.deviceName, summary.apiVersionString, summary.driverVersionString); advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions( summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported, - summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported)); + summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported, + summary.imageCubeArraySupported)); } AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString, advertisedExtensions); From 9bcf0a15a00e5e02a2e82363d09388818b7e8a80 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 22:03:26 -0400 Subject: [PATCH 7/7] [Docs] (DirectGLES, DirectVulkan): correct what advertising cube map arrays actually unlocks --- .../MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp | 9 +++++---- .../DirectVulkan/BackendObject_DirectVulkan.cpp | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index a1e7c356..5b567dd9 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1148,10 +1148,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // and a samplerCubeArray shader does not even compile, which is exactly what the POST // reports. So the string follows the host capability rather than the version. // - // It is worth naming even though cube map arrays are core at the version claimed, because - // KHR-GL4*.texture_gather.plain-gather-*-cube-array checks the STRING and has no - // core-version fallback - five cases per version list sat NotSupported on a device that - // supports the feature. + // Named for the application's benefit rather than the suite's: measured on Adreno 830, + // KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so + // this unlocks no conformance case. It is advertised because the feature is real and + // because an application that feature-detects cube map arrays off the string (rather than + // off the 4.0 version) would otherwise decline a path this backend serves. if (cubeMapArraySupported) { extensions.push_back(E_GL_ARB_texture_cube_map_array); } diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 3019ba6f..554e153e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -668,10 +668,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // view type cannot be created without the imageCubeArray device feature - so the string // follows the feature, not the version, exactly as the per-layer attachment bit does. // - // Worth naming even though cube map arrays are core at the version claimed, because - // KHR-GL4*.texture_gather.plain-gather-*-cube-array checks the STRING and has no - // core-version fallback - five cases per version list sat NotSupported on a device that - // supports the feature. + // Named for the application's benefit rather than the suite's: measured on Adreno 830, + // KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so + // this unlocks no conformance case. It is advertised because the feature is real and + // because an application that feature-detects cube map arrays off the string (rather than + // off the 4.0 version) would otherwise decline a path this backend serves. if (cubeMapArraySupported) { extensions.push_back(E_GL_ARB_texture_cube_map_array); }