Files
MobileGL/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
T
gold 8e3db2cb87 [Feature] (gbm): the server offers each taken frame to the container
The channel existed and had no callers.  Now the frame the server takes from the host is offered to
the container with the geometry the host decided, the apply loop consumes the releases the
compositor sends back, and the teardown that drops the host frame closes the channel.

Every call is on the optional path: with no container the render path is what it was.
2026-10-01 03:42:18 +08:00

1884 lines
112 KiB
C++

// 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 <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <Config.h>
#include <algorithm>
#include <cmath>
#include <format>
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;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// 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;
}
#endif
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<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions(
const MG_External::GLESCapabilities& capabilities) {
Flags<PixelFormatNormalizeOptionBit> options;
if (capabilities.IsAngleRenderer) {
options |= PixelFormatNormalizeOptionBit::NoRgb16;
options |= PixelFormatNormalizeOptionBit::NoSnorm16;
options |= PixelFormatNormalizeOptionBit::NoSnorm8;
}
return options;
}
Flags<PixelFormatNormalizeOptionBit> GetDriverPixelFormatNormalizeOptions(
const MG_External::GLESCapabilities& capabilities) {
Flags<PixelFormatNormalizeOptionBit> options = PixelFormatNormalizeOptionBit::NoDepthComponent32;
options |= PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
options |= PixelFormatNormalizeOptionBit::NoRGB16Snorm;
if (!capabilities.SupportsNorm16Texture) {
options |= PixelFormatNormalizeOptionBit::NoNorm16;
}
return options;
}
String BuildPixelFormatFallbackReason(Flags<PixelFormatNormalizeOptionBit> options, Bool forced) {
Vector<String> 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<PixelFormatNormalizeOptionBit> options,
Bool forced, GLESProbeFormatInfo& outInfo) {
const Flags<PixelFormatNormalizeOptionBit> 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<GLuint>(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<GLuint>(prevFramebuffer));
gl.glDeleteFramebuffers(1, &framebuffer);
return false;
}
const Bool complete = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(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<GLuint>(prevFramebuffer));
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
gl.glDeleteFramebuffers(1, &framebuffer);
gl.glDeleteRenderbuffers(2, renderbuffers);
return supported;
}
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<GLuint>(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<GLsizei>(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<GLuint>(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<GLint>(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<GLint>(internalFormat), 2, 2, 0, imageFormat, imageType,
nullptr);
}
break;
case TextureTarget::Texture3D:
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 2, 0, imageFormat, imageType,
nullptr);
break;
case TextureTarget::Texture2DArray:
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 1, 0, imageFormat, imageType,
nullptr);
break;
case TextureTarget::TextureCubeMapArray:
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(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<GLuint>(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<GLuint>(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<GLuint>(prevRenderbuffer));
gl.glDeleteRenderbuffers(1, &renderbuffer);
ClearGLErrors(gl);
return complete;
}
Vector<Int> ProbeRenderbufferSampleCounts(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat,
TextureInternalFormat logicalFormat, Int maxSamples) {
Vector<Int> 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<Int> ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLenum internalFormat, GLenum imageFormat, GLenum imageType,
TextureInternalFormat logicalFormat, Int maxSamples) {
Vector<Int> 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<PixelFormatNormalizeOptionBit> forcedOptions =
GetForcedPixelFormatNormalizeOptions(capabilities);
const Flags<PixelFormatNormalizeOptionBit> driverOptions =
GetDriverPixelFormatNormalizeOptions(capabilities);
for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
const auto logicalFormat = static_cast<TextureInternalFormat>(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<TextureTarget>(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<PixelFormatNormalizeOptionBit> 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<PixelFormatNormalizeOptionBit> 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<PixelFormatNormalizeOptionBit> renderbufferOptions =
TextureImpl::GetRenderTargetNormalizeOptions(capabilities, renderbufferTargetIndex);
GLESProbeFormatInfo renderbufferFallbackInfo = outerFallbackInfo;
Bool renderbufferHasForcedFallback = outerHasForcedFallback;
if (renderbufferOptions) {
const Flags<PixelFormatNormalizeOptionBit> 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);
}
} // 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<SizeT>(logicalFormat);
#if MOBILEGL_BUILD_DISAGGREGATED
// 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<Int>& probedCounts = activeCaps->SampleCounts[targetIndex][formatIndex];
if (!probedCounts.empty()) {
maxSamples = probedCounts.front();
}
}
#else
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
formatIndex < kFormatCapabilityFormatCount) {
// Descending, so the head is the largest count this device actually allocated.
const Vector<Int>& probedCounts =
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
if (!probedCounts.empty()) {
maxSamples = probedCounts.front();
}
}
#endif
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::InitWindowSurface() {
// Only use EGL for now
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
if (!DirectGLES::InitWindowSurface(nativeWindow)) {
MGLOG_E("Failed to initialize window surface for DirectGLES backend");
return false;
}
return true;
}
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();
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 std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
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;
}
const Bool sameHandle = m_eglSurfaceInitialized && m_eglSurface == surface &&
m_eglSurfaceKind == SurfaceKind::Window && m_windowHandle.Backend == handle.Backend &&
m_windowHandle.Handle == handle.Handle;
if (sameHandle) {
return true;
}
if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState();
}
#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) {
NoteNativeContextFreshFromSurfaceCreation();
} else {
NoteNativeContextGone();
}
return created;
#else
return BackendObject::CreateEGLWindowSurface(surface, handle);
#endif
}
Bool BackendObject_DirectGLES::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("DirectGLES backend not initialized");
return false;
}
if (m_eglSurfaceInitialized && m_eglSurface == surface && m_eglSurfaceKind == SurfaceKind::Pbuffer) {
return true;
}
if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// 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) {
NoteNativeContextFreshFromSurfaceCreation();
} else {
NoteNativeContextGone();
}
return created;
#else
return BackendObject::CreateEGLPbufferSurface(surface, width, height);
#endif
}
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
namespace {
// The host may still be starting when the client asks for its surface: it is a separate
// app that has to come up and allocate its pool. Waiting is better than refusing a
// surface a client is entitled to, and the wait is answered by name if it expires.
constexpr Int kHostFrameWaitMs = 30000;
}
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
// The EGL context comes from the same place a pbuffer session's does. What changes is not
// how this side draws but what it draws INTO: a frame the display host allocated, bound as
// this context's framebuffer. The pbuffer stays as the draw target for the moment before
// the first frame arrives, which is the only moment it is used.
if (!DirectGLES::InitPbufferSurface(width, height)) {
MGLOG_E("InitHostFrameSurface: the context the host frames are drawn in did not come up");
return false;
}
if (!m_hostFrameBridgeOpen) {
String why;
if (!m_hostFrameBridge.Open(MG_Backend::kHostFrameSocket, kHostFrameWaitMs, why)) {
MGLOG_E("InitHostFrameSurface: no display host: %s", why.c_str());
return false;
}
m_hostFrameBridgeOpen = true;
}
return TakeHostFrame();
}
Bool BackendObject_DirectGLES::TakeHostFrame() {
String why;
struct AHardwareBuffer* frame = nullptr;
MG_Backend::HostFrameOffer offer{};
if (!m_hostFrameBridge.Acquire(&frame, offer, why)) {
MGLOG_E("TakeHostFrame: %s", why.c_str());
return false;
}
HostFrameTarget target;
if (!HostFrameTargetCreate(frame, target, why)) {
MGLOG_E("TakeHostFrame: the host frame %ux%u did not become a draw target: %s", offer.Width,
offer.Height, why.c_str());
AHardwareBuffer_release(frame);
return false;
}
DestroyHostFrame();
m_hostFrameTarget = target;
m_hostFrameBuffer = frame;
m_hostFrameOffer = offer;
// BEFORE the bind: every default-framebuffer bind the engine makes from here on has to
// resolve to this frame, and that resolution is what this global is.
g_hostFrameFramebufferId = m_hostFrameTarget.Framebuffer;
FramebufferImpl::InvalidateFramebufferBindingCache();
HostFrameTargetBind(m_hostFrameTarget);
// AND ON TO THE CONTAINER, as the dma-buf descriptor behind it. The Wayland compositor
// over there renders into THESE frames - it runs this project's own GBM, whose
// gbm_bo_create answers with the phone's frame instead of an allocation of its own - so
// every frame taken here is useless to it until its descriptor has crossed. Open() binds
// the listening socket a container dials, and the IsUp() test keeps the ordinary frame
// down to an integer compare: the socket outlives every container and every connection,
// while OfferFrame accepts whoever dialled since the last frame and opens the socket
// itself when the first frame arrives before any container has.
//
// NEITHER ANSWER IS EXAMINED, and that is the channel's contract rather than an
// oversight: no container running, a container that has died, or one that has taken the
// descriptor and stopped reading must all leave this frame drawn and presented exactly as
// it would have been without a channel - the descriptor is a copy of the pixels' route,
// not a step in producing them.
if (!m_gbmFrameChannel.IsUp()) m_gbmFrameChannel.Open();
(void)m_gbmFrameChannel.OfferFrame(frame, offer);
MGLOG_I("host frame %u is the draw target: %ux%u", offer.Index, target.Width, target.Height);
return true;
}
void BackendObject_DirectGLES::DestroyHostFrame() {
g_hostFrameFramebufferId = 0;
HostFrameTargetDestroy(m_hostFrameTarget);
if (m_hostFrameBuffer != nullptr) {
AHardwareBuffer_release(m_hostFrameBuffer);
m_hostFrameBuffer = nullptr;
}
// THE CONNECTION THAT DESCRIBED IT GOES WITH IT. Every frame this side drops is one the
// container was told to render into by descriptor, and a channel left open across that
// would go on describing frames as if the one it named were still the draw target - so
// the connection ends here, the container falls back to allocating frames of its own
// (the state it starts in), and the next frame taken opens the socket again and is
// offered to whoever dials. Close() is idempotent and safe from a destructor, which is
// also what the member's own does at backend teardown, so this call costs the paths that
// drop nothing - a take that replaces the frame, a teardown before the first one - a
// handful of integer tests.
m_gbmFrameChannel.Close();
}
Bool BackendObject_DirectGLES::PresentHostFrame() {
// When this returns, the pixels are complete: glFinish is the fence the host is told
// about, and the host's own readback - which Complete reads - is what proves the drawing
// landed where the screen will look.
if (g_GLESFuncs.glFinish != nullptr) g_GLESFuncs.glFinish();
// WHAT THE CONTAINER SAID WHILE THIS FRAME WAS BEING DRAWN. This is the one place the
// server's apply loop passes through the host channel - the swap it runs lands here for a
// host-framed surface - and a release is the container's compositor saying it is done
// reading a frame this side handed over. Poll() is a bare recvmsg on a non-blocking
// socket: with no container connected it returns on its first test, and a container that
// is between frames answers EAGAIN, so a pass costs nothing when there is nothing to
// read. Nothing here is waited for and nothing is required to arrive; the count of
// frames offered and not released is a number the channel keeps for the log, never a
// frame the render path waits on.
m_gbmFrameChannel.Poll();
String why;
if (!m_hostFrameBridge.Complete(m_hostFrameOffer, /*imported=*/true, /*drawn=*/true, /*fenceOk=*/true, why)) {
MGLOG_E("PresentHostFrame: %s", why.c_str());
return false;
}
// The answer has been read, so the host has already put this frame on the glass: it is
// this side's to drop, and the next one becomes the drawing target.
DestroyHostFrame();
return TakeHostFrame();
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
(void)width;
(void)height;
MGLOG_E("InitHostFrameSurface: host frames are a disaggregated Android path");
return false;
}
Bool BackendObject_DirectGLES::TakeHostFrame() { return false; }
void BackendObject_DirectGLES::DestroyHostFrame() {}
Bool BackendObject_DirectGLES::PresentHostFrame() {
MGLOG_E("PresentHostFrame: host frames are a disaggregated Android path");
return false;
}
#endif
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
return DirectGLES::InitPbufferSurface(width, height);
}
Bool BackendObject_DirectGLES::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (IsReleaseCurrentRequest(dpy, draw, read, ctx)) {
if (!DirectGLES::ReleaseCurrent()) {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
}
if (!m_initialized) {
MGLOG_E("DirectGLES backend not initialized");
return false;
}
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
return false;
}
if (!m_eglSurfaceInitialized) {
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 &&
m_eglSurfaceInitialized && m_eglSurface == draw &&
DirectGLES::IsBackendContextCurrentOnThisThread() &&
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
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();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
return false;
}
// A HOST-FRAMED SURFACE HAS TO BE PUT BACK AFTER EVERY MAKE-CURRENT. The native call
// binds the surface's own default framebuffer - the pbuffer this path makes its context
// with - and that is not where these pixels go. Both the driver and the engine's binding
// shadow have to be told again, or the next default-framebuffer bind is deduped away and
// every draw lands in a pbuffer nobody sees.
if (m_eglSurfaceKind == SurfaceKind::HostFrame && m_hostFrameTarget.Framebuffer != 0) {
FramebufferImpl::InvalidateFramebufferBindingCache();
HostFrameTargetBind(m_hostFrameTarget);
}
return true;
}
Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
void BackendObject_DirectGLES::ReleaseEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::ReleaseEGLSurface(surface);
}
void BackendObject_DirectGLES::ReleaseEGLResources() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
DestroyEGLContext();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
BackendObject::ReleaseEGLResources();
}
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
DestroyEGLContext();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
}
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
return MutableRendererInfo();
}
String BackendObject_DirectGLES::GetBackendAPIVersionString() const {
if (!m_initialized) {
return "<uninitialized DirectGLES backend>";
}
return FormatBackendAPIVersionString(m_GLESCapabilities.GLESRendererString,
m_GLESCapabilities.GLESVersion.Major,
m_GLESCapabilities.GLESVersion.Minor);
}
const RendererInfo& GetRendererIdentity() {
return MutableRendererInfo();
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported) {
Vector<GLExtension> 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,
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,
// 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};
// 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 Renderer>, OpenGL ES <OpenGL ES Version>
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.MultiDrawElements = MultiDrawElements;
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.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
funcsTable.GL.CopyImageSubData = CopyImageSubData;
funcsTable.GL.GenerateMipmap = GenerateMipmap;
funcsTable.GL.ReadPixels = ReadPixels;
funcsTable.GL.GetTexImage = GetTexImage;
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.
m_dynamicParameters.MaxTextureImageUnits =
std::min(m_GLESCapabilities.MaxTextureImageUnits,
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS));
m_dynamicParameters.MaxVertexTextureImageUnits =
std::min(m_GLESCapabilities.MaxVertexTextureImageUnits,
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS));
m_dynamicParameters.MaxComputeTextureImageUnits =
std::min(m_GLESCapabilities.MaxComputeTextureImageUnits,
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS));
m_dynamicParameters.MaxCombinedTextureImageUnits =
std::min(m_GLESCapabilities.MaxCombinedTextureImageUnits,
static_cast<Int>(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<Int>(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<Int>(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.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);
// 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;
// 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;
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<const char*> 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