[Fix] (Espryt): the image-bindable re-mint reads every level back from the driver texture before replaying it, so GPU-written levels survive the first glBindImageTexture - iris-derivative SSIM 0.996196552 -> 0.996376198

This commit is contained in:
2026-09-29 03:06:45 -04:00
parent fff9d639f0
commit 568090f09f
6 changed files with 929 additions and 0 deletions
@@ -9014,6 +9014,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
#include "WireTextureReadback.inc"
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
@@ -23,6 +23,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Content uploads use scratch bindings, so draws and dispatches call this after
// texture synchronization.
void BindCurrentTextures();
// Driver-side color/depth level readback. Returns tightly packed, owned
// bytes in the requested pair; never consults a frontend texture or PACK/PBO.
// The caller supplies identity and extent, and whether the SOURCE allocation
// already uses an image carrier.
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
+121
View File
@@ -2786,13 +2786,119 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_backendTextureId;
}
// Replaces one level's shadow bytes with what the driver texture holds for it, then copies
// back the client writes the driver has not been sent yet (the level's pending rects, the
// same boxes a plain sync would upload over the driver's copy). Returns false, leaving the
// shadow untouched, when the driver's copy cannot stand in for the shadow: the client owns
// every byte of the level, the level is stored compressed, the driver has no image at this
// level and extent (defined after the last sync), or the readback cannot be served (a
// multisample image, a format the driver will not attach to a framebuffer).
static Bool AdoptDriverLevelIntoShadow(GLuint backendTextureId, BackendTextureObject& backendTexture,
const MG_State::GLState::ITextureObject& stateTextureObject,
MG_State::GLState::TextureObjectMipmap& mipmapObject,
TextureUploadTarget uploadTarget, Uint level) {
const IntVec3 texelSize = mipmapObject.GetMipmapTexelSize(uploadTarget, level);
const IntVec3 extent{texelSize.x(), texelSize.y(), std::max(texelSize.z(), 1)};
const Bool dirty = mipmapObject.IsStorageDirty(uploadTarget, level);
const MG_State::GLState::MipmapDirtyRegion dirtyRegion =
dirty ? mipmapObject.GetStorageDirtyRegion(uploadTarget, level)
: MG_State::GLState::MipmapDirtyRegion{};
if (dirty && dirtyRegion.CoversWholeLevel(extent)) return false;
if (mipmapObject.GetMipmapCompressedFormat(uploadTarget, level) != GL_NONE) return false;
if (!g_GLESFuncs.glGetTexLevelParameteriv) return false;
const TextureTarget target = stateTextureObject.GetTarget();
const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(target);
const GLenum faceTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
const IntVec3 backendExtent = GetBackendUploadSize(target, texelSize);
GLint driverWidth = 0, driverHeight = 0, driverDepth = 0;
backendTexture.Bind(nativeTarget);
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_WIDTH, &driverWidth);
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_HEIGHT,
&driverHeight);
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_DEPTH, &driverDepth);
if (driverWidth != backendExtent.x() || driverHeight != backendExtent.y() ||
std::max(driverDepth, 1) != std::max(backendExtent.z(), 1)) {
return false;
}
const TextureInternalFormat format = stateTextureObject.GetFormat();
GLenum shadowFormat = GL_NONE, shadowType = GL_NONE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(MG_Util::ConvertTextureInternalFormatToGLEnum(format),
{}, nullptr, &shadowFormat, &shadowType);
Vector<Uint8> snapshot;
if (!ReadTextureLevelTight(backendTextureId, target, uploadTarget, format, static_cast<GLint>(level),
texelSize, /*sourceUsesImageCarrier=*/false, shadowFormat, shadowType,
snapshot)) {
return false;
}
const SizeT shadowBytes = mipmapObject.GetMipmapByteSize(uploadTarget, level);
const SizeT texelCount =
static_cast<SizeT>(extent.x()) * static_cast<SizeT>(extent.y()) * static_cast<SizeT>(extent.z());
auto* shadow = static_cast<Uint8*>(mipmapObject.MapMipmapData(uploadTarget, level));
if (shadow == nullptr || snapshot.size() != shadowBytes || texelCount == 0 || shadowBytes % texelCount) {
return false;
}
if (dirty) {
// The rects index the SHADOW's texel grid (a 1D array's layers are its rows), which
// is also the snapshot's byte order.
const SizeT bpp = shadowBytes / texelCount;
const SizeT rowBytes = static_cast<SizeT>(extent.x()) * bpp;
const SizeT sliceBytes = static_cast<SizeT>(extent.y()) * rowBytes;
constexpr SizeT kMaxPending = MG_State::GLState::MipmapStorage::kMaxDirtyRects;
MG_State::GLState::MipmapDirtyRegion pending[kMaxPending];
SizeT pendingCount = mipmapObject.GetStorageDirtyRects(uploadTarget, level, pending, kMaxPending);
if (pendingCount == 0) {
pending[0] = dirtyRegion;
pendingCount = 1;
}
for (SizeT i = 0; i < pendingCount; ++i) {
const auto& box = pending[i];
if (box.Empty()) continue;
const SizeT spanBytes = static_cast<SizeT>(box.hi.x() - box.lo.x()) * bpp;
for (Int z = box.lo.z(); z < box.hi.z(); ++z) {
for (Int y = box.lo.y(); y < box.hi.y(); ++y) {
const SizeT offset = static_cast<SizeT>(z) * sliceBytes + static_cast<SizeT>(y) * rowBytes +
static_cast<SizeT>(box.lo.x()) * bpp;
Memcpy(snapshot.data() + offset, shadow + offset, spanBytes);
}
}
}
}
Memcpy(shadow, snapshot.data(), shadowBytes);
return true;
}
void BackendTextureObject::RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (m_imageBindableStorageRequired) {
return;
}
// Read before the two flags below schedule the re-mint: whether the driver texture
// already holds this object's levels, and so may hold texels the shadow never saw.
const Bool hadBackendStorage = m_isInitialized && m_backendTextureId != 0 &&
m_contextGeneration == g_backendContextGeneration;
m_imageBindableStorageRequired = true;
// ES only needs the storage to be immutable, so an immutable allocation that needs no
// channel widening is already the one a re-mint would produce: keep it, and with it
// every texel the GPU wrote. Pending uploads and real redefinitions still go through
// the normal shape and dirty checks of the next sync.
if (hadBackendStorage && m_backendStorageImmutable && !stateTextureObject->IsTextureView() &&
!IsMultisampleTextureTarget(stateTextureObject->GetTarget()) &&
!GetImageBindableStorageWidening(stateTextureObject->GetFormat())) {
m_forceTextureParamsResync = true;
return;
}
m_isInitialized = false;
// A view owns no levels. Its sync hands the requirement to the storage texture
// (SyncTextureViewToBackend), whose own transition below re-mints and replays them; a
// re-dirty issued here, through the view, would reach that texture first and claim
// every level for the shadow before the driver's copy could be read back.
if (stateTextureObject->IsTextureView()) {
m_forceTextureParamsResync = true;
return;
}
// Every level this object has ALREADY uploaded has to be replayed, because the
// regeneration this transition schedules re-mints the storage in the image carrier and
// only uploads levels the shadow still calls dirty - which, for a texture that was
@@ -2803,6 +2909,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the image loads are wrong. Reached whenever anything syncs the texture first - a
// glGetTexImage, a draw that samples it, an FBO attach - which is why it survived so
// long: the scenario that binds the image immediately after uploading never sees it.
//
// And the shadow is only the CLIENT's copy. Whatever the GPU wrote into a level - a
// render or clear into it, a copy, a glGenerateMipmap - lives in the driver texture
// alone, so replaying the shadow as it stands put the pre-render bytes (or the zeroes
// of a null-data definition) into the new storage. The driver's levels are therefore
// read back into the shadow first, with the client writes still pending merged over
// them, while the old storage still exists; a level the driver cannot hand back keeps
// the shadow, which is all this used to replay.
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
const auto levelCount = mipmapObject->GetMipmapLevelCount();
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
@@ -2810,6 +2924,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0) continue;
if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue;
if (hadBackendStorage &&
!AdoptDriverLevelIntoShadow(m_backendTextureId, *this, *stateTextureObject,
*mipmapObject, uploadTarget, level)) {
MGLOG_D("Texture %u level %u: re-minting from the shadow, the driver's copy is not "
"readable here",
stateTextureObject->GetExternalIndex(), level);
}
mipmapObject->MarkStorageDirty(uploadTarget, level, true);
}
}
@@ -0,0 +1,282 @@
// Included in DirectGLES.cpp after the native packed-word readback helper.
// A storage promotion needs the driver's own texels as owned tight rows, not
// client PACK layout or a fallback into a frontend texture's potentially stale
// shadow.
class ScopedWireTextureReadLevel {
public:
ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level)
: m_texture(texture), m_target(target) {
switch (target) {
case GL_TEXTURE_2D: m_binding = GL_TEXTURE_BINDING_2D; break;
case GL_TEXTURE_3D: m_binding = GL_TEXTURE_BINDING_3D; break;
case GL_TEXTURE_2D_ARRAY: m_binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
case GL_TEXTURE_CUBE_MAP: m_binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
case GL_TEXTURE_CUBE_MAP_ARRAY: m_binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
default: return;
}
GLint previous = 0;
DrainESErrors();
g_GLESFuncs.glGetIntegerv(m_binding, &previous);
g_GLESFuncs.glBindTexture(m_target, m_texture);
g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_BASE_LEVEL, &m_baseLevel);
g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_MAX_LEVEL, &m_maxLevel);
m_saved = g_GLESFuncs.glGetError() == GL_NO_ERROR;
if (m_saved) {
// ES framebuffer completeness can reject a defined level whose
// base level or intervening mip chain is missing/inconsistent.
// Read this existing native image as the sole base level. The
// logical GL parameters and every cached copy remain unchanged.
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, level);
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, level);
m_valid = g_GLESFuncs.glGetError() == GL_NO_ERROR;
}
g_GLESFuncs.glBindTexture(m_target, static_cast<GLuint>(previous));
}
~ScopedWireTextureReadLevel() {
if (!m_saved) return;
// The packed-word path can use its own texture scratch binding.
// Save the binding at restoration time too; do not assume an active
// texture unit or make the backend's binding cache forget a bind.
GLint previous = 0;
g_GLESFuncs.glGetIntegerv(m_binding, &previous);
g_GLESFuncs.glBindTexture(m_target, m_texture);
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, m_baseLevel);
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, m_maxLevel);
g_GLESFuncs.glBindTexture(m_target, static_cast<GLuint>(previous));
}
Bool Valid() const { return m_valid; }
private:
GLuint m_texture;
GLenum m_target;
GLenum m_binding = GL_NONE;
GLint m_baseLevel = 0, m_maxLevel = 0;
Bool m_saved = false, m_valid = false;
};
static Bool ReadWireColorRowsTight(GLsizei width, GLsizei height, GLenum format, GLenum type,
Bool forceOpaqueAlpha, const NormalizedImageCarrierRead& carrier,
Vector<Uint8>& bytes) {
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) return false;
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type);
if (!pixelBytes || width <= 0 || height <= 0) return false;
const GLenum componentType = QueryReadAttachmentComponentType();
const Bool integerAttachment = componentType == GL_INT || componentType == GL_UNSIGNED_INT;
if (carrier.Active() ? (mapping.isInteger || !integerAttachment)
: mapping.isInteger != integerAttachment) return false;
GLint implFormat = 0, implType = 0;
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat);
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType);
struct Candidate { GLenum format, type; };
Vector<Candidate> candidates;
if (carrier.Active()) {
candidates.push_back({GL_RGBA_INTEGER, GL_UNSIGNED_INT});
} else if (mapping.isInteger) {
if (GetWideReadChannelCount(implFormat) > 0 && IsIntegerReadFormat(implFormat) &&
(implType == GL_INT || implType == GL_UNSIGNED_INT))
candidates.push_back({static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)});
candidates.push_back({GL_RGBA_INTEGER, componentType == GL_INT ? GLenum(GL_INT) : GLenum(GL_UNSIGNED_INT)});
} else {
if (GetWideReadChannelCount(implFormat) > 0 && !IsIntegerReadFormat(implFormat) &&
(CanDecodeWideSourceType(implType) ||
(implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV)))
candidates.push_back({static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)});
if (componentType == GL_FLOAT) candidates.push_back({GL_RGBA, GL_FLOAT});
if (componentType == GL_SIGNED_NORMALIZED) {
candidates.push_back({GL_RGBA, GL_SHORT});
candidates.push_back({GL_RGBA, GL_BYTE});
} else candidates.push_back({GL_RGBA, GL_UNSIGNED_BYTE});
}
ScopedPixelPackBuffer packBuffer(0);
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
const SizeT pixels = static_cast<SizeT>(width) * static_cast<SizeT>(height);
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
Int channels = 0;
DrainESErrors();
for (const auto candidate : candidates) {
channels = GetWideReadChannelCount(candidate.format);
wide.resize(pixels * static_cast<SizeT>(channels) * GetReadbackComponentSize(candidate.type));
g_GLESFuncs.glReadPixels(0, 0, width, height, candidate.format, candidate.type, wide.data());
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { wideType = candidate.type; break; }
}
if (wideType == GL_NONE) return false;
if (carrier.Active()) {
Vector<Uint8> decoded(pixels * 4 * sizeof(Float));
auto* dst = reinterpret_cast<Float*>(decoded.data());
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
for (SizeT i = 0; i < pixels; ++i) {
for (SizeT c = 0; c < 4; ++c) {
const Float divisor = static_cast<Float>(carrier.ChannelMax[c]);
dst[i * 4 + c] = carrier.SignedNormalized
? std::max(static_cast<Float>(static_cast<Int16>(static_cast<Uint16>(src[i * 4 + c]))) / divisor, -1.0f)
: static_cast<Float>(src[i * 4 + c]) / divisor;
}
}
wide = Move(decoded); wideType = GL_FLOAT; channels = 4;
} else if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
Vector<Uint8> decoded(pixels * 4 * sizeof(Float));
auto* dst = reinterpret_cast<Float*>(decoded.data());
for (SizeT i = 0; i < pixels; ++i) {
Uint32 word = 0;
Memcpy(&word, wide.data() + i * sizeof(word), sizeof(word));
dst[i * 4] = static_cast<Float>(word & 1023u) / 1023.0f;
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 1023u) / 1023.0f;
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 1023u) / 1023.0f;
dst[i * 4 + 3] = static_cast<Float>((word >> 30) & 3u) / 3.0f;
}
wide = Move(decoded); wideType = GL_FLOAT; channels = 4;
}
if (channels < 4) ExpandNarrowWideRead(wide, pixels, channels, wideType);
if (forceOpaqueAlpha) ForceWideReadAlphaToOne(wide, pixels, wideType);
Vector<Uint8> converted(pixels * pixelBytes);
const SizeT sourceRowBytes = static_cast<SizeT>(width) * 4 * GetReadbackComponentSize(wideType);
for (GLsizei row = 0; row < height; ++row)
ReadbackImpl::ConvertWideReadbackRow(wide.data() + static_cast<SizeT>(row) * sourceRowBytes,
converted.data() + static_cast<SizeT>(row) * width * pixelBytes, width, wideType, mapping, type);
bytes = Move(converted);
return true;
}
static Bool ReadWireDepthStencilLevelTight(GLuint texture, TextureTarget target,
TextureUploadTarget uploadTarget, GLint level,
const IntVec3& extent, GLenum format, GLenum type,
Vector<Uint8>& bytes) {
const SizeT pixelBytes = MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(format), MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (!pixelBytes) return false;
const Bool needDepth = format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL;
const Bool needStencil = format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
ScopedFramebufferBinding bindings(true, false);
GLuint fbo = 0;
g_GLESFuncs.glGenFramebuffers(1, &fbo);
struct DeleteFbo { GLuint id; ~DeleteFbo() { g_GLESFuncs.glDeleteFramebuffers(1, &id); } } cleanup{fbo};
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, fbo);
ScopedPixelPackBuffer packBuffer(0);
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
const GLenum point = needDepth ? (needStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT)
: GL_STENCIL_ATTACHMENT;
const SizeT count = static_cast<SizeT>(extent.x()) * extent.y();
Vector<Uint8> result(count * extent.z() * pixelBytes);
if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS);
g_GLESFuncs.glFinish();
for (GLint z = 0; z < extent.z(); ++z) {
if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY)
g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, point, texture, level, z);
else
g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, point,
TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget), texture, level);
g_GLESFuncs.glReadBuffer(GL_NONE);
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false;
Vector<Float> depth;
Vector<Uint8> stencil;
if (needDepth && !ReadDepthValuesNative(0, 0, extent.x(), extent.y(), depth)) return false;
if (needStencil && !ReadStencilBytesNative(0, 0, extent.x(), extent.y(), stencil)) return false;
auto* dst = result.data() + count * z * pixelBytes;
for (SizeT i = 0; i < count; ++i) {
if (format == GL_DEPTH_STENCIL) {
const Uint32 s = stencil[i];
if (type == GL_UNSIGNED_INT_24_8) {
const Uint32 packed = (NormalizedDepthToUnsigned(depth[i], 16777215.0) << 8) | s;
Memcpy(dst + i * pixelBytes, &packed, sizeof(packed));
} else if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) {
Memcpy(dst + i * pixelBytes, &depth[i], sizeof(Float));
Memcpy(dst + i * pixelBytes + sizeof(Float), &s, sizeof(s));
} else return false;
} else if (type == GL_FLOAT) {
const Float value = needDepth ? depth[i] : static_cast<Float>(stencil[i]);
Memcpy(dst + i * pixelBytes, &value, sizeof(value));
} else if (type == GL_HALF_FLOAT) {
const Uint16 value = MG_Util::EncodeFloatToHalfBits(
needDepth ? depth[i] : static_cast<Float>(stencil[i]));
Memcpy(dst + i * pixelBytes, &value, sizeof(value));
} else {
const Bool signedType = type == GL_BYTE || type == GL_SHORT || type == GL_INT;
const Double maximum = signedType
? (pixelBytes == 1 ? 127.0 : pixelBytes == 2 ? 32767.0 : 2147483647.0)
: (pixelBytes == 1 ? 255.0 : pixelBytes == 2 ? 65535.0 : 4294967295.0);
const Uint32 value = needDepth ? NormalizedDepthToUnsigned(depth[i], maximum) : stencil[i];
if (pixelBytes == 1) dst[i] = static_cast<Uint8>(value);
else if (pixelBytes == 2) { const Uint16 v = static_cast<Uint16>(value); Memcpy(dst + i * 2, &v, 2); }
else if (pixelBytes == 4) Memcpy(dst + i * 4, &value, 4);
else return false;
}
}
}
bytes = Move(result);
return true;
}
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes) {
if (!texture || level < 0 || TextureImpl::IsMultisampleTextureTarget(target)) return false;
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
ScopedWireTextureReadLevel readLevel(texture, nativeTarget, level);
if (!readLevel.Valid()) return false;
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX)
return ReadWireDepthStencilLevelTight(texture, target, uploadTarget, level, extent, format, type, bytes);
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) return false;
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type);
if (!pixelBytes || extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false;
const GLenum faceTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
const auto widening = sourceUsesImageCarrier
? TextureImpl::GetImageBindableStorageWidening(logicalFormat) : TextureImpl::ImageBindableStorageWidening{};
const SizeT sliceBytes = static_cast<SizeT>(extent.x()) * extent.y() * pixelBytes;
Vector<Uint8> result(sliceBytes * static_cast<SizeT>(extent.z()));
if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS);
g_GLESFuncs.glFinish();
// A raw packed word may have redundant encodings or NaN payloads. Read
// it through R32UI when the source really still uses that native layout.
const Bool rawPacked = !widening && MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (rawPacked && pixelBytes == sizeof(Uint32)) {
const GLint face = uploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
uploadTarget <= TextureUploadTarget::CubeMapNegativeZ
? static_cast<GLint>(uploadTarget) - static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX) : 0;
for (GLint layer = 0; layer < extent.z(); ++layer)
if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(),
reinterpret_cast<Uint32*>(result.data() + static_cast<SizeT>(layer) * sliceBytes))) return false;
bytes = Move(result);
return true;
}
NormalizedImageCarrierRead carrier{};
if (widening && widening.CarriesNormalizedCodes()) {
for (SizeT c = 0; c < 4; ++c) carrier.ChannelMax[c] = widening.ChannelMax[c];
carrier.SignedNormalized = widening.SignedNormalized;
}
TempFBOBinder binder(true);
auto& framebuffer = binder.Framebuffer();
for (GLint layer = 0; layer < extent.z(); ++layer) {
if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY)
ScratchFBOImpl::EnsureColorAttachmentLayer(framebuffer, GL_READ_FRAMEBUFFER, texture, level, layer);
else
ScratchFBOImpl::EnsureColorAttachment2D(framebuffer, GL_READ_FRAMEBUFFER, texture, faceTarget, level);
ScratchFBOImpl::EnsureReadBuffer(framebuffer, GL_COLOR_ATTACHMENT0);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E_ONCE("Espryt wire texture readback: framebuffer status=0x%x texture=%u level=%d target=0x%x",
status, texture, level, nativeTarget);
return false;
}
Vector<Uint8> slice;
if (!ReadWireColorRowsTight(extent.x(), extent.y(), format, type,
TextureImpl::BackendTextureFormatAddsAlpha(logicalFormat, target), carrier, slice) ||
slice.size() != sliceBytes) return false;
Memcpy(result.data() + static_cast<SizeT>(layer) * sliceBytes, slice.data(), sliceBytes);
}
bytes = Move(result);
return true;
}
@@ -88,6 +88,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/NonCoreImageFormatScenario.cpp
Scenarios/ImageSizeAfterRespecScenario.cpp
Scenarios/ImageBindableRemintScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/IoBlockNameCollisionScenario.cpp
@@ -0,0 +1,516 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageBindableRemintScenario.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
//
// Scenario - THE FIRST glBindImageTexture OF A TEXTURE THE GPU HAS ALREADY WRITTEN.
//
// ES only allows immutable storage on an image unit, and a few formats need a wider carrier on
// top (TextureImpl::GetImageBindableStorageWidening), so Espryt re-mints a texture the first time
// it is image-bound after it already has driver storage (RequireImageBindableStorage). The new
// storage has to hold what the old one held. The re-mint used to fill it from the frontend's
// shadow - the CLIENT's copy of each level - and the shadow never sees a GPU write: a render or
// clear into the level, a glGenerateMipmap, a null-data definition only the GPU ever filled. Every
// such level came back as the bytes the application last uploaded, or zeroes. That is the
// ~26.9k-pixel iris-derivative difference: an RGBA16F render target, first image-bound after it
// was rendered into.
//
// Every case gives a texture its content by one means, lets the backend create its storage (a
// sampling draw, or the FBO attach of a GPU clear), writes it on the GPU where the case says so,
// and then binds it to an image unit for the first time: a compute imageLoad copies the level into
// a destination that was image-bound before it was ever synced (so the destination itself never
// re-mints), and the destination is read back through an FBO. The expectation is the GL answer.
//
// The controls are the other half: levels the GPU never wrote (including formats whose driver
// readback may be refused), levels the driver does not have yet because they were defined after
// the last sync, and a pending client write that has to land on top of GPU content. Magma never
// re-mints from a shadow (it copies image to image) and is the control backend.
#include <cstdint>
#include <cstdlib>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kEdge = 8;
constexpr const char* kVS = R"(#version 430 core
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
)";
constexpr const char* kSampleFS = R"(#version 430 core
uniform sampler2D u_tex;
out vec4 o_color;
void main() { o_color = texelFetch(u_tex, ivec2(gl_FragCoord.xy) % 4, 0); }
)";
// Copies an image into an RGBA8 image with imageLoad; a signed-normalized source is mapped
// to [0, 1] so it survives the unsigned destination.
std::string CopyCS(const char* srcFormat, bool snorm) {
std::string s = "#version 430 core\nlayout(local_size_x = 8, local_size_y = 8) in;\n";
s += std::string("layout(") + srcFormat + ", binding = 0) readonly uniform image2D u_src;\n";
s += "layout(rgba8, binding = 1) writeonly uniform image2D u_dst;\n";
s += "void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); vec4 v = imageLoad(u_src, p);\n";
if (snorm) s += " v = v * 0.5 + 0.5;\n";
s += " imageStore(u_dst, p, v); }\n";
return s;
}
std::vector<GLubyte> Pattern(int edge, GLubyte magic) {
std::vector<GLubyte> t(static_cast<std::size_t>(edge * edge) * 4u);
for (int y = 0; y < edge; ++y)
for (int x = 0; x < edge; ++x) {
const std::size_t at = static_cast<std::size_t>((y * edge + x) * 4);
t[at + 0] = magic;
t[at + 1] = static_cast<GLubyte>(16 * (x + 1));
t[at + 2] = static_cast<GLubyte>(16 * (y + 1));
t[at + 3] = 0xFF;
}
return t;
}
std::vector<GLubyte> Solid(int edge, GLubyte r, GLubyte g, GLubyte b, GLubyte a) {
std::vector<GLubyte> t(static_cast<std::size_t>(edge * edge) * 4u);
for (std::size_t i = 0; i < t.size(); i += 4) {
t[i] = r;
t[i + 1] = g;
t[i + 2] = b;
t[i + 3] = a;
}
return t;
}
const GLfloat kGreen[4] = {0.0f, 1.0f, 0.0f, 1.0f};
class ImageBindableRemintScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
std::string error;
m_sampleProgram = CompileProgram(kVS, kSampleFS, &error);
ASSERT_NE(m_sampleProgram, 0u) << error;
m_scratch = MakeColorFbo(4, 4);
glGenVertexArrays(1, &m_vao);
FirstGLError();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
if (m_sampleProgram) glDeleteProgram(m_sampleProgram);
if (m_vao) glDeleteVertexArrays(1, &m_vao);
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
if (m_fbo) glDeleteFramebuffers(1, &m_fbo);
BindDefaultFramebuffer();
DestroyColorFbo(m_scratch);
glViewport(0, 0, Gl().Width(), Gl().Height());
FirstGLError();
}
GLuint NewTexture() {
GLuint t = 0;
glGenTextures(1, &t);
m_textures.push_back(t);
return t;
}
static void Nearest(GLenum target, GLuint tex, int maxLevel) {
glBindTexture(target, tex);
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, maxLevel);
glBindTexture(target, 0);
}
// A draw that samples the texture: the backend now holds storage for it.
void SyncBySampling(GLuint tex) {
BindFbo(m_scratch);
glUseProgram(m_sampleProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex);
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_tex"), 0);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
BindDefaultFramebuffer();
glFinish();
}
// A GPU write the frontend's shadow never sees. A layer >= 0 names one layer of an
// array texture.
void GpuClear(GLuint tex, int level, const GLfloat* color, int layer = -1) {
if (!m_fbo) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
if (layer >= 0) {
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, level, layer);
} else {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, level);
}
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE))
<< "the clear target is incomplete";
glClearBufferfv(GL_COLOR, 0, color);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
}
// The first image binding of (src, level[, layer]): an imageLoad copy into a fresh RGBA8
// destination, read back through an FBO.
std::vector<GLubyte> ImageCopy(GLuint src, int level, GLenum unitFormat, const char* glslFormat,
bool snorm = false, int layer = 0) {
const GLuint dst = NewTexture();
glBindTexture(GL_TEXTURE_2D, dst);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glBindTexture(GL_TEXTURE_2D, 0);
glBindImageTexture(1, dst, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
glBindImageTexture(0, src, level, GL_FALSE, layer, GL_READ_ONLY, unitFormat);
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture";
const std::string cs = CopyCS(glslFormat, snorm);
const char* csp = cs.c_str();
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &csp, nullptr);
glCompileShader(shader);
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
EXPECT_EQ(linked, GL_TRUE) << "the copy program did not link";
glUseProgram(program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glUseProgram(0);
glDeleteProgram(program);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
if (!m_fbo) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst, 0);
std::vector<GLubyte> out(static_cast<std::size_t>(kEdge * kEdge) * 4u, 0xA5);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, out.data());
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
EXPECT_EQ(FirstGLError(), 0u) << "copy/readback";
return out;
}
// The edge x edge corner of `got` against an RGBA8 expectation, the first `channels`
// channels only, within 2 codes (a normalized value's round trip).
static void ExpectImage(const std::vector<GLubyte>& got, int edge, const std::vector<GLubyte>& want,
int channels = 4) {
int bad = 0;
std::ostringstream first;
for (int y = 0; y < edge; ++y)
for (int x = 0; x < edge; ++x) {
const std::size_t g = static_cast<std::size_t>((y * kEdge + x) * 4);
const std::size_t w = static_cast<std::size_t>((y * edge + x) * 4);
bool texelBad = false;
for (int c = 0; c < channels; ++c)
texelBad |= std::abs(int(got[g + c]) - int(want[w + c])) > 2;
if (texelBad && bad++ == 0) {
first << "(" << x << "," << y << ") got " << int(got[g]) << "," << int(got[g + 1]) << ","
<< int(got[g + 2]) << "," << int(got[g + 3]) << " want " << int(want[w]) << ","
<< int(want[w + 1]) << "," << int(want[w + 2]) << "," << int(want[w + 3]);
}
}
EXPECT_EQ(bad, 0) << bad << " of " << edge * edge << " texels differ; first " << first.str();
}
GLuint m_sampleProgram = 0, m_vao = 0, m_fbo = 0;
ColorFbo m_scratch{};
std::vector<GLuint> m_textures;
};
// Control: the shadow and the driver agree.
TEST_F(ImageBindableRemintScenario, UploadedLevelKeepsItsUpload) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, p);
}
TEST_F(ImageBindableRemintScenario, GpuClearOverAnUploadSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
}
// Immutable in a core image format: the storage is already what an image unit needs.
TEST_F(ImageBindableRemintScenario, GpuClearOverAnImmutableUploadSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
}
// A null-data definition only the GPU ever filled: the shadow holds zeroes.
TEST_F(ImageBindableRemintScenario, GpuClearOfANullDefinitionSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
}
// A client write still pending when the bind comes lands on top of the GPU's content,
// and only inside its own box.
TEST_F(ImageBindableRemintScenario, PendingSubImageLandsOnTheGpuClear) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
const auto blue = Solid(2, 0, 0, 255, 255);
glBindTexture(GL_TEXTURE_2D, t);
glTexSubImage2D(GL_TEXTURE_2D, 0, 2, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
auto want = Solid(kEdge, 0, 255, 0, 255);
for (int y = 2; y < 4; ++y)
for (int x = 2; x < 4; ++x) {
const std::size_t at = static_cast<std::size_t>((y * kEdge + x) * 4);
want[at] = 0;
want[at + 1] = 0;
want[at + 2] = 255;
want[at + 3] = 255;
}
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, want);
}
TEST_F(ImageBindableRemintScenario, GeneratedMipLevelSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto s = Solid(kEdge, 200, 100, 50, 255);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, s.data());
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), kEdge / 2, Solid(kEdge / 2, 200, 100, 50, 255));
}
TEST_F(ImageBindableRemintScenario, GpuClearOfAnUploadedMipLevelSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto s0 = Solid(kEdge, 200, 100, 50, 255);
const auto s1 = Solid(kEdge / 2, 10, 20, 30, 255);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, s0.data());
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, s1.data());
Nearest(GL_TEXTURE_2D, t, 1);
SyncBySampling(t);
GpuClear(t, 1, kGreen);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), kEdge / 2, Solid(kEdge / 2, 0, 255, 0, 255));
}
// Control: a level defined (null data) after the last sync is not on the driver yet, so
// there is nothing to read back for it; the bound level 0 must still be the upload.
TEST_F(ImageBindableRemintScenario, LevelDefinedAfterTheSyncDoesNotDisturbTheOthers) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, p);
}
// Control: the same late level, partly uploaded - the shadow is the only copy of it.
TEST_F(ImageBindableRemintScenario, LevelDefinedAndUploadedAfterTheSyncKeepsTheUpload) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const auto blue = Solid(2, 0, 0, 255, 255);
glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
glBindTexture(GL_TEXTURE_2D, 0);
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), 2, blue);
}
// GL_RG8 is not an ESSL image format, so the re-mint also WIDENS the storage (to RGBA8)
// and cannot keep the old allocation even though it is immutable.
TEST_F(ImageBindableRemintScenario, GpuClearSurvivesAWideningRemint) {
if (!Ready()) return;
const GLuint t = NewTexture();
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u, 0x33);
glBindTexture(GL_TEXTURE_2D, t);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
const GLfloat c[4] = {0.5f, 0.25f, 0.0f, 1.0f};
GpuClear(t, 0, c);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RG8, "rg8"), kEdge, Solid(kEdge, 128, 64, 0, 255), 2);
}
// Control: widened, uploaded only.
TEST_F(ImageBindableRemintScenario, UploadSurvivesAWideningRemint) {
if (!Ready()) return;
const GLuint t = NewTexture();
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u);
for (std::size_t i = 0; i < rg.size(); i += 2) {
rg[i] = 0x33;
rg[i + 1] = 0x99;
}
glBindTexture(GL_TEXTURE_2D, t);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RG8, "rg8"), kEdge, Solid(kEdge, 0x33, 0x99, 0, 255), 2);
}
// Control: a signed-normalized level (not colour-renderable on every ES driver, so its
// readback may be refused and the shadow used instead), uploaded only.
TEST_F(ImageBindableRemintScenario, SnormUploadSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
std::vector<GLbyte> s(static_cast<std::size_t>(kEdge * kEdge) * 4u);
for (std::size_t i = 0; i < s.size(); i += 4) {
s[i] = 127;
s[i + 1] = 0;
s[i + 2] = -127;
s[i + 3] = 127;
}
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8_SNORM, kEdge, kEdge, 0, GL_RGBA, GL_BYTE, s.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8_SNORM, "rgba8_snorm", true), kEdge, Solid(kEdge, 255, 128, 0, 255),
3);
}
// Control: RGBA16_SNORM is desktop-only as an image format, so this re-mint widens too.
TEST_F(ImageBindableRemintScenario, Snorm16UploadSurvivesAWideningRemint) {
if (!Ready()) return;
const GLuint t = NewTexture();
std::vector<GLshort> s(static_cast<std::size_t>(kEdge * kEdge) * 4u);
for (std::size_t i = 0; i < s.size(); i += 4) {
s[i] = 32767;
s[i + 1] = 0;
s[i + 2] = -32767;
s[i + 3] = 32767;
}
glBindTexture(GL_TEXTURE_2D, t);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA16_SNORM, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_SHORT, s.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA16_SNORM, "rgba16_snorm", true), kEdge,
Solid(kEdge, 255, 128, 0, 255), 3);
}
// An array texture is read back one layer at a time: the GPU-cleared layer and the
// uploaded one both have to come through. The clear's FBO attach is what gives the
// texture its driver storage here.
TEST_F(ImageBindableRemintScenario, GpuClearOfOneArrayLayerSurvivesBesideTheUploadedLayer) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
std::vector<GLubyte> layers(p);
layers.insert(layers.end(), p.begin(), p.end());
glBindTexture(GL_TEXTURE_2D_ARRAY, t);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kEdge, kEdge, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
layers.data());
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
Nearest(GL_TEXTURE_2D_ARRAY, t, 0);
GpuClear(t, 0, kGreen, /*layer=*/1);
glFinish();
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8", false, /*layer=*/1), kEdge, Solid(kEdge, 0, 255, 0, 255));
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8", false, /*layer=*/0), kEdge, p);
}
// A view owns no storage, so its first image binding is the VIEWED texture's transition,
// and the GPU's texels there have to come through it. Needs glTextureView, which Espryt
// only has where the ES driver does (not on llvmpipe).
TEST_F(ImageBindableRemintScenario, GpuClearSurvivesTheFirstImageBindingOfAView) {
if (!Ready()) return;
const GLuint owner = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, owner);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, owner, 0);
SyncBySampling(owner);
GpuClear(owner, 0, kGreen);
SyncBySampling(owner);
const GLuint view = NewTexture();
FirstGLError();
glTextureView(view, GL_TEXTURE_2D, owner, GL_RGBA8, 0, 1, 0, 1);
if (const GLenum error = FirstGLError()) {
GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName() << " ("
<< GLErrorName(error) << ")";
}
Nearest(GL_TEXTURE_2D, view, 0);
SyncBySampling(view);
ExpectImage(ImageCopy(view, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
}
} // namespace
} // namespace MGITest