diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 726cbf8d..dbc42c72 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 94947965..1fee4ce3 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -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& 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); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index e0012f3c..bd1f76f2 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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(level), GL_TEXTURE_WIDTH, &driverWidth); + g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast(level), GL_TEXTURE_HEIGHT, + &driverHeight); + g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast(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 snapshot; + if (!ReadTextureLevelTight(backendTextureId, target, uploadTarget, format, static_cast(level), + texelSize, /*sourceUsesImageCarrier=*/false, shadowFormat, shadowType, + snapshot)) { + return false; + } + const SizeT shadowBytes = mipmapObject.GetMipmapByteSize(uploadTarget, level); + const SizeT texelCount = + static_cast(extent.x()) * static_cast(extent.y()) * static_cast(extent.z()); + auto* shadow = static_cast(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(extent.x()) * bpp; + const SizeT sliceBytes = static_cast(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(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(z) * sliceBytes + static_cast(y) * rowBytes + + static_cast(box.lo.x()) * bpp; + Memcpy(snapshot.data() + offset, shadow + offset, spanBytes); + } + } + } + } + Memcpy(shadow, snapshot.data(), shadowBytes); + return true; + } + void BackendTextureObject::RequireImageBindableStorage( const SharedPtr& 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); } } diff --git a/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc new file mode 100644 index 00000000..5f7b0b6f --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc @@ -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(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(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& 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 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(implFormat), static_cast(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(implFormat), static_cast(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(width) * static_cast(height); + Vector wide; + GLenum wideType = GL_NONE; + Int channels = 0; + DrainESErrors(); + for (const auto candidate : candidates) { + channels = GetWideReadChannelCount(candidate.format); + wide.resize(pixels * static_cast(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 decoded(pixels * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(decoded.data()); + const auto* src = reinterpret_cast(wide.data()); + for (SizeT i = 0; i < pixels; ++i) { + for (SizeT c = 0; c < 4; ++c) { + const Float divisor = static_cast(carrier.ChannelMax[c]); + dst[i * 4 + c] = carrier.SignedNormalized + ? std::max(static_cast(static_cast(static_cast(src[i * 4 + c]))) / divisor, -1.0f) + : static_cast(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 decoded(pixels * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(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(word & 1023u) / 1023.0f; + dst[i * 4 + 1] = static_cast((word >> 10) & 1023u) / 1023.0f; + dst[i * 4 + 2] = static_cast((word >> 20) & 1023u) / 1023.0f; + dst[i * 4 + 3] = static_cast((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 converted(pixels * pixelBytes); + const SizeT sourceRowBytes = static_cast(width) * 4 * GetReadbackComponentSize(wideType); + for (GLsizei row = 0; row < height; ++row) + ReadbackImpl::ConvertWideReadbackRow(wide.data() + static_cast(row) * sourceRowBytes, + converted.data() + static_cast(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& 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(extent.x()) * extent.y(); + Vector 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 depth; + Vector 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(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(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(value); + else if (pixelBytes == 2) { const Uint16 v = static_cast(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& 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(extent.x()) * extent.y() * pixelBytes; + Vector result(sliceBytes * static_cast(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(uploadTarget) - static_cast(TextureUploadTarget::CubeMapPositiveX) : 0; + for (GLint layer = 0; layer < extent.z(); ++layer) + if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(), + reinterpret_cast(result.data() + static_cast(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 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(layer) * sliceBytes, slice.data(), sliceBytes); + } + bytes = Move(result); + return true; + } diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 7dad1f1d..3c4d3498 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ImageBindableRemintScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ImageBindableRemintScenario.cpp new file mode 100644 index 00000000..e06587fa --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/ImageBindableRemintScenario.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 +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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 Pattern(int edge, GLubyte magic) { + std::vector t(static_cast(edge * edge) * 4u); + for (int y = 0; y < edge; ++y) + for (int x = 0; x < edge; ++x) { + const std::size_t at = static_cast((y * edge + x) * 4); + t[at + 0] = magic; + t[at + 1] = static_cast(16 * (x + 1)); + t[at + 2] = static_cast(16 * (y + 1)); + t[at + 3] = 0xFF; + } + return t; + } + + std::vector Solid(int edge, GLubyte r, GLubyte g, GLubyte b, GLubyte a) { + std::vector t(static_cast(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(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 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 out(static_cast(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& got, int edge, const std::vector& 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((y * kEdge + x) * 4); + const std::size_t w = static_cast((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 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((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 rg(static_cast(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 rg(static_cast(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 s(static_cast(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 s(static_cast(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 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