diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index ae958394..0168972c 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -5141,6 +5142,150 @@ namespace MobileGL::MG_Backend::DirectGLES { DrainBlitErrors(); } + // ---- glBlitFramebuffer onto a non-zero array layer ------------------------------------- + // + // Some drivers write to layer 0 whatever layer the DRAW framebuffer's + // glFramebufferTextureLayer attachment names, and raise no error doing it (Adreno 830; + // SelfTest::ProbeBlitIgnoresDestinationArrayLayer measures it, with the destination-layer-0 + // case as the control). glCopyImageSubData takes the destination layer as an argument rather + // than reading it off an attachment, and honours it on the same driver - so a blit that is a + // plain 1:1 copy is issued that way instead. + // + // ONLY a plain 1:1 copy. glCopyImageSubData cannot scale, flip, convert format or resolve + // samples, and it is not clipped by the scissor, so every one of those is a reason to hand + // the call back to the driver rather than quietly perform a different operation. Those blits + // still land on the wrong layer; a once-per-process line says so rather than leaving it to be + // rediscovered. + // + // Per aspect, not all-or-nothing: the returned mask is the bits this performed itself, and + // the caller passes the rest to the driver. A COLOR|DEPTH blit whose colour half scales and + // whose depth half does not still gets its depth half repaired. + static GLbitfield BlitLayeredDestinationAspects( + const SharedPtr& readFramebuffer, + const SharedPtr& drawFramebuffer, GLint srcX0, GLint srcY0, + GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) { + if (mask == 0 || !readFramebuffer || !drawFramebuffer) return 0; + if (!g_GLESFuncs.glCopyImageSubData) return 0; + if (!MG_Util::SelfTest::BlitIgnoresDestinationArrayLayer(g_GLESFuncs)) return 0; + + // The default framebuffer has no layers to get wrong, and a blit between the two halves + // of the same framebuffer object is not a shape this substitutes for. + const Int width = srcX1 - srcX0; + const Int height = srcY1 - srcY0; + const Bool oneToOne = width > 0 && height > 0 && (dstX1 - dstX0) == width && (dstY1 - dstY0) == height; + // The scissor clips a blit and does not clip a copy, so an enabled scissor makes the two + // different operations no matter how the rectangles line up. + const Bool scissorEnabled = + (RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0; + + using MobileGL::FramebufferAttachmentType; + struct AspectPlan { + GLbitfield bit; + FramebufferAttachmentType source; + FramebufferAttachmentType destination; + }; + // The colour aspect follows glReadBuffer on the read side and draw buffer 0 on the write + // side, which is the only draw buffer a blit onto a layered destination can be pinned to + // here: a blit writes EVERY enabled draw buffer, so a framebuffer with more than one is + // left to the driver rather than half-repaired. + const auto& drawBuffers = drawFramebuffer->GetDrawBuffers(); + // GL_NONE is what an unwritten draw-buffer slot holds, and it is a different value from + // the "no such attachment" one - counting it as enabled made every framebuffer look like + // it had eight and sent every colour blit to the driver. + Int enabledDrawBuffers = 0; + for (const FramebufferAttachmentType buffer : drawBuffers) { + if (buffer != FramebufferAttachmentType::Unknown && buffer != FramebufferAttachmentType::None) { + ++enabledDrawBuffers; + } + } + const AspectPlan plans[] = { + {GL_COLOR_BUFFER_BIT, readFramebuffer->GetReadBuffer(), drawBuffers[0]}, + {GL_DEPTH_BUFFER_BIT, FramebufferAttachmentType::Depth, FramebufferAttachmentType::Depth}, + {GL_STENCIL_BUFFER_BIT, FramebufferAttachmentType::Stencil, FramebufferAttachmentType::Stencil}, + }; + + GLbitfield handled = 0; + for (const AspectPlan& plan : plans) { + if ((mask & plan.bit) == 0) continue; + if (plan.source == FramebufferAttachmentType::Unknown || + plan.destination == FramebufferAttachmentType::Unknown || + plan.source == FramebufferAttachmentType::None || + plan.destination == FramebufferAttachmentType::None) { + continue; + } + const auto& sourceAttachment = readFramebuffer->GetAttachment(plan.source); + const auto& destinationAttachment = drawFramebuffer->GetAttachment(plan.destination); + // Renderbuffers have no layers, so a destination that is one cannot be hitting this. + if (!sourceAttachment.IsTexture() || !destinationAttachment.IsTexture()) continue; + // Layer 0 is the case the driver gets right, and a LAYERED attachment (glFramebufferTexture + // with no layer) blits its layer 0 by spec - neither is this defect. + if (destinationAttachment.GetTextureLayer() == 0) continue; + if (destinationAttachment.IsLayered() || sourceAttachment.IsLayered()) continue; + + const auto& sourceTexture = sourceAttachment.GetTexture(); + const auto& destinationTexture = destinationAttachment.GetTexture(); + if (!sourceTexture || !destinationTexture) continue; + // glCopyImageSubData moves texel blocks: same format both ends, or it is a different + // operation. Multisample endpoints would additionally have to agree on sample count, + // which is a resolve the driver still owns. + if (sourceTexture->GetFormat() != destinationTexture->GetFormat()) continue; + if (sourceTexture->GetSamples() > 0 || destinationTexture->GetSamples() > 0) continue; + + // A combined depth-stencil texture is ONE image to glCopyImageSubData: it carries both + // aspects across whether or not the mask asked for both. Taking only GL_DEPTH_BUFFER_BIT + // on a DEPTH24_STENCIL8 destination would overwrite a stencil the application asked to + // keep, so the copy is only allowed when the mask covers everything the format holds. + const TextureInternalFormat format = destinationTexture->GetFormat(); + const Bool hasDepth = MG_Util::IsDepthFormatInternalFormat(format); + const Bool hasStencil = MG_Util::IsStencilFormatInternalFormat(format); + if (hasDepth && (mask & GL_DEPTH_BUFFER_BIT) == 0) continue; + if (hasStencil && (mask & GL_STENCIL_BUFFER_BIT) == 0) continue; + // ... and having carried both, it must be credited with both, or the caller hands the + // stencil half to the driver and it lands on layer 0 after all. + const GLbitfield aspectBits = + hasDepth || hasStencil + ? static_cast((hasDepth ? GL_DEPTH_BUFFER_BIT : 0) | + (hasStencil ? GL_STENCIL_BUFFER_BIT : 0)) + : static_cast(GL_COLOR_BUFFER_BIT); + if ((handled & aspectBits) == aspectBits) continue; + + if (!oneToOne || scissorEnabled || (plan.bit == GL_COLOR_BUFFER_BIT && enabledDrawBuffers != 1)) { + MGLOG_E_ONCE("BlitFramebuffer: this driver ignores a non-zero destination array layer and this " + "blit cannot be expressed as a copy (%s), so it will land on layer 0", + !oneToOne ? "it scales or flips" + : scissorEnabled ? "the scissor test is enabled" + : "the destination has more than one draw buffer"); + continue; + } + + auto backendSource = TextureImpl::SyncTextureObjectToBackend(sourceTexture); + auto backendDestination = TextureImpl::SyncTextureObjectToBackend(destinationTexture); + if (!backendSource || !backendDestination) continue; + const GLuint sourceName = backendSource->GetBackendTextureId(); + const GLuint destinationName = backendDestination->GetBackendTextureId(); + if (sourceName == 0 || destinationName == 0) continue; + const GLenum sourceTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(sourceTexture->GetTarget()); + const GLenum destinationTarget = + TextureImpl::ConvertTextureTargetToBackendGLEnum(destinationTexture->GetTarget()); + + ClearGLErrors(); + g_GLESFuncs.glCopyImageSubData(sourceName, sourceTarget, sourceAttachment.GetTextureLevel(), srcX0, srcY0, + sourceAttachment.GetTextureLayer(), destinationName, destinationTarget, + destinationAttachment.GetTextureLevel(), dstX0, dstY0, + destinationAttachment.GetTextureLayer(), width, height, 1); + if (const GLenum error = g_GLESFuncs.glGetError(); error != GL_NO_ERROR) { + // The driver blit still runs for this aspect - onto the wrong layer, but the + // substitute has to leave the call no worse off than it found it. + MGLOG_E_ONCE("BlitFramebuffer: the layered-destination copy substitute failed with %s; the " + "driver blit will run instead and land on layer 0", + MG_Util::ConvertGLEnumToString(error).c_str()); + continue; + } + handled |= aspectBits; + } + return handled & mask; + } + void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER @@ -5167,7 +5312,15 @@ namespace MobileGL::MG_Backend::DirectGLES { }); MGLOG_D("ES %s(%d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", __func__, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str()); - IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + // A no-op on every driver that honours a non-zero destination array layer, which is all + // of them but the probed one. Whatever it performs itself is taken out of the mask. + mask &= ~BlitLayeredDestinationAspects( + MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), + MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0, + srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask); + if (mask != 0) { + IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); @@ -5189,7 +5342,12 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ES %s(%d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", __func__, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str()); - IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + // See the DSA-free entry point above: only the probed defect makes this do anything. + mask &= ~BlitLayeredDestinationAspects(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, + dstY0, dstX1, dstY1, mask); + if (mask != 0) { + IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } // Debug-only diagnostics: which GLES depth texture did this blit write? #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG if (mask & GL_DEPTH_BUFFER_BIT) { diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index 81075cb4..ae05bc04 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -122,6 +122,7 @@ namespace MobileGL::MG_Util::SelfTest { GLint activeTexture = GL_TEXTURE0; GLint texture2D = 0; GLint texture2DMultisample = 0; + GLint texture2DArray = 0; GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; GLint packAlignment = 4; GLint packRowLength = 0; @@ -155,6 +156,7 @@ namespace MobileGL::MG_Util::SelfTest { gl.glGetIntegerv(GL_ACTIVE_TEXTURE, &state.activeTexture); gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &state.texture2D); gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &state.texture2DMultisample); + gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &state.texture2DArray); gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment); gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength); if (gl.glGetFloatv != nullptr) { @@ -205,6 +207,7 @@ namespace MobileGL::MG_Util::SelfTest { gl.glBindTexture(GL_TEXTURE_2D, static_cast(state.texture2D)); gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, static_cast(state.texture2DMultisample)); + gl.glBindTexture(GL_TEXTURE_2D_ARRAY, static_cast(state.texture2DArray)); } gl.glActiveTexture(static_cast(state.activeTexture)); } @@ -1348,6 +1351,174 @@ namespace MobileGL::MG_Util::SelfTest { } namespace { + // ===================== LAYERED BLIT DESTINATION ===================== + + constexpr const char* kLayeredBlitProbeName = "layered blit destination"; + // Four texels wide: a 1x1 blit is a shape drivers special-case, and a rectangle keeps + // the probe on the ordinary path. Two layers is all the question needs. + constexpr GLsizei kLayeredBlitSize = 4; + constexpr GLsizei kLayeredBlitLayers = 2; + + // One RGBA8 2D array whose every layer is filled with a distinguishable byte. + GLuint MakeLayeredBlitTexture(const GLESFunctionsTable& gl, GLubyte layer0, GLubyte layer1) { + GLuint texture = 0; + gl.glGenTextures(1, &texture); + if (texture == 0) return 0; + gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + gl.glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kLayeredBlitSize, kLayeredBlitSize, + kLayeredBlitLayers); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + const GLubyte fills[kLayeredBlitLayers] = {layer0, layer1}; + for (GLint layer = 0; layer < kLayeredBlitLayers; ++layer) { + GLubyte texels[kLayeredBlitSize * kLayeredBlitSize * 4]; + for (SizeT i = 0; i < sizeof(texels); i += 4) { + texels[i + 0] = fills[layer]; + texels[i + 1] = fills[layer]; + texels[i + 2] = fills[layer]; + texels[i + 3] = 255; + } + gl.glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kLayeredBlitSize, kLayeredBlitSize, 1, + GL_RGBA, GL_UNSIGNED_BYTE, texels); + } + gl.glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + return texture; + } + + // A framebuffer naming exactly one layer of one array texture. + GLuint MakeLayeredBlitFramebuffer(const GLESFunctionsTable& gl, GLuint texture, GLint layer) { + GLuint framebuffer = 0; + gl.glGenFramebuffers(1, &framebuffer); + if (framebuffer == 0) return 0; + gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + gl.glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, layer); + if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + gl.glDeleteFramebuffers(1, &framebuffer); + return 0; + } + return framebuffer; + } + + // The red byte of texel (0, 0) of one layer, read through a framebuffer that names it. + // 256 is "could not read", which no fill value can be. + Int ReadLayeredBlitTexel(const GLESFunctionsTable& gl, GLuint texture, GLint layer) { + const GLuint framebuffer = MakeLayeredBlitFramebuffer(gl, texture, layer); + if (framebuffer == 0) return 256; + gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + gl.glReadBuffer(GL_COLOR_ATTACHMENT0); + gl.glPixelStorei(GL_PACK_ALIGNMENT, 1); + GLubyte pixel[4] = {0, 0, 0, 0}; + gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + gl.glDeleteFramebuffers(1, &framebuffer); + Drain(gl); + return static_cast(pixel[0]); + } + + // Blits source layer 1 onto `destinationLayer` of a freshly filled destination and + // reports which layer actually received it, or -1 when the blit could not be issued. + Int LayeredBlitLandsOnLayer(const GLESFunctionsTable& gl, GLint destinationLayer, GLubyte magic) { + const GLuint source = MakeLayeredBlitTexture(gl, 0x11, magic); + const GLuint destination = MakeLayeredBlitTexture(gl, 0x33, 0x44); + const GLuint sourceFramebuffer = MakeLayeredBlitFramebuffer(gl, source, 1); + const GLuint destinationFramebuffer = MakeLayeredBlitFramebuffer(gl, destination, destinationLayer); + Int landedOn = -1; + if (source != 0 && destination != 0 && sourceFramebuffer != 0 && destinationFramebuffer != 0) { + gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer); + gl.glReadBuffer(GL_COLOR_ATTACHMENT0); + gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFramebuffer); + Drain(gl); + gl.glBlitFramebuffer(0, 0, kLayeredBlitSize, kLayeredBlitSize, 0, 0, kLayeredBlitSize, + kLayeredBlitSize, GL_COLOR_BUFFER_BIT, GL_NEAREST); + if (gl.glGetError() == GL_NO_ERROR) { + landedOn = -2; // issued, but seen on no layer yet + for (GLint layer = 0; layer < kLayeredBlitLayers; ++layer) { + if (ReadLayeredBlitTexel(gl, destination, layer) == static_cast(magic)) { + landedOn = layer; + break; + } + } + } + } + if (sourceFramebuffer != 0) gl.glDeleteFramebuffers(1, &sourceFramebuffer); + if (destinationFramebuffer != 0) gl.glDeleteFramebuffers(1, &destinationFramebuffer); + if (source != 0) gl.glDeleteTextures(1, &source); + if (destination != 0) gl.glDeleteTextures(1, &destination); + Drain(gl); + return landedOn; + } + } // namespace + + Bool ProbeBlitIgnoresDestinationArrayLayer(const GLESFunctionsTable& gl) { + if (!gl.glGenTextures || !gl.glBindTexture || !gl.glTexStorage3D || !gl.glTexSubImage3D || + !gl.glTexParameteri || !gl.glDeleteTextures || !gl.glGenFramebuffers || !gl.glBindFramebuffer || + !gl.glFramebufferTextureLayer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || + !gl.glBlitFramebuffer || !gl.glReadBuffer || !gl.glReadPixels || !gl.glPixelStorei || !gl.glGetError || + !gl.glIsEnabled || !gl.glDisable) { + return false; + } + + SavedState saved; + Save(gl, saved); + // A scissor left on by whoever ran before would clip the probe's own blit and make a + // working driver look broken. + gl.glDisable(GL_SCISSOR_TEST); + Drain(gl); + + // THE CONTROL: the same blit onto destination layer 0, which is the case no + // implementation gets wrong. It also proves the SOURCE layer is honoured, since the + // magic byte it looks for only exists on source layer 1 - so a driver that cannot blit + // between array layers at all, or that has no working glFramebufferTextureLayer, fails + // here and reaches no verdict rather than being reported as having this bug. + const Int controlLanded = LayeredBlitLandsOnLayer(gl, 0, 0x5Au); + Bool detected = false; + if (controlLanded != 0) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the destination-layer-0 control " + "landed on layer %d instead of 0)", + kLayeredBlitProbeName, controlLanded); + } else { + // THE SUBJECT: the identical blit asking for layer 1. Only the destination layer moved. + const Int subjectLanded = LayeredBlitLandsOnLayer(gl, 1, 0x5Au); + detected = subjectLanded == 0; + if (subjectLanded != 0 && subjectLanded != 1) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the subject blit landed on " + "no layer at all: %d)", + kLayeredBlitProbeName, subjectLanded); + } else { + MGLOG_I("[driver-bug] %s probe: a blit asking for destination layer 1 landed on " + "layer %d%s", + kLayeredBlitProbeName, subjectLanded, + detected ? " - THE DESTINATION LAYER IS IGNORED" : ""); + } + } + + Restore(gl, saved); + return detected; + } + + Bool BlitIgnoresDestinationArrayLayer(const GLESFunctionsTable& gl) { + // One driver per process, and the answer is structural rather than sampled. + static const Bool ignored = ProbeBlitIgnoresDestinationArrayLayer(gl); + return ignored; + } + + namespace { + Optional ProbeLayeredBlitDestinationBug(const GLESFunctionsTable& gl) { + if (!BlitIgnoresDestinationArrayLayer(gl)) return std::nullopt; + return DriverBugFinding{ + "glBlitFramebuffer ignores the destination array layer", + DriverBugVerdict::Fixed, + "a glBlitFramebuffer whose DRAW framebuffer attaches a non-zero array layer with " + "glFramebufferTextureLayer writes to layer 0 instead, for colour and depth alike, " + "and raises no error doing it. The layer is honoured everywhere else on the same " + "driver - rendering into it works, glReadPixels off it works, and the blit's own " + "SOURCE layer is read correctly - so neither layered attachments nor blitting is " + "withdrawn. MobileGL performs such a blit with glCopyImageSubData instead, which " + "takes the destination layer explicitly and honours it here; a blit that scales, " + "flips, changes format, resolves samples or is clipped by the scissor cannot be " + "expressed that way and is still handed to the driver"}; + } + // ===================== EXPLICIT VERTEX INPUT LOCATION CEILING ===================== constexpr const char* kAttributeLocationProbeName = "explicit vertex input location"; @@ -1639,6 +1810,7 @@ namespace MobileGL::MG_Util::SelfTest { &ProbeCrossStageImageQualifierMergeBug, &ProbeImageCoherencyResidualBug, &ProbeExplicitVertexInputLocationCeilingBug, + &ProbeLayeredBlitDestinationBug, }; } // namespace diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index 4e2de4b9..a77b21b0 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -55,6 +55,27 @@ namespace MobileGL::MG_Util::SelfTest { String detail; }; + // Blits one layer of an RGBA8 2D array onto another array's layer 1 and reports whether the + // copy landed where it was asked to. Returns true only when the destination layer is ignored + // while the control lands correctly. + // + // Adreno 830 writes to layer 0 whatever layer the DRAW framebuffer's + // glFramebufferTextureLayer attachment names, for colour and depth alike, and raises no + // error. Everything else about the layer works on the same driver, which is what makes this + // a blit defect rather than a layered-attachment one. + // + // THE CONTROL is the same blit onto destination layer 0. It passes on every implementation + // that can blit between array layers at all, and because the value it looks for exists only + // on the SOURCE's layer 1 it also proves the source layer is honoured - so a driver with no + // working glFramebufferTextureLayer reaches no verdict instead of being reported as this. + // + // Returns false when an entry point is missing, when the probe's own framebuffers come back + // incomplete, or when the control fails. Restores every piece of GL state it touches. + Bool ProbeBlitIgnoresDestinationArrayLayer(const MG_External::GLESFunctionsTable& gl); + + // ProbeBlitIgnoresDestinationArrayLayer(), evaluated at most once per process. + Bool BlitIgnoresDestinationArrayLayer(const MG_External::GLESFunctionsTable& gl); + // What the vertex-input location probe measured. The ceiling is reported rather than // hard-coded: it is a driver property, and a clamp derived from a number measured on some // other device is exactly the hard-coded vendor quirk this file exists to avoid.