// MobileGL - MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp // Copyright (c) 2025-2026 MobileGL-Dev // Licensed under the GNU Lesser General Public License v3.0: // https://www.gnu.org/licenses/gpl-3.0.txt // https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header #include "BackendObject_DirectGLES.h" #include "MG_Backend/BackendObject.h" #include "MG_Backend/BackendObjects.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MobileGL::MG_Backend::DirectGLES { namespace { Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { (void)dpy; return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT; } // ID-54 / ID-67 (v1, under ID-52/ID-59's grant for this file). Which VIRTUAL (dpy, draw, // read, ctx) the process's one native ES context is currently bound FOR, on the apply // thread. DirectGLES has one native context and one native surface (g_Context, g_Surface) // whatever virtual handles the client uses, so "is a native eglMakeCurrent needed" is never // "is the native triple different" - it is "did the VIRTUAL context change": // DirectGLES::MakeCurrent is also where the seven caches that describe the frontend context // are invalidated, and a different virtual context needs them invalidated even though the // driver binds the same triple. ID-67: a make-current with a DIFFERENT tuple is a real // native bind (and a caps republish, ServerLoop's half); an IDENTICAL one is neither. // // Three states. NotBound: the next bind is native. FreshFromSurfaceCreation: the surface's // own creation (InitPbufferSurface / InitWindowSurface) bound natively and invalidated, and // no virtual tuple has claimed that bind yet - the first tuple adopts it, which is the // "2 -> 1 native binds per process" of round 3. BoundForTuple: bound and invalidated for // the recorded tuple; only that exact tuple may skip. Process-wide like the native state it // mirrors; under split exactly one DirectGLES object exists (the server's), and only the // apply thread reaches these. enum class NativeBindState : Uint8 { NotBound, FreshFromSurfaceCreation, BoundForTuple }; NativeBindState g_nativeBindState = NativeBindState::NotBound; EGLDisplay g_nativeBoundDpy = EGL_NO_DISPLAY; EGLSurface g_nativeBoundDraw = EGL_NO_SURFACE; EGLSurface g_nativeBoundRead = EGL_NO_SURFACE; EGLContext g_nativeBoundCtx = EGL_NO_CONTEXT; void NoteNativeContextGone() { g_nativeBindState = NativeBindState::NotBound; } void NoteNativeContextFreshFromSurfaceCreation() { g_nativeBindState = NativeBindState::FreshFromSurfaceCreation; } void NoteNativeBoundFor(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { g_nativeBindState = NativeBindState::BoundForTuple; g_nativeBoundDpy = dpy; g_nativeBoundDraw = draw; g_nativeBoundRead = read; g_nativeBoundCtx = ctx; } Bool NativeBindCanBeSkippedFor(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { switch (g_nativeBindState) { case NativeBindState::NotBound: return false; case NativeBindState::FreshFromSurfaceCreation: return true; case NativeBindState::BoundForTuple: return g_nativeBoundDpy == dpy && g_nativeBoundDraw == draw && g_nativeBoundRead == read && g_nativeBoundCtx == ctx; } return false; } void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) { if (!gl.glGetError) return; while (gl.glGetError() != GL_NO_ERROR) {} } Bool CheckNoGLError(const MG_External::GLESFunctionsTable& gl) { return !gl.glGetError || gl.glGetError() == GL_NO_ERROR; } GLenum GetTextureBindingQuery(TextureTarget target) { switch (target) { case TextureTarget::Texture2D: return GL_TEXTURE_BINDING_2D; case TextureTarget::Texture3D: return GL_TEXTURE_BINDING_3D; case TextureTarget::TextureCubeMap: return GL_TEXTURE_BINDING_CUBE_MAP; case TextureTarget::Texture2DArray: return GL_TEXTURE_BINDING_2D_ARRAY; case TextureTarget::TextureCubeMapArray: return GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; case TextureTarget::Texture2DMultisample: return GL_TEXTURE_BINDING_2D_MULTISAMPLE; case TextureTarget::Texture2DMultisampleArray: return GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY; default: return GL_UNKNOWN_MGL; } } Bool IsGLESProbeTextureTarget(TextureTarget target) { switch (target) { case TextureTarget::Texture2D: case TextureTarget::Texture3D: case TextureTarget::TextureCubeMap: case TextureTarget::Texture2DArray: case TextureTarget::TextureCubeMapArray: case TextureTarget::Texture2DMultisample: case TextureTarget::Texture2DMultisampleArray: return true; default: return false; } } Bool IsGLESProbeMultisampleTarget(TextureTarget target) { return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray; } GLenum GetFramebufferAttachment(TextureInternalFormat format) { const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(format); const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(format); if (isDepth && isStencil) return GL_DEPTH_STENCIL_ATTACHMENT; if (isDepth) return GL_DEPTH_ATTACHMENT; if (isStencil) return GL_STENCIL_ATTACHMENT; return GL_COLOR_ATTACHMENT0; } FormatCapabilityFlags GetAttachmentCaps(TextureInternalFormat format) { FormatCapabilityFlags caps = FormatCapability::FramebufferRenderable; const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(format); const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(format); if (!isDepth && !isStencil) { caps |= FormatCapability::ColorAttachment; } if (isDepth) { caps |= FormatCapability::DepthAttachment; } if (isStencil) { caps |= FormatCapability::StencilAttachment; } return caps; } Bool IsDepthOnlyFormat(TextureInternalFormat format) { return MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format); } Bool IsFilterableFormat(TextureInternalFormat format) { const GLenum glFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format); GLenum normalizedInternalFormat = glFormat; GLenum imageFormat = GL_RGBA; GLenum imageType = GL_UNSIGNED_BYTE; MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, &imageFormat, &imageType); return imageFormat != GL_RED_INTEGER && imageFormat != GL_RG_INTEGER && imageFormat != GL_RGB_INTEGER && imageFormat != GL_RGBA_INTEGER && !MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format); } FormatCapabilityFlags GetTextureFeatureCaps(TextureInternalFormat format, TextureTarget target) { FormatCapabilityFlags caps = FormatCapability::Creatable | FormatCapability::Sampled; if (IsFilterableFormat(format)) { caps |= FormatCapability::LinearFilter; } if (!IsGLESProbeMultisampleTarget(target) && !MG_Util::IsStencilFormatInternalFormat(format)) { caps |= FormatCapability::GenerateMipmap; } if (IsDepthOnlyFormat(format)) { caps |= FormatCapability::TextureShadow; } if (IsGLESProbeMultisampleTarget(target)) { caps |= FormatCapability::MultisampleTexture; } return caps; } FormatCapabilityFlags GetRenderbufferFeatureCaps(TextureInternalFormat format) { return FormatCapabilityFlags(FormatCapability::Creatable) | GetAttachmentCaps(format) | FormatCapability::MultisampleRenderbuffer; } struct GLESProbeFormatInfo { GLenum InternalFormat = GL_UNKNOWN_MGL; GLenum ImageFormat = GL_RGBA; GLenum ImageType = GL_UNSIGNED_BYTE; String Reason; }; GLESProbeFormatInfo BuildNativeProbeFormatInfo(GLenum requestedInternalFormat) { GLESProbeFormatInfo info; info.InternalFormat = requestedInternalFormat; MG_Util::TextureFormatProcessor::NormalizePixelFormat(requestedInternalFormat, PixelFormatNormalizeOptionBit::None, nullptr, &info.ImageFormat, &info.ImageType); return info; } Flags GetForcedPixelFormatNormalizeOptions( const MG_External::GLESCapabilities& capabilities) { Flags options; if (capabilities.IsAngleRenderer) { options |= PixelFormatNormalizeOptionBit::NoRgb16; options |= PixelFormatNormalizeOptionBit::NoSnorm16; options |= PixelFormatNormalizeOptionBit::NoSnorm8; } return options; } Flags GetDriverPixelFormatNormalizeOptions( const MG_External::GLESCapabilities& capabilities) { Flags options = PixelFormatNormalizeOptionBit::NoDepthComponent32; options |= PixelFormatNormalizeOptionBit::NoRGBA8Snorm; options |= PixelFormatNormalizeOptionBit::NoRGB16Snorm; if (!capabilities.SupportsNorm16Texture) { options |= PixelFormatNormalizeOptionBit::NoNorm16; } return options; } String BuildPixelFormatFallbackReason(Flags options, Bool forced) { Vector reasons; if (options & PixelFormatNormalizeOptionBit::NoNorm16) { reasons.push_back("EXT_texture_norm16 not supported"); } if (options & PixelFormatNormalizeOptionBit::NoRgb16) { reasons.push_back(forced ? "RGB16 fallback forced by backend policy" : "RGB16 native path is not supported"); } if (options & PixelFormatNormalizeOptionBit::NoSnorm16) { reasons.push_back(forced ? "SNORM16 fallback forced by backend policy" : "SNORM16 native path is not supported"); } if (options & PixelFormatNormalizeOptionBit::NoSnorm8) { reasons.push_back(forced ? "SNORM8 fallback forced by backend policy" : "SNORM8 native path is not supported"); } if (options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm) { reasons.push_back(forced ? "RGBA8_SNORM fallback forced by backend policy" : "RGBA8_SNORM render target path is not supported"); } if (options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) { reasons.push_back(forced ? "RGB16_SNORM fallback forced by backend policy" : "RGB16_SNORM render target path is not supported"); } if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) { reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES"); } if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { reasons.push_back("no colour-renderable three-channel format on OpenGL ES"); } // A format is either 8- or 16-bit signed normalized, so at most one of the two ever // survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated. if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) || (options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) { reasons.push_back("EXT_render_snorm not supported"); } String reason; for (SizeT i = 0; i < reasons.size(); ++i) { if (i != 0) reason += "; "; reason += reasons[i]; } return reason.empty() ? "Native format probe failed" : reason; } String ConvertFallbackInternalFormatToString(GLenum internalFormat) { const TextureInternalFormat logicalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalFormat); if (logicalFormat != TextureInternalFormat::Unknown) { return MG_Util::ConvertTextureInternalFormatToString(logicalFormat); } return MG_Util::ConvertGLEnumToString(internalFormat); } void LogGLESFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex, const GLESProbeFormatInfo& fallbackInfo) { MGLOG_D("Caveat: %s %s not fully supported. Reason: %s. Fallback: %s", GetFormatCapabilityTargetName(targetIndex).c_str(), MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(), fallbackInfo.Reason.c_str(), ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str()); } Bool BuildFallbackProbeFormatInfo(GLenum requestedInternalFormat, Flags options, Bool forced, GLESProbeFormatInfo& outInfo) { const Flags applicableOptions = MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, options); if (!applicableOptions) { return false; } MG_Util::TextureFormatProcessor::NormalizePixelFormat(requestedInternalFormat, applicableOptions, &outInfo.InternalFormat, &outInfo.ImageFormat, &outInfo.ImageType); outInfo.Reason = BuildPixelFormatFallbackReason(applicableOptions, forced); return outInfo.InternalFormat != GL_UNKNOWN_MGL; } FormatCapabilityFlags BuildTextureCapsFromProbe(TextureInternalFormat logicalFormat, TextureTarget target, Bool renderable) { FormatCapabilityFlags caps = GetTextureFeatureCaps(logicalFormat, target); if (renderable) { caps |= GetAttachmentCaps(logicalFormat); if (target == TextureTarget::Texture3D || target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMapArray || target == TextureTarget::Texture2DMultisampleArray) { caps |= FormatCapability::FramebufferLayered; } } return caps; } void AddFullFormatCaps(FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex, FormatCapabilityFlags caps) { cache.FullCaps[targetIndex][formatIndex] |= caps; } Bool AddCaveatFormatCaps(FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex, FormatCapabilityFlags caps) { Bool added = false; for (FormatCapability capability : kReportedFormatCapabilities) { if (HasFormatCapability(caps, capability) && !HasFormatCapability(cache.FullCaps[targetIndex][formatIndex], capability)) { cache.CaveatCaps[targetIndex][formatIndex] |= capability; added = true; } } return added; } Int GetGLESFormatMaxSamples(const MG_External::GLESCapabilities& capabilities, TextureInternalFormat logicalFormat, GLenum imageFormat) { const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat); const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat); const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER; if (isDepth || isStencil) { return capabilities.MaxDepthTextureSamples; } if (isInteger) { return capabilities.MaxIntegerSamples; } return capabilities.MaxColorTextureSamples; } // The RENDERBUFFER twin, and it is a different set of pnames on purpose. // GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is // bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the // integer formats. Using the texture ceilings here - which is what the renderbuffer probe // did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0 // context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps // in the GLES loader) and the walk below would never run past one sample, recording {1} // for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered - // reports 4. Once the frontend validates against this list, that would reject every // multisample renderbuffer on such a context. Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities, GLenum imageFormat) { const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER; return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples; } Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLuint texture, TextureInternalFormat format) { GLuint framebuffer = 0; GLint prevFramebuffer = 0; if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers) { return false; } gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer); gl.glGenFramebuffers(1, &framebuffer); gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); const GLenum attachment = GetFramebufferAttachment(format); switch (target) { case TextureTarget::Texture2D: gl.glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, texture, 0); break; case TextureTarget::TextureCubeMap: gl.glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_CUBE_MAP_POSITIVE_X, texture, 0); break; case TextureTarget::Texture3D: case TextureTarget::Texture2DArray: case TextureTarget::TextureCubeMapArray: case TextureTarget::Texture2DMultisampleArray: if (!gl.glFramebufferTextureLayer) { gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFramebuffer)); gl.glDeleteFramebuffers(1, &framebuffer); return false; } gl.glFramebufferTextureLayer(GL_FRAMEBUFFER, attachment, texture, 0, 0); break; case TextureTarget::Texture2DMultisample: gl.glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D_MULTISAMPLE, texture, 0); break; default: gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFramebuffer)); gl.glDeleteFramebuffers(1, &framebuffer); return false; } const Bool complete = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFramebuffer)); gl.glDeleteFramebuffers(1, &framebuffer); return complete; } // Whether the driver renders to a framebuffer whose depth and stencil come from // two different renderbuffers. GL only requires support when both attachments are // the same image, and ES drivers commonly answer GL_FRAMEBUFFER_UNSUPPORTED here; // reporting COMPLETE from the frontend and then rendering into a framebuffer the // driver refuses leaves the results silently empty. Bool ProbeDistinctDepthStencilAttachments(const MG_External::GLESFunctionsTable& gl) { if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glRenderbufferStorage || !gl.glDeleteRenderbuffers) { return true; } GLint prevFramebuffer = 0, prevRenderbuffer = 0; gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer); gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer); GLuint framebuffer = 0; GLuint renderbuffers[2] = {0, 0}; gl.glGenFramebuffers(1, &framebuffer); gl.glGenRenderbuffers(2, renderbuffers); gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]); gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 4, 4); gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[1]); gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, 4, 4); gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[0]); gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[1]); const Bool supported = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFramebuffer)); gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(prevRenderbuffer)); gl.glDeleteFramebuffers(1, &framebuffer); gl.glDeleteRenderbuffers(2, renderbuffers); return supported; } // GL_SAMPLE_POSITION is a property of the multisample framebuffer the driver builds, so it // is asked once per power-of-two sample count, of a throwaway RGBA8 renderbuffer framebuffer, // through the driver's own glGetMultisamplefv (ES 3.1 core). A count the driver rounds up // reports the first positions of the pattern it really uses - the samples gl_SampleID // 0..count-1 address in a shader. Counts that cannot be built are left unset. void ProbeSamplePositions(const MG_External::GLESFunctionsTable& gl, const MG_External::GLESCapabilities& capabilities, DynamicBackendParameters& dynamicParameters) { const auto& version = capabilities.GLESVersion; if (version.Major < 3 || (version.Major == 3 && version.Minor < 1)) return; if (!gl.glGetMultisamplefv || !gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glRenderbufferStorageMultisample || !gl.glDeleteRenderbuffers || !gl.glGetIntegerv) { return; } GLint prevFramebuffer = 0, prevRenderbuffer = 0; gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer); gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer); for (Int samples = 2; samples <= DynamicBackendParameters::kMaxSamplePositionCount && samples <= capabilities.MaxSamples; samples *= 2) { GLuint framebuffer = 0, renderbuffer = 0; gl.glGenFramebuffers(1, &framebuffer); gl.glGenRenderbuffers(1, &renderbuffer); gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); gl.glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, 4, 4); gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); GLint actualSamples = 0; if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { gl.glGetIntegerv(GL_SAMPLES, &actualSamples); } if (actualSamples >= samples) { Float positions[2 * DynamicBackendParameters::kMaxSamplePositionCount] = {}; for (Int index = 0; index < samples; ++index) { gl.glGetMultisamplefv(GL_SAMPLE_POSITION, static_cast(index), &positions[2 * index]); } dynamicParameters.SetSamplePositions(samples, positions); } gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFramebuffer)); gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(prevRenderbuffer)); gl.glDeleteFramebuffers(1, &framebuffer); gl.glDeleteRenderbuffers(1, &renderbuffer); } // A count the driver refused leaves its error behind; it must not reach the application. if (gl.glGetError) { for (Int drained = 0; drained < 16 && gl.glGetError() != GL_NO_ERROR; ++drained) {} } } Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLuint renderbuffer, TextureInternalFormat format) { GLuint framebuffer = 0; GLint prevFramebuffer = 0; if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers) { return false; } gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer); gl.glGenFramebuffers(1, &framebuffer); gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GetFramebufferAttachment(format), GL_RENDERBUFFER, renderbuffer); const Bool complete = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFramebuffer)); gl.glDeleteFramebuffers(1, &framebuffer); return complete; } // `samples` only reaches the multisample targets; every other target ignores it. The // descending sample walk (ProbeTextureSampleCounts) reuses this whole routine rather than // repeating the gen/bind/completeness/delete dance. Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat, GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat, Bool* outRenderable, Int samples = 1) { if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) { return false; } const GLenum glTarget = MG_Util::ConvertTextureTargetToGLEnum(target); const GLenum bindingQuery = GetTextureBindingQuery(target); if (glTarget == GL_UNKNOWN_MGL || bindingQuery == GL_UNKNOWN_MGL) { return false; } GLint previousBinding = 0; gl.glGetIntegerv(bindingQuery, &previousBinding); GLuint texture = 0; gl.glGenTextures(1, &texture); gl.glBindTexture(glTarget, texture); ClearGLErrors(gl); const Bool isMultisample = IsGLESProbeMultisampleTarget(target); if (isMultisample) { const auto probeSamples = static_cast(std::max(samples, 1)); if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) { gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE); } else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) { gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE); } else { gl.glBindTexture(glTarget, static_cast(previousBinding)); gl.glDeleteTextures(1, &texture); return false; } } else { if (gl.glTexParameteri) { gl.glTexParameteri(glTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); gl.glTexParameteri(glTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); } switch (target) { case TextureTarget::Texture2D: gl.glTexImage2D(glTarget, 0, static_cast(internalFormat), 2, 2, 0, imageFormat, imageType, nullptr); break; case TextureTarget::TextureCubeMap: for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; ++face) { gl.glTexImage2D(face, 0, static_cast(internalFormat), 2, 2, 0, imageFormat, imageType, nullptr); } break; case TextureTarget::Texture3D: gl.glTexImage3D(glTarget, 0, static_cast(internalFormat), 2, 2, 2, 0, imageFormat, imageType, nullptr); break; case TextureTarget::Texture2DArray: gl.glTexImage3D(glTarget, 0, static_cast(internalFormat), 2, 2, 1, 0, imageFormat, imageType, nullptr); break; case TextureTarget::TextureCubeMapArray: gl.glTexImage3D(glTarget, 0, static_cast(internalFormat), 2, 2, 6, 0, imageFormat, imageType, nullptr); break; default: break; } } const Bool created = CheckNoGLError(gl); Bool renderable = false; if (created) { renderable = ProbeFramebufferCompletenessForTexture(gl, target, texture, logicalFormat); } if (outRenderable) { *outRenderable = renderable; } gl.glBindTexture(glTarget, static_cast(previousBinding)); gl.glDeleteTextures(1, &texture); ClearGLErrors(gl); return created; } Bool ProbeRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat, TextureInternalFormat logicalFormat, Bool multisample, Int samples) { if (!gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glDeleteRenderbuffers) { return false; } GLint prevRenderbuffer = 0; gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer); GLuint renderbuffer = 0; gl.glGenRenderbuffers(1, &renderbuffer); gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); ClearGLErrors(gl); if (multisample) { if (!gl.glRenderbufferStorageMultisample) { gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(prevRenderbuffer)); gl.glDeleteRenderbuffers(1, &renderbuffer); return false; } gl.glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internalFormat, 1, 1); } else { gl.glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, 1, 1); } const Bool created = CheckNoGLError(gl); const Bool complete = created && ProbeFramebufferCompletenessForRenderbuffer(gl, renderbuffer, logicalFormat); gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(prevRenderbuffer)); gl.glDeleteRenderbuffers(1, &renderbuffer); ClearGLErrors(gl); return complete; } Vector ProbeRenderbufferSampleCounts(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat, TextureInternalFormat logicalFormat, Int maxSamples) { Vector sampleCounts; for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) { if (ProbeRenderbuffer(gl, internalFormat, logicalFormat, true, samples)) { sampleCounts.push_back(samples); } } sampleCounts.push_back(1); return sampleCounts; } // The multisample TEXTURE twin of ProbeRenderbufferSampleCounts. It used to be a // hardcoded {1}, which made glGetInternalformativ(GL_SAMPLES) claim a one-sample maximum // for every format on the multisample targets even where glTexImage2DMultisample happily // accepts four - GL 4.6 core 8.8 makes that query the definition of the maximum, so the // two answers cannot both be right. Completeness is required at every count, exactly as // the renderbuffer walk requires it; the caller only reaches here once the one-sample // probe has already succeeded, so 1 terminates the list without being re-probed. Vector ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat, GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat, Int maxSamples) { Vector sampleCounts; for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) { Bool renderable = false; const Bool created = ProbeTexture(gl, target, internalFormat, imageFormat, imageType, logicalFormat, &renderable, samples); if (created && renderable) { sampleCounts.push_back(samples); } } sampleCounts.push_back(1); return sampleCounts; } void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl, const MG_External::GLESCapabilities& capabilities, FormatCapabilityCache& cache) { cache.Clear(); const Flags forcedOptions = GetForcedPixelFormatNormalizeOptions(capabilities); const Flags driverOptions = GetDriverPixelFormatNormalizeOptions(capabilities); for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) { const auto logicalFormat = static_cast(formatIndex); GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(logicalFormat); if (requestedInternalFormat == GL_UNKNOWN_MGL) { continue; } const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat); GLESProbeFormatInfo outerFallbackInfo; const Bool outerHasForcedFallback = BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo); if (!outerHasForcedFallback) { BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo); } for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) { const auto target = static_cast(targetIndex); // Colour-attachable targets need a colour-renderable fallback; the ordinary // fallback for a three-channel format is another three-channel one, which ES // accepts as a texture but never as an attachment. Recompute the fallback per // target so those formats get widened where the target demands it. const Flags renderTargetOptions = TextureImpl::GetRenderTargetNormalizeOptions(capabilities, targetIndex); // Multisample storage has no three-channel form on ES at all, so its widening // is unconditional and skips the native probe (which cannot succeed). Every // other target keeps the widening on the DRIVER branch, behind the native // probe: `shouldProbeFallback = !nativeCreated || !nativeRenderable` below is // what makes the substitution conditional on the driver actually refusing, so // a driver that does render to a three-channel image keeps allocating it byte // for byte. That is a per-format runtime answer, NOT a desktop-vs-device // split: llvmpipe renders to GL_RGB16F but refuses GL_RGB8_SNORM, GL_SRGB8, // GL_RGB32F and the RGB integer formats, so the CI driver widens those eight // too. Re-run the retrace fixtures and the glcts suites on any change here. const Bool widenUnconditionally = IsGLESProbeMultisampleTarget(target); GLESProbeFormatInfo fallbackInfo = outerFallbackInfo; Bool hasForcedFallback = outerHasForcedFallback; if (renderTargetOptions) { // Folded into the forced options only when a forced fallback already // applies, so the render-target bits never *create* one: ANGLE's forced // GL_RGB8_SNORM -> GL_RGB16F is still three-channel and still needs // widening, but a non-ANGLE driver must not lose its native probe. const Flags forcedProbeOptions = (outerHasForcedFallback || widenUnconditionally) ? forcedOptions | renderTargetOptions : forcedOptions; hasForcedFallback = BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedProbeOptions, true, fallbackInfo); if (!hasForcedFallback) { BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | renderTargetOptions, false, fallbackInfo); } // HONEST STATUS OF THE FORCED PATH. A forced fallback is only ever built // for ANGLE (GetForcedPixelFormatNormalizeOptions returns nothing for any // other renderer), and it SKIPS the native probe entirely - the widened // format is asserted rather than measured on this device. That assertion // is validated on exactly one configuration, the android-angle retrace // golden; it is NOT covered by the headless llvmpipe suites, which take // the driver branch below and prove nothing about ANGLE's answers. So log // the choice at INFO rather than the usual MGLOG_D caveat: on any other // ANGLE device the device report is the only evidence there is of which // storage format the image really got. Once per format on the ordinary 2D // target - repeating it for all ten targets would bury the report. if (hasForcedFallback && target == TextureTarget::Texture2D && (MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions( requestedInternalFormat, renderTargetOptions) & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)) { MGLOG_I("Three-channel widening (FORCED path, no native probe): %s stored as %s. " "Reason: %s. Device-validated on the android-angle golden only.", MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(), ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str(), fallbackInfo.Reason.c_str()); } } // 1D, 1D-array and rectangle textures live on an ES target (see // TextureImpl::MapToBackendTextureTarget), so they have to be probed there too - // probing the desktop-only target itself always failed, which left those slots // of the cache empty and stopped any fallback format from being selected for // them (a GL_DEPTH_COMPONENT32 1D texture then got no storage at all). const TextureTarget probeTarget = TextureImpl::MapToBackendTextureTarget(target); Bool shouldProbeFallback = hasForcedFallback; if (!hasForcedFallback) { Bool nativeRenderable = false; const Bool nativeCreated = ProbeTexture(gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat, nativeInfo.ImageType, logicalFormat, &nativeRenderable); if (nativeCreated) { AddFullFormatCaps(cache, targetIndex, formatIndex, BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable)); if (IsGLESProbeMultisampleTarget(target)) { const Int maxSamples = GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat); cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts( gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat, nativeInfo.ImageType, logicalFormat, maxSamples); } } shouldProbeFallback = !nativeCreated || !nativeRenderable; } if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) { Bool fallbackRenderable = false; const Bool fallbackCreated = ProbeTexture(gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat, fallbackInfo.ImageType, logicalFormat, &fallbackRenderable); if (fallbackCreated) { if (AddCaveatFormatCaps( cache, targetIndex, formatIndex, BuildTextureCapsFromProbe(logicalFormat, target, fallbackRenderable))) { LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo); } if (IsGLESProbeMultisampleTarget(target)) { const Int maxSamples = GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat); cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts( gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat, fallbackInfo.ImageType, logicalFormat, maxSamples); } } } } const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex(); // A renderbuffer exists only to be attached, so it needs the same three-channel // widening the colour-attachable texture targets get - and on the same terms: the // native storage is probed first, so a driver that renders to it keeps it. const Flags renderbufferOptions = TextureImpl::GetRenderTargetNormalizeOptions(capabilities, renderbufferTargetIndex); GLESProbeFormatInfo renderbufferFallbackInfo = outerFallbackInfo; Bool renderbufferHasForcedFallback = outerHasForcedFallback; if (renderbufferOptions) { const Flags forcedProbeOptions = outerHasForcedFallback ? forcedOptions | renderbufferOptions : forcedOptions; renderbufferHasForcedFallback = BuildFallbackProbeFormatInfo( requestedInternalFormat, forcedProbeOptions, true, renderbufferFallbackInfo); if (!renderbufferHasForcedFallback) { BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | renderbufferOptions, false, renderbufferFallbackInfo); } } Bool shouldProbeFallbackRenderbuffer = renderbufferHasForcedFallback; if (!renderbufferHasForcedFallback) { const Bool nativeRenderbufferComplete = ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1); if (nativeRenderbufferComplete) { AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex, GetRenderbufferFeatureCaps(logicalFormat)); const Int maxSamples = GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat); cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples); } else { shouldProbeFallbackRenderbuffer = true; } } if (shouldProbeFallbackRenderbuffer && renderbufferFallbackInfo.InternalFormat != GL_UNKNOWN_MGL && ProbeRenderbuffer(gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, false, 1)) { if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex, GetRenderbufferFeatureCaps(logicalFormat))) { LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo); } const Int maxSamples = GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat); cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts( gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples); } } } // The advertised renderer info must be mutable after its first use: // E_GL_ARB_timer_query can only be decided once the ES capabilities // are known, long after the list is first read (see // UpdateAdvertisedTimerQueryExtension below). RendererInfo& MutableRendererInfo() { static RendererInfo rendererInfo = { .RendererName = "Espryt", // Renderer Name .BackendName = "Direct (OpenGL ES)", // Backend Name .ExtraVendor = Nullopt, // Extra vendor .RendererGLInfo = { .TargetGLVersion = {4, 6, 0}, // GL target version .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, false), .IsCompatibilityProfile = false // Is Compatibility Profile }, .StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability }; return rendererInfo; } // GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension // string via glGetStringi plus non-null glQueryCounter and // glGetQueryObject(u)i64v entries). GetRendererInfo is first invoked // from LogBackendInfo() during MG_Backend::Init, BEFORE any ES // context or capabilities exist, so the advertisement cannot be baked // into the static initializer above; it is reconciled here at the end // of InitCapabilities instead (mirroring DirectVulkan's mutable // m_rendererInfo + UpdateAdvertisedExtensions). InitCapabilities // completes inside the first MakeEGLCurrent on a context, so an app // thread can only observe the extension string after the // advertisement for its context has settled; rebuilding the whole // list keeps the re-run after a context recreation idempotent. void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) { MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions( AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy, capabilities.SupportsDrawIndirect, capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance, capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray, AdvertisedFeatureSupport{.TessellationShader = capabilities.SupportsTessellationShader, .CullDistance = capabilities.MaxCullDistances > 0, .ClipControl = capabilities.SupportsClipControl}); // Exactly what InitCapabilities forwards into the dynamic parameters: the base // extension and _primitive when the driver has them, _attachment not yet. auto& extensions = MutableRendererInfo().RendererGLInfo.Extensions; if (capabilities.SupportsFragmentShadingRate) { extensions.push_back(E_GL_EXT_fragment_shading_rate); if (capabilities.SupportsFragmentShadingRatePrimitive) { extensions.push_back(E_GL_EXT_fragment_shading_rate_primitive); } } // Native where the ES driver has subgroups, emulated in the compute stage where it // has none (SubgroupPolicy.h); the dynamic parameters below say which. if (ResolveEsprytSubgroupImplementation(capabilities) != SubgroupImplementation::None) { extensions.push_back(E_GL_KHR_shader_subgroup); } } } // namespace void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl, const MG_External::GLESCapabilities& capabilities, FormatCapabilityCache& cache) { PopulateFormatCapabilitiesImpl(gl, capabilities, cache); } Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat, Int samples) { if (samples <= 1) { return samples; } Int maxSamples = 0; const SizeT formatIndex = static_cast(logicalFormat); // C6 / ID-52: read the ROLE's own cache. Under split that is the server's private backend // (ActiveBackendFormatCaps), not pActiveBackendObject, which holds the client's mirror. const FormatCapabilityCache* activeCaps = ActiveBackendFormatCaps(); if (activeCaps != nullptr && targetIndex < kFormatCapabilityTargetCount && formatIndex < kFormatCapabilityFormatCount) { // Descending, so the head is the largest count this device actually allocated. const Vector& probedCounts = activeCaps->SampleCounts[targetIndex][formatIndex]; if (!probedCounts.empty()) { maxSamples = probedCounts.front(); } } if (maxSamples <= 0) { maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat); } return std::min(samples, std::max(maxSamples, 1)); } BackendObject_DirectGLES::~BackendObject_DirectGLES() { DestroyEGLContext(); #if MOBILEGL_BUILD_DISAGGREGATED NoteNativeContextGone(); // P12: under a transport this object is the SERVER's backend and its destruction is the // end of a session (ServerLoop's apply thread, after the final drain). The in-process // display server runs the next session in this same process, so the twins this one built // - naming ids of the context just destroyed - must not answer for the next client's // handles (Managers.h, DropEveryTwinForEndedServerSession). Monolith keeps its twins. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { DropEveryTwinForEndedServerSession(); // Review fix: and the swap interval it asked for (DirectGLES.h). ForgetRequestedSwapInterval(); } #endif } Bool BackendObject_DirectGLES::RebindNativeSurfaceFor(EGLSurface surface) { const auto it = m_nativeSurfaces.find(surface); if (it == m_nativeSurfaces.end()) return false; if (DirectGLES::BindSessionSurface(it->second)) return true; // The session no longer owns it (destroyed with a window loss): forget and recreate. m_nativeSurfaces.erase(it); return false; } void BackendObject_DirectGLES::RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw) { // The ACTIVE CONTEXT's own surface, exactly: the creation's bind attached the new surface to // whichever context the session had bound (often another window's thread), and putting back // "the surface activated last" instead re-attached THAT context to a third window's surface - // a desktop drawing into and reading back from the panel's pbuffer. if (previousNativeDraw == EGL_NO_SURFACE) return; if (!DirectGLES::BindSessionSurface(previousNativeDraw)) return; for (const auto& [client, native] : m_nativeSurfaces) { if (native != previousNativeDraw) continue; (void)EGLRegistry().AdoptActiveSurface(client); break; } } Bool BackendObject_DirectGLES::InitWindowSurface() { const EGLSurface activating = EGLRegistry().ActivatingSurface(); if (RebindNativeSurfaceFor(activating)) return true; // A suspended surface whose placeholder went: another placeholder - never the window, which // is gone. if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), activating) != m_suspendedWindowSurfaces.end()) { const EGLSurface placeholder = DirectGLES::CreateSessionPbuffer(1, 1); if (placeholder == EGL_NO_SURFACE || !DirectGLES::BindSessionSurface(placeholder)) { MGLOG_E("DirectGLES: no placeholder for the suspended window surface %p", reinterpret_cast(activating)); return false; } m_nativeSurfaces[activating] = placeholder; return true; } // Only use EGL for now auto nativeWindow = reinterpret_cast(EGLRegistry().CurrentWindowHandle().Handle); if (!DirectGLES::InitWindowSurface(nativeWindow)) { MGLOG_E("Failed to initialize window surface for DirectGLES backend"); return false; } if (activating != EGL_NO_SURFACE) { m_nativeSurfaces[activating] = DirectGLES::CurrentSessionDrawSurface(); } return true; } Bool BackendObject_DirectGLES::LatchIfGpuFaulted() { #if MOBILEGL_BUILD_DISAGGREGATED return LatchIfGuiltyBeforeApply(); #else return false; #endif } void BackendObject_DirectGLES::Initialize() { MG_Util::BackendLoader::AcquireEGLFunctions(m_EGLFunctions); MG_Util::BackendLoader::AcquireGLESFunctions(m_GLESFunctions, m_EGLFunctions.eglGetProcAddress); DirectGLES::SetEGLFuncsTable(m_EGLFunctions); DirectGLES::SetGLESFuncsTable(m_GLESFunctions); BufferImpl::RegisterBufferBackendOps(); m_initialized = true; } Bool BackendObject_DirectGLES::InitCapabilities() { if (!m_initialized) { MGLOG_E("DirectGLES backend not initialized"); return false; } if (!MG_Util::BackendLoader::FillInGLESCapabilities(m_GLESCapabilities, m_GLESFunctions)) { MGLOG_E("Failed to fill in GLES capabilities for DirectGLES backend"); return false; } DirectGLES::SetGLESCapabilities(m_GLESCapabilities); // Now that g_GLESCapabilities knows the host extensions, entry points, and ES version, // reconcile every runtime-gated advertisement (see the comment on // UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first // built). UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities); UpdateDynamicBackendParameters(); // The colour-attachment cap the framebuffer family checks is known from here on. DirectGLES::ResolveRecordArmFamilies(); PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities()); PrintFormatCapabilities(GetFormatCapabilities()); return true; } Bool BackendObject_DirectGLES::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) { if (!m_initialized) { MGLOG_E("DirectGLES backend not initialized"); return false; } return BackendObject::InitializeEGLDisplay(dpy, major, minor); } Bool BackendObject_DirectGLES::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) { const auto lock = EGLRegistry().Lock(); if (!m_initialized) { MGLOG_E("DirectGLES backend not initialized"); return false; } if ((handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 && handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32) || !handle.Handle) { MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows"); return false; } EGLSurfaceRegistry& egl = EGLRegistry(); const WindowHandle activeWindow = egl.CurrentWindowHandle(); const Bool sameHandle = egl.HasActiveSurface() && egl.ActiveSurface() == surface && egl.ActiveSurfaceKind() == EGLSurfaceRegistry::SurfaceKind::Window && activeWindow.Backend == handle.Backend && activeWindow.Handle == handle.Handle; if (sameHandle) { return true; } // ---- P14 S4: A SECOND SURFACE NO LONGER TEARS THE CONTEXT DOWN. ---- // // This used to be `DestroyEGLContext(); ResetEGLRuntimeState();` whenever a surface was // created while another was active, i.e. every pbuffer -> window -> pbuffer switch destroyed // the native context - and with it every buffer, texture and program the application had // built. The native context now belongs to the (session, context token) tuple the backend // resolves, and creating a surface for it only adds a surface: the tuple keeps its context // and its objects. ResetRuntimeState still runs because the registry's per-surface // bookkeeping (which surface is active, which threads made it current) does describe the // surface that is being replaced. const EGLSurface previous = egl.HasActiveSurface() ? egl.ActiveSurface() : EGL_NO_SURFACE; const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface(); if (egl.HasActiveSurface()) { egl.ResetRuntimeState(); } #if MOBILEGL_BUILD_DISAGGREGATED // ID-54 / ID-67: the surface's creation bound natively (InitWindowSurface -> MakeCurrent); // the first virtual tuple adopts that bind. On failure nothing is known to be bound. The // pull arm below is the original statement, byte for byte (G1). const Bool created = BackendObject::CreateEGLWindowSurface(surface, handle); if (created && previous != EGL_NO_SURFACE && previous != surface) { // Another surface was current: the creation must not leave the context that held it // drawing to the new one (P14 multi-window clients). RestoreSurfaceAfterCreation(previousNativeDraw); NoteNativeContextGone(); } else if (created) { NoteNativeContextFreshFromSurfaceCreation(); } else { NoteNativeContextGone(); } return created; #else return BackendObject::CreateEGLWindowSurface(surface, handle); #endif } Bool BackendObject_DirectGLES::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) { const auto lock = EGLRegistry().Lock(); if (!m_initialized) { MGLOG_E("DirectGLES backend not initialized"); return false; } EGLSurfaceRegistry& egl = EGLRegistry(); if (egl.HasActiveSurface() && egl.ActiveSurface() == surface && egl.ActiveSurfaceKind() == EGLSurfaceRegistry::SurfaceKind::Pbuffer) { return true; } // P14 S4: as for the window surface above - the native context is the (session, context // token) tuple's and survives a surface switch. This used to be DestroyEGLContext(). const EGLSurface previous = egl.HasActiveSurface() ? egl.ActiveSurface() : EGL_NO_SURFACE; const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface(); if (egl.HasActiveSurface()) { egl.ResetRuntimeState(); } // ID-54 / ID-67: as for the window surface - InitPbufferSurface bound natively. The pull // arm below is the original statement, byte for byte (G1). const Bool created = BackendObject::CreateEGLPbufferSurface(surface, width, height); if (created && previous != EGL_NO_SURFACE && previous != surface) { RestoreSurfaceAfterCreation(previousNativeDraw); NoteNativeContextGone(); } else if (created) { NoteNativeContextFreshFromSurfaceCreation(); } else { NoteNativeContextGone(); } return created; } Bool BackendObject_DirectGLES::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) { const auto lock = EGLRegistry().Lock(); const auto record = EGLRegistry().Find(surface); const auto native = m_nativeSurfaces.find(surface); if (!record || record->Kind != EGLSurfaceRegistry::SurfaceKind::Pbuffer || native == m_nativeSurfaces.end()) { return BackendObject::ResizeEGLWindowSurface(surface, width, height); } // A pbuffer has no resize: the client surface gets a new native pbuffer at the new size, // and every context that drew to the old one draws to the new one. const EGLint w = static_cast(std::max(width, 1)); const EGLint h = static_cast(std::max(height, 1)); const EGLSurface replacement = DirectGLES::CreateSessionPbuffer(w, h); if (replacement == EGL_NO_SURFACE) { MGLOG_E("ResizeEGLWindowSurface: no %dx%d pbuffer could replace the old drawable", w, h); return false; } DirectGLES::ReplaceSessionSurface(native->second, replacement); native->second = replacement; return BackendObject::ResizeEGLWindowSurface(surface, width, height); } Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) { const EGLSurface activating = EGLRegistry().ActivatingSurface(); if (RebindNativeSurfaceFor(activating)) return true; if (!DirectGLES::InitPbufferSurface(width, height)) return false; if (activating != EGL_NO_SURFACE) { m_nativeSurfaces[activating] = DirectGLES::CurrentSessionDrawSurface(); } return true; } Bool BackendObject_DirectGLES::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { const auto lock = EGLRegistry().Lock(); if (IsReleaseCurrentRequest(dpy, draw, read, ctx)) { if (!DirectGLES::ReleaseCurrent()) { return false; } NoteNativeContextGone(); return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx); } if (!m_initialized) { MGLOG_E("DirectGLES backend not initialized"); return false; } const EGLSurfaceRegistry& egl = EGLRegistry(); if (!egl.DisplayInitialized() || egl.Display() != dpy) { MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized"); return false; } if (!egl.HasActiveSurface()) { MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized"); return false; } if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) { MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid"); return false; } #if MOBILEGL_BUILD_DISAGGREGATED // ID-54 / C7, the native half of "bind once per tuple" (v1, under ID-52/ID-59's grant for // this file; #if-guarded so the pull build is byte-identical). Under an active transport // this runs on the apply thread, which is the ONLY thread that ever binds the server's // context, and the surface it is asked for was made natively current on this very thread // by its own creation (InitPbufferSurface / InitWindowSurface -> DirectGLES::MakeCurrent). // A second native eglMakeCurrent for the same surface is then a repeat: it rewrites the // owner with the same thread, re-registers the same op table and invalidates seven caches // that describe a context that did not change - the per-client-make-current storm the // v2 review measured as "2 native binds per process, unchanged". So when the requested // draw surface IS the active one and EGL itself says this thread holds the context // (IsBackendContextCurrentOnThisThread re-verifies against eglGetCurrentContext), the // native call is skipped and only the base class's bookkeeping below runs - which is // still required: it is what InitCapabilities and SwapEGLBuffers' current-thread record // hang off. Monolith transport in this build, and the pull build, bind exactly as before. // // AND ONLY FOR THE SAME VIRTUAL TUPLE (ID-67). The skip is keyed on NativeBindCanBeSkippedFor: // the surface's own creation bind is adopted by the FIRST tuple, an identical tuple skips, // and a DIFFERENT tuple - a second MobileGL context onto the same surface - runs the native // call again even though the driver's triple is the same, because MakeCurrent's seven // invalidations describe the frontend context that is changing. Red once, two ways: // make this arm unconditional (ServerLoopTest's C7 control reads 2 native binds at the EGL // function table instead of 1), or make NativeBindCanBeSkippedFor answer true for any tuple // (the ID-67 control's different tuple stays at 1 native bind where 2 are required). const Bool nativelyCurrentAlready = MG_Config::Transport != MG_Config::TransportMode::Monolith && egl.HasActiveSurface() && egl.ActiveSurface() == draw && DirectGLES::IsBackendContextCurrentOnThisThread() && NativeBindCanBeSkippedFor(dpy, draw, read, ctx); // The draw surface's own native surface (P14 multi-window): a known one is re-bound, so the // bind goes to it and not to whichever surface the session drew to last. // The real bind still runs whenever the skip above says so (ID-67: a different tuple onto // the same surface needs MakeCurrent's invalidations even though the driver triple is equal). const auto nativeDraw = m_nativeSurfaces.find(draw); if (!nativelyCurrentAlready && nativeDraw != m_nativeSurfaces.end()) { (void)DirectGLES::SelectSessionSurface(nativeDraw->second); } const Bool bound = nativelyCurrentAlready || DirectGLES::MakeCurrent(); if (!bound) { NoteNativeContextGone(); return false; } NoteNativeBoundFor(dpy, draw, read, ctx); #else if (!DirectGLES::MakeCurrent()) { return false; } #endif if (!BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) { (void)DirectGLES::ReleaseCurrent(); NoteNativeContextGone(); return false; } return true; } Bool BackendObject_DirectGLES::BlitDefaultFramebufferToSharedImage(const SharedImageView& image, const MG_Util::Damage::Region& region) { #if MOBILEGL_BUILD_DISAGGREGATED return DirectGLES::SharedImageImpl::BlitDefaultFramebufferTo(image, region); #else (void)image, (void)region; return false; #endif } Int32 BackendObject_DirectGLES::QueryCurrentBufferAge(Bool damageRegionFollows) { return DirectGLES::CurrentDrawBufferAge(damageRegionFollows); } Bool BackendObject_DirectGLES::SetCurrentDamageRegion(const MG_Util::Damage::Region& region) { return DirectGLES::SetCurrentDrawDamageRegion(region); } Bool BackendObject_DirectGLES::PublishSharedImageAccesses() { #if MOBILEGL_BUILD_DISAGGREGATED return DirectGLES::SharedImageImpl::PublishPendingAccesses(); #else return true; #endif } Bool BackendObject_DirectGLES::ExportNativeFence(int* fence) { *fence = -1; #if MOBILEGL_BUILD_DISAGGREGATED return DirectGLES::SharedImageImpl::ExportNativeFence(fence); #else return true; #endif } Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) { return BackendObject::SwapEGLBuffers(dpy, draw); } void BackendObject_DirectGLES::ReleaseEGLSurface(EGLSurface surface) { const auto lock = EGLRegistry().Lock(); BackendObject::ReleaseEGLSurface(surface); } void BackendObject_DirectGLES::ReleaseEGLResources() { const auto lock = EGLRegistry().Lock(); DestroyEGLContext(); NoteNativeContextGone(); m_suspendedWindowSurfaces.clear(); BackendObject::ReleaseEGLResources(); } Bool BackendObject_DirectGLES::SuspendServerWindow(void* window) { const auto lock = EGLRegistry().Lock(); if (window == nullptr) return false; Vector onWindow; for (const EGLSurface client : EGLRegistry().WindowSurfacesOn(window)) { if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) != m_suspendedWindowSurfaces.end()) continue; onWindow.push_back(client); } for (const EGLSurface client : onWindow) { // A surface never activated has no native surface: only its record names the window. const auto native = m_nativeSurfaces.find(client); if (native != m_nativeSurfaces.end()) { const EGLSurface placeholder = DirectGLES::SuspendSessionWindowSurface(native->second); if (placeholder == EGL_NO_SURFACE) return false; native->second = placeholder; } m_suspendedWindowSurfaces.push_back(client); } // The native binding moved onto the placeholder: the next make-current binds for real. NoteNativeContextGone(); MGLOG_I("DirectGLES: %zu window surface(s) on server window %p now draw into placeholder pbuffers; the " "contexts and their objects stay", onWindow.size(), window); return true; } Bool BackendObject_DirectGLES::ResumeServerWindow(void* window, Uint32 width, Uint32 height) { const auto lock = EGLRegistry().Lock(); if (window == nullptr) return false; Vector suspended; suspended.swap(m_suspendedWindowSurfaces); for (const EGLSurface client : suspended) { if (!EGLRegistry().SetWindowSurfaceExtent(client, width, height, window)) continue; // released while suspended // Never activated: its first activation creates it on the new window. const auto native = m_nativeSurfaces.find(client); if (native == m_nativeSurfaces.end()) continue; const EGLSurface surface = DirectGLES::ResumeSessionWindowSurface(native->second, reinterpret_cast(window)); if (surface == EGL_NO_SURFACE) return false; native->second = surface; } NoteNativeContextGone(); MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(), window, width, height); return true; } Bool BackendObject_DirectGLES::ServerWindowResized(void* window, Uint32 width, Uint32 height) { const auto lock = EGLRegistry().Lock(); if (window == nullptr) return false; Bool published = false; for (const EGLSurface client : EGLRegistry().WindowSurfacesOn(window)) { if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) != m_suspendedWindowSurfaces.end()) continue; (void)EGLRegistry().SetWindowSurfaceExtent(client, width, height); // The window surface's buffers follow the window by themselves (from the next one the // driver dequeues); only the client has to be told. const auto native = m_nativeSurfaces.find(client); if (native == m_nativeSurfaces.end()) continue; published = DirectGLES::RepublishWindowSurfaceShape(native->second, reinterpret_cast(window)) || published; } return published; } void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) { // P14: releasing a surface releases THAT SURFACE (OnEGLSurfaceForgotten), never the // contexts. This used to be DestroyEGLContext() - every native context of the session, with // every object the client had built - which a multi-window client hit on its first closed // popup. (void)surface; NoteNativeContextGone(); } void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) { m_suspendedWindowSurfaces.erase( std::remove(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), surface), m_suspendedWindowSurfaces.end()); const auto it = m_nativeSurfaces.find(surface); if (it == m_nativeSurfaces.end()) return; const EGLSurface native = it->second; m_nativeSurfaces.erase(it); DirectGLES::DestroySessionSurface(native); } const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const { return MutableRendererInfo(); } String BackendObject_DirectGLES::GetBackendAPIVersionString() const { if (!m_initialized) { return ""; } return FormatBackendAPIVersionString(m_GLESCapabilities.GLESRendererString, m_GLESCapabilities.GLESVersion.Major, m_GLESCapabilities.GLESVersion.Minor); } const RendererInfo& GetRendererIdentity() { return MutableRendererInfo(); } Vector BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported, Bool drawIndirectSupported, Bool nonZeroIndirectBaseInstanceSupported, Bool textureViewSupported, Bool cubeMapArraySupported, const AdvertisedFeatureSupport& features) { Vector extensions = { // The version tokens have to reach the version the backend actually claims: // TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an // application feature-detecting off these tokens the opposite of what // GL_MAJOR_VERSION / GL_MINOR_VERSION told it. V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43, V_OpenGL44, V_OpenGL45, V_OpenGL46, E_GL_ARB_draw_buffers_blend, // The GLSL 1.00/1.10-era ARB trio plus NPOT textures: all core since GL 2.0 and // trivially true for every context this backend claims (4.6). KWin 6's // EglContext::checkSupported() gates desktop-GL compositing on exactly these four // strings, so withholding them fails its renderer bring-up on an otherwise capable // implementation. E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader, E_GL_ARB_texture_non_power_of_two, E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store, E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, // Both are core from GL 3.2/3.3 on and implemented here for // every advertised version, but an app targeting 3.0/3.1 // only reaches them through the extension string - the CTS // picks a whole different shader for draw_buffers without // explicit_attrib_location. DirectVulkan advertises both. E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size, // Core since GL 3.1 and implemented for every version advertised here. The string // matters because applications gate the ENTRY POINTS on it rather than on the // version: a caller that finds the extension missing never resolves // glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform // blocks anyway calls through a null pointer. E_GL_ARB_uniform_buffer_object, // Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3, // so on a 4.0 context the string is the only way to reach it. The host ES driver // has had the same texture parameter since ES 3.1, which every device MobileGL // runs on provides. E_GL_ARB_stencil_texturing, // Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex, // glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and // glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string // 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 // available, so withholding it makes MobileGL look less capable than it is. E_GL_ARB_instanced_arrays, // The whole of KHR_debug lives in GLImpl - the message log, the group stack and the // 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, // Core in the 4.x versions this backend already claims, and named so a context of an // older requested version reaches the same, already-implemented surface (the conformance // suite gates its GL 3.x runs of these on the string alone): // - transform feedback objects, pause/resume and glDrawTransformFeedback (4.0); E_GL_ARB_transform_feedback2, // - multiple capture buffers per stream, gl_SkipComponents / gl_NextBuffer, the // indexed queries and glDrawTransformFeedbackStream (4.0); E_GL_ARB_transform_feedback3, // - GL_RGB10_A2UI (3.3), GL_RGB9_E5 (3.0) and the integer colour formats (3.0); E_GL_ARB_texture_rgb10_a2ui, E_GL_EXT_texture_shared_exponent, E_GL_EXT_texture_integer, // - the eleven pipeline-statistics query targets (4.6). Accepted by glBeginQuery and // answered with GL_QUERY_COUNTER_BITS = 0, the spec's own "this counter is not // instrumented" signal (see IsPipelineStatisticsQueryTarget in GL_Query.cpp); E_GL_ARB_pipeline_statistics_query, // - mix() over integer and boolean vectors in GLSL below 4.50. The frontend compiles it // to OpSelect, which the backend prints back as a core boolean mix. E_GL_EXT_shader_integer_mix, // 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. E_GL_ARB_get_program_binary}; // Tessellation is core from GL 4.0, but on the host it is ES 3.2 or // GL_EXT_tessellation_shader, so the string follows the driver. GL_Query also reads it to // open the two tessellation pipeline-statistics targets. if (features.TessellationShader) { extensions.push_back(E_GL_ARB_tessellation_shader); } // gl_CullDistance goes through GL_EXT_clip_cull_distance, whose cull half the loader only // reports where the driver answers a non-zero GL_MAX_CULL_DISTANCES. if (features.CullDistance) { extensions.push_back(E_GL_ARB_cull_distance); } // glClipControl is forwarded to the driver's glClipControlEXT. if (features.ClipControl) { extensions.push_back(E_GL_ARB_clip_control); } // Minecraft 26.3 checks this prerequisite before it even considers // GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader // folds the version and pointer checks into SupportsDrawIndirect. if (drawIndirectSupported) { extensions.push_back(E_GL_ARB_draw_indirect); } // ARB_base_instance also defines the last word of an indirect command. Direct calls are // emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect // draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise // that incomplete case. if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) { extensions.push_back(E_GL_ARB_base_instance); } // GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES // driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram // are serviced entirely inside the frontend. Whether the device driver advertises // the string is irrelevant here (the POST reports it separately, for the day the // driver-side link is what gets parallelised). // // Gated on the async flag deliberately, and this is the whole reason the gate // exists. Advertising the string is the one part of asynchronous compilation that a // recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE // the moment they see it - they enqueue whole pipeline batches and poll // GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so // MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour // change as well as the threading, or the kill switch would only be half a switch. if (MG_Util::Async::AsyncShaderCompileEnabled()) { extensions.push_back(E_GL_KHR_parallel_shader_compile); } // GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a // shader compiles and runs already - it is narrowed to 32 bits before the module // reaches this backend - so an application that simply uses doubles needs nothing // advertised. What the extension additionally promises is 64-bit PRECISION, which no // mobile GPU has and the narrowing cannot fake, so advertising it by default would // make an application that checks the string take a path MobileGL cannot honour. if (MG_Config::Features.AdvertiseFp64) { extensions.push_back(E_GL_ARB_gpu_shader_fp64); } // Only advertised when the device driver actually has usable timer queries // (GL_EXT_disjoint_timer_query plus its entry points) and the // MOBILEGL_DISABLE_TIMERQUERY escape hatch is off. 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. // // 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); } // 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 // the two carry independent per-texture parameters at the same time - which is exactly // what applications use it for (Better Clouds samples one D24S8 through its own name with // DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in // a single shading pass). A copy-based fallback satisfies neither half, and fails // silently; withholding the string and answering glTextureView with INVALID_OPERATION is // the only behaviour that cannot be mistaken for success. // // The host extension is necessary and NOT sufficient, which is why this second gate // exists. Adreno 830 has EXT_texture_view, and on it the whole functional half of // KHR-GL4{2,3}.texture_view fails: base_and_max_levels, reference_counting and // view_sampling Fail and view_classes crashes, while only the two pure-API cases // (errors, gettexparameter - neither of which touches the host view) pass. The cause is // known and is MobileGL's, not the driver's: SyncTextureViewToBackend normalizes the // VIEW's ES internalformat independently of the storage it aliases, so whenever the two // land on different renderability carriers the host rejects the pair, the error is // swallowed, and the view is left as a storage-less name that samples as zeros. // DirectVulkan builds the view as a second VkImageView over one VkImage and has no such // seam - it passes 5 of the 7 cases on the same device - so the string stays there. // // Until that reconciliation exists, advertising here would be the same lie the comment // above refuses to tell, just with an extra prerequisite met. Set // MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work. if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) { extensions.push_back(E_GL_ARB_texture_view); } // Only advertised when the host ES driver actually filters anisotropically: the sampler // state is accepted regardless, but forwarding it would be a no-op without the extension, // and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently // get plain trilinear. if (anisotropicFilteringSupported) { extensions.push_back(E_GL_EXT_texture_filter_anisotropic); extensions.push_back(E_GL_ARB_texture_filter_anisotropic); } return extensions; } String FormatBackendAPIVersionString(const String& glesRendererString, Int glesMajor, Int glesMinor) { // Format: // , OpenGL ES return std::format("{}, OpenGL ES {}.{}", glesRendererString, glesMajor, glesMinor); } BackendType BackendObject_DirectGLES::GetBackendType() const { return BackendType::DirectGLES; } const GlobalBackendFunctionsTable& BackendObject_DirectGLES::GetBackendFunctions() const { static GlobalBackendFunctionsTable funcsTable; static Bool funcsTableInitialized = false; if (!funcsTableInitialized) { funcsTable.Present = DirectGLES::Present; funcsTable.SetSwapInterval = DirectGLES::SetSwapInterval; funcsTable.GL.DrawArrays = DrawArrays; funcsTable.GL.DrawElements = DrawElements; funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex; funcsTable.GL.MultiDrawArrays = MultiDrawArrays; funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex; funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect; funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount; funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect; funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount; funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex; funcsTable.GL.DrawRangeElements = DrawRangeElements; funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance; funcsTable.GL.DrawElementsInstancedBaseVertex = DrawElementsInstancedBaseVertex; funcsTable.GL.DrawElementsInstancedBaseInstance = DrawElementsInstancedBaseInstance; funcsTable.GL.DrawElementsInstanced = DrawElementsInstanced; funcsTable.GL.DrawArraysInstancedBaseInstance = DrawArraysInstancedBaseInstance; funcsTable.GL.DrawArraysInstanced = DrawArraysInstanced; funcsTable.GL.DrawElementsIndirect = DrawElementsIndirect; funcsTable.GL.DrawArraysIndirect = DrawArraysIndirect; funcsTable.GL.DispatchCompute = DispatchCompute; funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect; funcsTable.GL.MemoryBarrier = MemoryBarrier; funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion; funcsTable.GL.BindImageTexture = BindImageTexture; funcsTable.GL.GetIntegeri_v = GetIntegeri_v; funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding; funcsTable.GL.Clear = Clear; funcsTable.GL.ClearBufferfi = ClearBufferfi; funcsTable.GL.ClearBufferfv = ClearBufferfv; funcsTable.GL.ClearBufferuiv = ClearBufferuiv; funcsTable.GL.ClearBufferiv = ClearBufferiv; funcsTable.GL.BlitFramebuffer = BlitFramebuffer; funcsTable.GL.CopyTexImage2D = CopyTexImage2D; funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D; funcsTable.GL.CopyImageSubData = CopyImageSubData; funcsTable.GL.GenerateMipmap = GenerateMipmap; funcsTable.GL.ReadPixels = ReadPixels; funcsTable.GL.FenceSync = FenceSync; funcsTable.GL.ClientWaitSync = ClientWaitSync; funcsTable.GL.WaitSync = WaitSync; funcsTable.GL.DeleteSync = DeleteSync; funcsTable.GL.GetSyncStatus = GetSyncStatus; // Optional timer-query group: left null (the frontend then falls // back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks // themselves additionally degrade to null handles / zero results // when GL_EXT_disjoint_timer_query or its entry points are // missing, or when the calling thread does not own the ES // context. if (!MG_Config::Features.DisableTimerQuery) { // AreTimerQueriesSupported is a pure capability read (no // current ES context required, false until the caps are // filled in), which is exactly the dynamic support check // the frontend wants from IsTimerQuerySupported. funcsTable.GL.IsTimerQuerySupported = AreTimerQueriesSupported; funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery; funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery; funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp; funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs; } // Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY) // and share the handle-based result/delete entries, which must exist even // when the timer-query group above is disabled. funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery; funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery; // Real driver primitive counters: the frontend's CPU accounting cannot see a // geometry shader's amplification. funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery; funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery; // ...but where it CAN see the whole capture - no geometry stage - the frontend's // own count is the desktop-exact one and the ES driver's is only as good as the // vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that // follows a large render pass). The query above stays installed: it is still what // answers an amplifying span, and PRIMITIVES_GENERATED always. funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true; funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable; funcsTable.GL.GetQueryResult64 = GetQueryResult64; funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery; // Transform feedback is captured by the real ES driver rather than // reconstructed from the draw recording, so the frontend has to hand the // span boundaries over. funcsTable.GL.PatchParameteri = DirectGLES::PatchParameteri; funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback; funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback; funcsTable.GL.PauseTransformFeedback = XfbImpl::PauseTransformFeedback; funcsTable.GL.ResumeTransformFeedback = XfbImpl::ResumeTransformFeedback; funcsTable.GL.BindTransformFeedback = XfbImpl::BindTransformFeedback; funcsTable.GL.DeleteTransformFeedback = XfbImpl::DeleteTransformFeedback; funcsTableInitialized = true; } return funcsTable; } const DynamicBackendParameters& BackendObject_DirectGLES::GetDynamicParameters() const { return m_dynamicParameters; } void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities) { m_GLESCapabilities = capabilities; UpdateDynamicBackendParameters(); } void BackendObject_DirectGLES::UpdateDynamicBackendParameters() { m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment; m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_GLESCapabilities.ShaderStorageBufferOffsetAlignment; m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy; m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin; m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax; m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin; m_dynamicParameters.SmoothLineWidthRangeMax = m_GLESCapabilities.SmoothLineWidthRangeMax; m_dynamicParameters.SmoothLineWidthGranularity = m_GLESCapabilities.SmoothLineWidthGranularity; m_dynamicParameters.PointSizeRangeMin = m_GLESCapabilities.PointSizeRangeMin; m_dynamicParameters.PointSizeRangeMax = m_GLESCapabilities.PointSizeRangeMax; m_dynamicParameters.PointSizeGranularity = m_GLESCapabilities.PointSizeGranularity; m_dynamicParameters.Max3DTextureSize = m_GLESCapabilities.Max3DTextureSize; m_dynamicParameters.MaxArrayTextureLayers = m_GLESCapabilities.MaxArrayTextureLayers; m_dynamicParameters.MaxCubeMapTextureSize = m_GLESCapabilities.MaxCubeMapTextureSize; m_dynamicParameters.MaxFramebufferWidth = m_GLESCapabilities.MaxFramebufferWidth; m_dynamicParameters.MaxFramebufferHeight = m_GLESCapabilities.MaxFramebufferHeight; m_dynamicParameters.MaxFramebufferLayers = m_GLESCapabilities.MaxFramebufferLayers; m_dynamicParameters.MaxRenderbufferSize = m_GLESCapabilities.MaxRenderbufferSize; m_dynamicParameters.MaxTextureSize = m_GLESCapabilities.MaxTextureSize; m_dynamicParameters.MaxColorTextureSamples = m_GLESCapabilities.MaxColorTextureSamples; m_dynamicParameters.MaxDepthTextureSamples = m_GLESCapabilities.MaxDepthTextureSamples; m_dynamicParameters.MaxFramebufferSamples = m_GLESCapabilities.MaxFramebufferSamples; m_dynamicParameters.MaxIntegerSamples = m_GLESCapabilities.MaxIntegerSamples; m_dynamicParameters.MaxSamples = m_GLESCapabilities.MaxSamples; m_dynamicParameters.MaxSampleMaskWords = m_GLESCapabilities.MaxSampleMaskWords; m_dynamicParameters.MaxPatchVertices = m_GLESCapabilities.MaxPatchVertices; m_dynamicParameters.MaxTessGenLevel = m_GLESCapabilities.MaxTessGenLevel; m_dynamicParameters.MinProgramTextureGatherOffset = m_GLESCapabilities.MinProgramTextureGatherOffset; m_dynamicParameters.MaxProgramTextureGatherOffset = m_GLESCapabilities.MaxProgramTextureGatherOffset; // Clamp the advertised sampler limits the same way the DirectVulkan backend does: per-stage // GL_MAX_TEXTURE_IMAGE_UNITS must never exceed host-side fixed arrays sized off it (e.g. // Minecraft's 128-entry Blaze3D GlStateManager.TEXTURES[], iterated by Iris), and the combined // limit must stay within our texture-unit state array capacity. // // The fragment limit is raised to the desktop value (32) even where the ES driver counts 16 // per stage: Iris sizes its WHOLE-PROGRAM unit budget off this query (vertex and fragment // samplers share it, and it refuses the next sampler as soon as the budget is full), so 16 // leaves shader packs such as iterationT without a pipeline at all. Unit NUMBERS up to the // driver's combined limit are legal in every stage, and a stage that really samples more // textures than the driver allows still fails at link with the driver's own log. m_dynamicParameters.MaxTextureImageUnits = std::min(std::max(m_GLESCapabilities.MaxTextureImageUnits, m_GLESCapabilities.MaxCombinedTextureImageUnits), static_cast(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS)); m_dynamicParameters.MaxVertexTextureImageUnits = std::min(m_GLESCapabilities.MaxVertexTextureImageUnits, static_cast(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS)); m_dynamicParameters.MaxComputeTextureImageUnits = std::min(m_GLESCapabilities.MaxComputeTextureImageUnits, static_cast(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS)); m_dynamicParameters.MaxCombinedTextureImageUnits = std::min(m_GLESCapabilities.MaxCombinedTextureImageUnits, static_cast(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS)); // Never advertise more attributes than the state layer can store: the current-value array and // the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS. m_dynamicParameters.MaxVertexAttribs = std::min(m_GLESCapabilities.MaxVertexAttribs, static_cast(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS)); m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks; m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks; // Per-stage storage-block counts, forwarded from the host driver rather than invented. // A stage the driver cannot serve reports 0, which is a legal answer everywhere these // limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every // graphics stage except fragment) and is the only answer that lets an application take // its own fallback instead of building a program the driver will refuse to link. The // stage limit cannot exceed the combined limit or the number of binding points there // are to bind buffers to, so clamp to both. const auto clampStageStorageBlocks = [this](Int stageLimit) { return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0), std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)}); }; m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings; m_dynamicParameters.MaxVertexShaderStorageBlocks = clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks); m_dynamicParameters.MaxTessControlShaderStorageBlocks = clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks); m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks); m_dynamicParameters.MaxGeometryShaderStorageBlocks = clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks); m_dynamicParameters.MaxFragmentShaderStorageBlocks = clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks); m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations; // The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same // numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has // them once the table entry retires (plan B section 4.4.1); GL_Getter floors them. for (SizeT axis = 0; axis < 3; ++axis) { m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis]; m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis]; } // (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.) // 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; m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize; const Int maxSupportedTextureUnits = static_cast(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); m_dynamicParameters.MaxImageUnits = std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0); m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0); const auto clampStageImageUniforms = [this](Int stageLimit) { return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms}); }; m_dynamicParameters.MaxVertexImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms); m_dynamicParameters.MaxTessControlImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxTessControlImageUniforms); m_dynamicParameters.MaxTessEvaluationImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxTessEvaluationImageUniforms); m_dynamicParameters.MaxGeometryImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms); m_dynamicParameters.MaxFragmentImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms); m_dynamicParameters.MaxComputeImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms); m_dynamicParameters.SupportsDistinctDepthStencilAttachments = ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs); ProbeSamplePositions(DirectGLES::g_GLESFuncs, m_GLESCapabilities, m_dynamicParameters); // SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the // attachment's layer passed through, so this backend really does render to the layer it was // given - provided the driver resolved the entry point at all. // SyncAttachmentObject (Managers.cpp, the glFramebufferTextureLayer branch) routes exactly // five upload targets to glFramebufferTextureLayer with the attachment's layer passed // through, so this backend really does render to the layer it was given - provided the driver // resolved the entry point at all. The cube map array is the one target that also needs // ES-level support before it has any storage to attach. m_dynamicParameters.PerLayerFramebufferAttachmentTargets = 0; if (DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr) { using DynParams = MG_Backend::DynamicBackendParameters; m_dynamicParameters.PerLayerFramebufferAttachmentTargets |= DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D) | DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture1DArray) | DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) | DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray); if (m_GLESCapabilities.SupportsTextureCubeMapArray) { m_dynamicParameters.PerLayerFramebufferAttachmentTargets |= DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray); } } // Not a driver question and never will be: GLSL ES has no 64-bit float type in ANY version // or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES profile") and a // module that still declared Float64 would never reach the driver at all. The demotion is // mathematically mandatory here, on every device, forever - which is why this stays false // regardless of what the driver underneath happens to support. m_dynamicParameters.SupportsShaderFloat64 = false; { const EsprytSubgroupParameters subgroups = ResolveEsprytSubgroupParameters(m_GLESCapabilities); m_dynamicParameters.SubgroupSize = subgroups.size; m_dynamicParameters.SubgroupSupportedStages = subgroups.stages; m_dynamicParameters.SubgroupSupportedFeatures = subgroups.features; m_dynamicParameters.SubgroupQuadOperationsInAllStages = subgroups.quadInAllStages; MGLOG_I("DirectGLES: GL_KHR_shader_subgroup %s (size %u, stages 0x%x, features 0x%x)", SubgroupImplementationName(ResolveEsprytSubgroupImplementation(m_GLESCapabilities)), subgroups.size, subgroups.stages, subgroups.features); } // Follows the line above, and must: OpenGL ES has no double-precision vertex format and no // fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here. m_dynamicParameters.SupportsFloat64VertexAttributes = false; // Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two // extension pairs the loader probed, independently, because they really do come // separately. False arms the shared phase-B demotion // (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then // never names the built-in in those stages and needs no extension. // MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be // exercised on a healthy driver (the pinned integration lane); =0 restores the // detected answer's declines. m_dynamicParameters.SupportsTessellationPointSize = m_GLESCapabilities.TessellationPointSizeSupport != MG_External::GLESCapabilities::PointSizeTier::None; m_dynamicParameters.SupportsGeometryPointSize = m_GLESCapabilities.GeometryPointSizeSupport != MG_External::GLESCapabilities::PointSizeTier::None; switch (MG_Config::Features.PointSizeDemotion) { case MG_Config::QuirkOverride::ForceOn: MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry " "gl_PointSize as unhosted so the demotion runs on this driver"); m_dynamicParameters.SupportsTessellationPointSize = false; m_dynamicParameters.SupportsGeometryPointSize = false; break; case MG_Config::QuirkOverride::ForceOff: MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the " "plain declines regardless of the driver's extensions"); m_dynamicParameters.SupportsTessellationPointSize = true; m_dynamicParameters.SupportsGeometryPointSize = true; break; case MG_Config::QuirkOverride::Auto: break; } m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; // The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which // is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler // would reject. m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances; m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances; m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports; // Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX // for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test // devices. That is not a shortfall being hidden: without the extension only viewport 0 is // ever rasterized, so no vertex "selects" a viewport index and naming a convention would // describe behaviour this backend does not implement. m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex; m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex; // GL_EXT_fragment_shading_rate is a straight pass-through: the pipeline rate and the // combiner ops are forwarded per draw (SyncRenderState), gl_ShadingRateEXT and // gl_PrimitiveShadingRateEXT are emitted into the ESSL as the driver's own built-ins, and // the device answers every query. The attachment is not forwarded yet, so it is not // claimed, whatever the driver offers. m_dynamicParameters.SupportsFragmentShadingRate = m_GLESCapabilities.SupportsFragmentShadingRate; m_dynamicParameters.SupportsPrimitiveFragmentShadingRate = m_GLESCapabilities.SupportsFragmentShadingRatePrimitive; m_dynamicParameters.SupportsAttachmentFragmentShadingRate = false; m_dynamicParameters.FragmentShadingRateNonTrivialCombiners = m_GLESCapabilities.FragmentShadingRateNonTrivialCombiners; m_dynamicParameters.FragmentShadingRateWithShaderDepthStencilWrites = m_GLESCapabilities.FragmentShadingRateWithShaderDepthStencilWrites; m_dynamicParameters.FragmentShadingRateWithSampleMask = m_GLESCapabilities.FragmentShadingRateWithSampleMask; m_dynamicParameters.FragmentShadingRatePrimitiveWithMultiViewport = m_GLESCapabilities.FragmentShadingRatePrimitiveWithMultiViewport; for (SizeT i = 0; i < std::size(m_dynamicParameters.FragmentShadingRateMasks); ++i) { m_dynamicParameters.FragmentShadingRateMasks[i] = m_GLESCapabilities.FragmentShadingRateMasks[i]; } m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth; m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight; m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax; m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits; m_dynamicParameters.MinFragmentInterpolationOffset = std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) && m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f ? m_GLESCapabilities.MinFragmentInterpolationOffset : -0.5f; m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f; m_dynamicParameters.FragmentInterpolationOffsetBits = 4; if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 && std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) { const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits); if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) { m_dynamicParameters.MaxFragmentInterpolationOffset = m_GLESCapabilities.MaxFragmentInterpolationOffset; m_dynamicParameters.FragmentInterpolationOffsetBits = m_GLESCapabilities.FragmentInterpolationOffsetBits; } } m_dynamicParameters.SupportsWideLines = m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f; const auto containsAny = [](const String& haystack, std::initializer_list needles) { return std::any_of(needles.begin(), needles.end(), [&](const char* needle) { return haystack.find(needle) != String::npos; }); }; const String vendorAndRenderer = m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString; if (containsAny(vendorAndRenderer, {"llvmpipe", "SwiftShader", "softpipe"})) { // Check software rasterizers first: ANGLE-on-llvmpipe reports both. m_dynamicParameters.GpuVendor = GpuVendorKind::Software; } else if (containsAny(vendorAndRenderer, {"Qualcomm", "Adreno"})) { m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm; } else if (containsAny(vendorAndRenderer, {"Mali", "ARM"})) { m_dynamicParameters.GpuVendor = GpuVendorKind::Arm; } else if (containsAny(vendorAndRenderer, {"NVIDIA"})) { m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia; } else if (containsAny(vendorAndRenderer, {"AMD", "Radeon"})) { m_dynamicParameters.GpuVendor = GpuVendorKind::Amd; } else if (containsAny(vendorAndRenderer, {"Intel"})) { m_dynamicParameters.GpuVendor = GpuVendorKind::Intel; } else if (containsAny(vendorAndRenderer, {"Imagination", "PowerVR"})) { m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec; } else { m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown; } } const MG_External::GLESFunctionsTable& BackendObject_DirectGLES::GetGLESFunctions() const { return m_GLESFunctions; } const MG_External::EGLFunctionsTable& BackendObject_DirectGLES::GetEGLFunctions() const { return m_EGLFunctions; } } // namespace MobileGL::MG_Backend::DirectGLES