mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user