[Fix] (DirectGLES): blit onto a non-zero array layer with glCopyImageSubData where the driver ignores the destination layer

This commit is contained in:
2026-08-25 05:27:46 -04:00
parent 52c050131e
commit 1cde801a01
3 changed files with 353 additions and 2 deletions
+160 -2
View File
@@ -20,6 +20,7 @@
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h> #include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h> #include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h> #include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/SelfTest/DriverBugProbes.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h> #include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h> #include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
@@ -5141,6 +5142,150 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrainBlitErrors(); 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<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& 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<GLbitfield>((hasDepth ? GL_DEPTH_BUFFER_BIT : 0) |
(hasStencil ? GL_STENCIL_BUFFER_BIT : 0))
: static_cast<GLbitfield>(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, void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) { GLint dstY1, GLbitfield mask, GLenum filter) {
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER #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, 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()); 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) { DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); 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, 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()); 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? // Debug-only diagnostics: which GLES depth texture did this blit write?
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
if (mask & GL_DEPTH_BUFFER_BIT) { if (mask & GL_DEPTH_BUFFER_BIT) {
@@ -122,6 +122,7 @@ namespace MobileGL::MG_Util::SelfTest {
GLint activeTexture = GL_TEXTURE0; GLint activeTexture = GL_TEXTURE0;
GLint texture2D = 0; GLint texture2D = 0;
GLint texture2DMultisample = 0; GLint texture2DMultisample = 0;
GLint texture2DArray = 0;
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
GLint packAlignment = 4; GLint packAlignment = 4;
GLint packRowLength = 0; GLint packRowLength = 0;
@@ -155,6 +156,7 @@ namespace MobileGL::MG_Util::SelfTest {
gl.glGetIntegerv(GL_ACTIVE_TEXTURE, &state.activeTexture); gl.glGetIntegerv(GL_ACTIVE_TEXTURE, &state.activeTexture);
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &state.texture2D); gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &state.texture2D);
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &state.texture2DMultisample); 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_ALIGNMENT, &state.packAlignment);
gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength); gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength);
if (gl.glGetFloatv != nullptr) { if (gl.glGetFloatv != nullptr) {
@@ -205,6 +207,7 @@ namespace MobileGL::MG_Util::SelfTest {
gl.glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(state.texture2D)); gl.glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(state.texture2D));
gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE,
static_cast<GLuint>(state.texture2DMultisample)); static_cast<GLuint>(state.texture2DMultisample));
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, static_cast<GLuint>(state.texture2DArray));
} }
gl.glActiveTexture(static_cast<GLenum>(state.activeTexture)); gl.glActiveTexture(static_cast<GLenum>(state.activeTexture));
} }
@@ -1348,6 +1351,174 @@ namespace MobileGL::MG_Util::SelfTest {
} }
namespace { 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<Int>(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<Int>(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<DriverBugFinding> 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 ===================== // ===================== EXPLICIT VERTEX INPUT LOCATION CEILING =====================
constexpr const char* kAttributeLocationProbeName = "explicit vertex input location"; constexpr const char* kAttributeLocationProbeName = "explicit vertex input location";
@@ -1639,6 +1810,7 @@ namespace MobileGL::MG_Util::SelfTest {
&ProbeCrossStageImageQualifierMergeBug, &ProbeCrossStageImageQualifierMergeBug,
&ProbeImageCoherencyResidualBug, &ProbeImageCoherencyResidualBug,
&ProbeExplicitVertexInputLocationCeilingBug, &ProbeExplicitVertexInputLocationCeilingBug,
&ProbeLayeredBlitDestinationBug,
}; };
} // namespace } // namespace
@@ -55,6 +55,27 @@ namespace MobileGL::MG_Util::SelfTest {
String detail; 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 // 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 // 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. // other device is exactly the hard-coded vendor quirk this file exists to avoid.