Compare commits

...
12 changed files with 1538 additions and 152 deletions
+101 -15
View File
@@ -5563,6 +5563,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glMemoryBarrierByRegion(barriers);
}
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
//
// The frontend hands this backend the target the APPLICATION named, and three of the
// targets core GL has do not exist in ES at all. They are not missing here either - the
// texture managers already store a 1D texture as a height-1 2D one, a 1D array as a
// height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) -
// but glCopyImageSubData was the one path that never asked for that translation and passed
// 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen,
// and with the error only asserted on (asserts are compiled out of an INFO build) the
// destination silently keeps whatever it held.
//
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
// target.
struct GLESCopyImageEndpoint {
GLenum target = GL_TEXTURE_2D;
GLint x = 0;
GLint y = 0;
GLint z = 0;
};
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) {
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
GLESCopyImageEndpoint endpoint{};
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
if (stateTarget == TextureTarget::Texture1DArray) {
endpoint.x = x;
endpoint.y = 0;
endpoint.z = y;
return endpoint;
}
endpoint.x = x;
endpoint.y = y;
endpoint.z = z;
return endpoint;
}
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
// of the copy is one - GL forbids a copy whose two endpoints disagree about how many layers
// move, so at most one of the two can be a 1D array only in the degenerate single-layer
// case, where the swap is the identity anyway.
static void ApplyGLESCopyImageExtent(GLenum appSrcTarget, GLenum appDstTarget, GLsizei& height, GLsizei& depth) {
const TextureTarget srcStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appSrcTarget);
const TextureTarget dstStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appDstTarget);
if (srcStateTarget != TextureTarget::Texture1DArray && dstStateTarget != TextureTarget::Texture1DArray) {
return;
}
std::swap(height, depth);
}
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
@@ -5592,6 +5642,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ);
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ);
GLsizei copyHeight = srcHeight;
GLsizei copyDepth = srcDepth;
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat());
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat());
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat());
@@ -5599,12 +5655,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
"DirectGLES CopyImageSubData only supports depth-only image copies.");
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
dstBackendTexture->GetBackendTextureId(), dstLevel, dstX, dstY, srcWidth, srcHeight);
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
return;
}
@@ -5615,29 +5671,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
// with the always-live helper so a stale flag cannot misroute a
// succeeded native copy into the 2D-only fallback.
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth);
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth);
const GLenum copyImageError = g_GLESFuncs.glGetError();
if (copyImageError == GL_NO_ERROR) {
return;
}
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES color CopyImageSubData only supports single-layer copies.");
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY);
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
return;
}
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth);
AssertNoGLError("glCopyImageSubData");
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth);
// Every error condition glCopyImageSubData has was already ruled out by the frontend
// validator, so a driver error here is an internal invariant violation, not something
// the application can provoke. Say so where an INFO build can still see it, then trap
// in the builds that trap - the previous bare assert left a release build with a
// destination that silently kept its old contents.
const GLenum copyImageError = g_GLESFuncs.glGetError();
if (copyImageError != GL_NO_ERROR) {
MGLOG_E_ONCE("glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)",
MG_Util::ConvertGLEnumToString(copyImageError).c_str(),
MG_Util::ConvertGLEnumToString(src.target).c_str(),
MG_Util::ConvertGLEnumToString(srcTarget).c_str(),
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
}
}
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
@@ -7177,6 +7247,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
// glGetTexImage returns the stored texels, and for a packed internal format read with the
// matching client type the shadow word already IS the client word. Decoding it to float and
// re-encoding would canonicalize an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000: the
// same value, different bits), so those pairs copy the words straight through.
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
textureMipmapObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
if (!ReadbackImpl::StorePackedWordsToClient(static_cast<const Uint8*>(shadow), width, sliceHeight,
sliceCount, type, pixels, applyPackImageParams)) {
return false;
}
MGLOG_D("GetTexImage: copied %s/%s verbatim from the CPU shadow copy",
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
return true;
}
Vector<Uint8> wide;
Bool isInteger = false;
Bool isSigned = false;
+96 -66
View File
@@ -1596,88 +1596,71 @@ namespace MobileGL::MG_Backend::DirectGLES {
return (rowBytes + align - 1) / align * align;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
// Walks the client-side destination the PACK parameters describe and hands each row to
// `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client
// texels. Shared by the converting and the raw-word stores so both address the destination -
// and feed the bound pixel-pack buffer - identically.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
template <typename FillRow>
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
return true;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
return true;
}
}
}
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
const SizeT srcPixelBytes = 4 * srcComponentSize;
Vector<Uint8> convertedRow(dstRowBytes);
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
fillRow(slice, row, convertedRow.data());
if (packParams.SwapBytes) {
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
if (groupSize > 1) {
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
if (packParams.SwapBytes && swapGroupSize > 1) {
for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize);
}
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
}
}
if (pixelPackBufferObject) {
// WritebackFromBackend bumps change serials with no backend op; re-open
// the buffer draw-clean memos (once for the whole row loop).
@@ -1685,5 +1668,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return true;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type);
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
return StoreClientRows(dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels,
applyPackImageParams,
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
const SizeT flatRow = static_cast<SizeT>(slice) *
static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow =
wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, dstRow, static_cast<SizeT>(width), wideType,
mapping, type);
});
}
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams) {
PackedReadbackLayout packedLayout{};
if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) {
return false;
}
const SizeT srcRowBytes = static_cast<SizeT>(width) * 4;
return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels,
applyPackImageParams,
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
const SizeT flatRow = static_cast<SizeT>(slice) *
static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes);
});
}
} // namespace ReadbackImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+10
View File
@@ -115,6 +115,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams);
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
// False when `type` is not a 4-byte packed type.
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl
namespace PrgramImpl {
@@ -26,6 +26,7 @@
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Math/HalfFloat.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/Texture/PixelStoreProcessor.h"
#include <Config.h>
#include <algorithm>
#include <cstdlib>
@@ -2621,6 +2622,24 @@ void main() {
}
}
// The GL internal format a packed VkFormat stores, for the raw-word readback test below.
// Only the packed 32-bit layouts MobileGL keeps natively need an entry; anything else takes
// the wide decode path.
static TextureInternalFormat GetPackedReadbackInternalFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
return TextureInternalFormat::RGB9E5;
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
return TextureInternalFormat::R11FG11FB10F;
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
return TextureInternalFormat::RGB10A2;
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
return TextureInternalFormat::RGB10A2UI;
default:
return TextureInternalFormat::Unknown;
}
}
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
GLsizei sliceHeight, GLsizei sliceCount, GLenum format, GLenum type,
void* pixels, Bool applyPackImageParams,
@@ -2636,6 +2655,18 @@ void main() {
return false;
}
// A packed image read with the matching client type hands back its own words: the
// decode-to-float / re-encode round trip is lossy in the bits (it canonicalizes an
// RGB9_E5 shared exponent), which glGetTexImage must not do. Left to the wide path when
// GL_CLAMP_READ_COLOR may still have to act, i.e. for glReadPixels.
if (!applyReadColorClamp &&
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
GetPackedReadbackInternalFormat(srcFormat), MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
return DirectGLES::ReadbackImpl::StorePackedWordsToClient(srcPixels, width, sliceHeight, sliceCount,
type, pixels, applyPackImageParams);
}
Vector<Uint8> wide;
GLenum wideType = GL_FLOAT;
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width,
@@ -8499,24 +8530,106 @@ void main() {
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
}
namespace {
// GL hands CopyImageSubData ONE z/depth pair and lets the texture target decide what it
// means. Vulkan splits that meaning across two different fields of VkImageCopy, chosen by
// the image type:
//
// VK_IMAGE_TYPE_3D - slices live on the z axis: srcOffset.z/dstOffset.z select them and
// extent.depth counts them. The subresource layer range must stay
// (0, 1): Vulkan reads a 3D image as a single layer whose depth is
// the mip level's depth (VUID-VkImageCopy-apiVersion-07932/-07933).
// everything else - slices live in the array dimension: baseArrayLayer selects them and
// layerCount counts them, while offset.z stays 0 and (when neither
// endpoint is 3D) extent.depth stays 1.
//
// A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 -
// relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must
// equal the array side's layerCount".
struct CopyImageEndpoint {
// True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
Bool slicesAreDepth = false;
// The GL z offset, kept in whichever field this endpoint's image type reads it from.
Uint32 baseSlice = 0;
// Slices this endpoint can address at the selected mip level; the copy range check
// needs the level's depth for a 3D image (3D mips shrink in z) and the image's array
// size for a layered one (array layers do not shrink).
Uint32 availableSlices = 1;
Uint32 BaseArrayLayer() const { return slicesAreDepth ? 0u : baseSlice; }
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
};
Bool TryResolveCopyImageEndpoint(TextureTarget target,
const VkTextureManager::TextureResource& resource, Uint32 mipLevel,
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) {
if (glZ < 0 || glDepth <= 0) {
return false;
}
const Uint32 baseSlice = static_cast<Uint32>(glZ);
switch (target) {
case TextureTarget::Texture1D:
case TextureTarget::Texture2D:
case TextureTarget::TextureRectangle:
case TextureTarget::Texture2DMultisample:
// Not layered at all: GL still requires the z/depth pair, and it can only name the
// one slice these targets have.
outEndpoint = {};
return baseSlice == 0 && glDepth == 1;
case TextureTarget::Texture3D:
outEndpoint.slicesAreDepth = true;
outEndpoint.baseSlice = baseSlice;
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel);
return true;
case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisampleArray:
case TextureTarget::TextureCubeMap:
case TextureTarget::TextureCubeMapArray:
// A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL
// numbers its faces on the same z axis an array texture numbers its layers, so both
// arrive as a plain layer range.
outEndpoint.slicesAreDepth = false;
outEndpoint.baseSlice = baseSlice;
outEndpoint.availableSlices = resource.arrayLayers;
return true;
default:
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
// would have to be remapped against a Vulkan extent that also has to stay height 1
// for a VK_IMAGE_TYPE_1D image; GL_TEXTURE_BUFFER has no image at all. Declined
// rather than mis-addressed.
return false;
}
}
} // namespace
void VulkanRenderer::CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && srcDepth > 0,
"CopyImageSubData requires positive copy dimensions.");
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr,
"CopyImageSubData requires valid source and destination textures.");
// The frontend already declines a zero or negative extent, so anything else here is a
// caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a
// zero extent.depth is as invalid as a zero width.
if (srcWidth <= 0 || srcHeight <= 0 || srcDepth <= 0) {
MGLOG_E_ONCE("%s: non-positive copy extent %dx%dx%d; declining the copy", __func__, srcWidth, srcHeight,
srcDepth);
return;
}
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
MOBILEGL_ASSERT(srcTextureTarget == TextureTarget::Texture2D && dstTextureTarget == TextureTarget::Texture2D,
"CopyImageSubData currently only supports GL_TEXTURE_2D sources and destinations.");
MOBILEGL_ASSERT(srcDepth == 1 && srcZ == 0 && dstZ == 0,
"CopyImageSubData currently only supports single-layer 2D copies.");
MOBILEGL_ASSERT(srcTexture.get() != dstTexture.get(),
"CopyImageSubData does not support in-place texture copies yet.");
// Both endpoints of a same-image copy would have to share one VkImageLayout, so the
// TRANSFER_SRC/TRANSFER_DST pair below cannot express it (it needs VK_IMAGE_LAYOUT_GENERAL
// and an overlap check). Refused outright, and refused for real rather than through an
// assertion the release build drops: recording the pair anyway is a validation error and,
// on a tiler, a copy whose source has already been overwritten.
if (srcTexture.get() == dstTexture.get()) {
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__,
srcTexture->GetExternalIndex());
return;
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
@@ -8585,29 +8698,89 @@ void main() {
return;
}
// The supported envelope, replacing the "GL_TEXTURE_2D only" assertion that used to stand
// here: every target whose slices this function can address on one of the two Vulkan axes.
// A refusal has to be a real decline, not an assertion - the assertion compiled to nothing
// in a release build and the unsupported shape reached vkCmdCopyImage anyway.
CopyImageEndpoint srcEndpoint;
CopyImageEndpoint dstEndpoint;
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) ||
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) {
MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy",
__func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(),
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth);
return;
}
// The slice half of the region-bounds guard above. A layered endpoint's bound is NOT the
// mip-0 2D extent: an array texture is bounded by its layer count (which no mip level
// shrinks) and a 3D texture by the selected level's depth (which every level halves), so
// both come from the endpoint that resolved them.
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices ||
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) {
MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
"declining the copy",
__func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth);
return;
}
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture);
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d",
__func__, srcTexture->GetExternalIndex());
// A clear still parked on the destination would otherwise materialize AFTER this copy and
// wipe the texels it just wrote.
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture);
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d",
__func__, dstTexture->GetExternalIndex());
const VkImageLayout srcOriginalLayout = srcResource->layout;
const VkImageLayout dstOriginalLayout = dstResource->layout;
MOBILEGL_ASSERT(srcOriginalLayout != VK_IMAGE_LAYOUT_UNDEFINED,
"CopyImageSubData source image has undefined layout.");
const VkImageLayout dstRestoreLayout = dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED
? ((copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
// A layout of UNDEFINED means nothing has ever been written to the image, which on the
// SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are
// undefined by the same spec sentence that lets the application ask. Both sides therefore
// take the same shape - transition the whole image out of UNDEFINED and settle it on a
// real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to.
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) {
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
return originalLayout;
}
return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
: dstOriginalLayout;
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
};
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout);
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout);
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
VkImageLayout srcCopyLayout = srcOriginalLayout;
Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
// The barrier has to name every layer the copy touches, not just layer 0 - otherwise the
// slice fix above lands the copy on layers the barrier never transitioned, which is the
// same defect one level down. TransitionImageLayout always starts its range at
// baseArrayLayer 0, so VK_REMAINING_ARRAY_LAYERS is the whole range and a superset of
// [baseSlice, baseSlice + depth).
//
// Not `arrayLayers`, which is 1 for a 3D image: MobileGL creates 3D images
// 2D_ARRAY_COMPATIBLE, and a literal 1 on one of those means "every depth slice" today but
// "depth slice 0" once VK_KHR_maintenance9 is enabled - i.e. it would silently become a
// single-slice barrier again on a newer driver. The validation layer says so by name.
static constexpr Uint32 kAllLayers = VK_REMAINING_ARRAY_LAYERS;
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
srcResource->aspect, 0, srcResource->mipLevels, kAllLayers);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
srcCopyLayout = srcResource->layout;
} else {
Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1, kAllLayers);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
}
VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags dstAccessMask = 0;
@@ -8618,29 +8791,42 @@ void main() {
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
dstResource->aspect, 0, dstResource->mipLevels, dstResource->arrayLayers);
dstResource->aspect, 0, dstResource->mipLevels, kAllLayers);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
dstCopyLayout = dstResource->layout;
} else {
Bool dstReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1, kAllLayers);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
}
// The GL slice count reaches Vulkan on the layer axis of whichever endpoint is NOT 3D, and
// on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the
// single layer (0, 1) and its slices are counted by the depth of the copy extent. With two
// non-3D endpoints both layer counts carry it and extent.depth stays 1.
const Bool copyCrossesDepthAxis = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth;
VkImageCopy copyRegion{};
copyRegion.srcSubresource.aspectMask = copyAspectMask;
copyRegion.srcSubresource.mipLevel = srcMipLevel;
copyRegion.srcSubresource.baseArrayLayer = 0;
copyRegion.srcSubresource.layerCount = 1;
copyRegion.srcOffset = {srcX, srcY, 0};
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer();
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount;
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()};
copyRegion.dstSubresource.aspectMask = copyAspectMask;
copyRegion.dstSubresource.mipLevel = dstMipLevel;
copyRegion.dstSubresource.baseArrayLayer = 0;
copyRegion.dstSubresource.layerCount = 1;
copyRegion.dstOffset = {dstX, dstY, 0};
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight), 1};
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer();
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount;
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()};
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
copyCrossesDepthAxis ? copySliceCount : 1u};
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u "
"z=%d) extent=[%d x %d x %u]",
MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(), srcMipLevel,
copyRegion.srcSubresource.baseArrayLayer, copyRegion.srcSubresource.layerCount,
copyRegion.srcOffset.z, MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), dstMipLevel,
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
vkCmdCopyImage(frame.commandBuffer,
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
@@ -8648,12 +8834,21 @@ void main() {
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcRestoreAccessMask = 0;
GetImageTransitionDestinationState(srcOriginalLayout, srcRestoreStageMask, srcRestoreAccessMask);
Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, srcOriginalLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
srcResource->aspect, 0, srcResource->mipLevels, kAllLayers);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
} else {
Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1, kAllLayers);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
}
VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags dstRestoreAccessMask = 0;
@@ -8663,13 +8858,13 @@ void main() {
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
dstResource->aspect, 0, dstResource->mipLevels, dstResource->arrayLayers);
dstResource->aspect, 0, dstResource->mipLevels, kAllLayers);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
} else {
Bool dstRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1, kAllLayers);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
}
@@ -9630,11 +9825,14 @@ void main() {
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
GetImageTransitionSourceState(originalLayout, srcStageMask, srcAccessMask);
// The copy below reads EVERY layer of the level, so the barrier has to name every layer
// too; a layerCount of 1 left an array texture's layers 1.. in whatever layout they were
// last left in while the transfer read them.
Bool ok = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect,
static_cast<Uint32>(level), 1);
static_cast<Uint32>(level), 1, VK_REMAINING_ARRAY_LAYERS);
MOBILEGL_ASSERT(ok, "%s: failed to transition texture image", __func__);
VkBufferImageCopy copyRegion{};
@@ -9654,7 +9852,7 @@ void main() {
frame.commandBuffer, resource->image, resource->layout, originalLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, resource->aspect,
static_cast<Uint32>(level), 1);
static_cast<Uint32>(level), 1, VK_REMAINING_ARRAY_LAYERS);
MOBILEGL_ASSERT(ok, "%s: failed to restore texture image layout", __func__);
if (!SubmitReadbackCommandsAndWait(frame)) {
+129 -10
View File
@@ -3382,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl {
dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
namespace {
// The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE
// enums and every PROXY enum all convert to a TextureTarget this frontend recognises,
// so ValidateTextureTarget lets them through; here they are INVALID_ENUM.
Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) {
switch (target) {
case GL_RENDERBUFFER:
case GL_TEXTURE_1D:
case GL_TEXTURE_1D_ARRAY:
case GL_TEXTURE_2D:
case GL_TEXTURE_2D_ARRAY:
case GL_TEXTURE_2D_MULTISAMPLE:
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
case GL_TEXTURE_3D:
case GL_TEXTURE_CUBE_MAP:
case GL_TEXTURE_CUBE_MAP_ARRAY:
case GL_TEXTURE_RECTANGLE:
return true;
default:
break;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
std::format("{} is not a target glCopyImageSubData accepts as the {}.",
MG_Util::ConvertGLEnumToString(target), endpointName)));
return false;
}
IntVec3 GetCopyImageLevelSize(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level) {
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (!mipmapTexture) return textureObject->GetBaseSize();
return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
}
// glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2
// spells the failure INVALID_VALUE - "if either name does not correspond to a valid
// object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for
// the ~30 entry points that reach it through a BOUND object (where the name was never
// in question and the fault is the binding), so this is a local rule rather than a
// change to the helper.
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* endpointName) {
if (textureObject) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
std::format("The {} name does not correspond to an existing image object.", endpointName)));
return false;
}
// Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that
// does not match the object INVALID_ENUM, where the shared uniformity helper records
// INVALID_OPERATION for the upload paths that share it.
Bool ValidateCopyImageTargetMatchesObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureTarget target, const char* endpointName) {
if (!textureObject || textureObject->GetTarget() == target) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
std::format("The {} target {} does not match the target the object was created with ({}).",
endpointName, MG_Util::ConvertTextureTargetToString(target),
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
return false;
}
} // namespace
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) {
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
return false;
}
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
@@ -3396,8 +3468,13 @@ namespace MobileGL::MG_Impl::GLImpl {
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
return false;
}
if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) ||
!TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) {
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
// 18.3.2 does not accept them here - only the eleven whole-image targets do.
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
return false;
}
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
return false;
}
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
@@ -3425,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl {
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
return false;
}
if (!TextureImpl::ValidateBaseInternalFormatMatch(srcTexture->GetFormat(), dstTexture->GetFormat())) {
// A multisample image can only be copied to one with the same sample count, and a
// single-sample image reports zero - so this one comparison is also what rejects
// copying between a multisample target and a non-multisample one.
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("The two images have different sample counts ({} vs. {}).",
srcTexture->GetSamples(), dstTexture->GetSamples())));
return false;
}
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
// and no defined storage to copy into.
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
srcTexture->IsComplete(), dstTexture->IsComplete())));
return false;
}
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
return false;
}
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
srcLevelSize.x(), srcLevelSize.y(), "source") ||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
return false;
}
return true;
@@ -5600,10 +5714,15 @@ namespace MobileGL::MG_Impl::GLImpl {
void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
auto srcTexture = GetTextureObjectByName(srcName, __func__);
auto dstTexture = GetTextureObjectByName(dstName, __func__);
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel,
srcWidth, srcHeight, srcDepth)) {
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
// SharedPtr, and let the validator record the error this entry point owes.
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
MG_State::pGLContext->GetTextureObject(srcName);
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
MG_State::pGLContext->GetTextureObject(dstName);
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
return;
}
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
+73 -12
View File
@@ -15,6 +15,7 @@
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Metrics/TextureMetrics.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTextureTarget(TextureTarget target) {
@@ -515,26 +516,86 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
}
} // namespace
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
if (unsizedFormat1 != unsizedFormat2) {
// The 3-argument GenericErrorInfo constructor used to be spelled as a single
// std::format() call whose format string was the component name, so every
// diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then
// hand over component/function/message separately.
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
CopyImageTexelBlock block{};
if (compressedFormat != GL_NONE) {
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
if (info.blockByteSize != 0) {
block.byteSize = info.blockByteSize;
block.blockWidth = info.blockWidth;
block.blockHeight = info.blockHeight;
block.compressed = true;
return block;
}
}
// The size MobileGL actually stores a texel of this format in, which for every format GL
// gives a required size is that required size. The handful of legacy formats GL leaves
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
// copy between them would in fact preserve the bytes.
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
return block;
}
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
const CopyImageTexelBlock& dstBlock) {
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
"A copied image has no storage whose texel size is known."));
return false;
}
if (srcBlock.byteSize != dstBlock.byteSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
std::format("The base internal format of the two formats do not match ({} vs. {})",
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
"formats are not copy-compatible.",
srcBlock.byteSize, dstBlock.byteSize)));
return false;
}
// Two compressed images additionally have to agree on the SHAPE of the block, not only
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
if (srcBlock.compressed && dstBlock.compressed &&
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
dstBlock.blockHeight)));
return false;
}
return true;
}
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
Int imageWidth, Int imageHeight, const char* endpointName) {
if (!block.compressed) return true;
const Int blockWidth = static_cast<Int>(block.blockWidth);
const Int blockHeight = static_cast<Int>(block.blockHeight);
if (blockWidth <= 1 && blockHeight <= 1) return true;
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
// whole even when the last block is partial.
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
if (originAligned && widthOk && heightOk) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
"grid of a {} x {} image.",
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
return false;
}
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
+26 -2
View File
@@ -50,8 +50,32 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TextureTarget target);
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
// The texel block of one glCopyImageSubData endpoint, resolved to the two things the
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
// a blocked image" from "uncompressed, and so it is measured in texels".
struct CopyImageTexelBlock {
SizeT byteSize = 0;
Uint blockWidth = 1;
Uint blockHeight = 1;
Bool compressed = false;
};
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
// alone can no longer tell a BPTC image from the RGBA8 backing it.
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
// pairs with an uncompressed one whose texel is as big as the compressed block.
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
const CopyImageTexelBlock& dstBlock);
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
// the edge of the image.
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
Int imageWidth, Int imageHeight, const char* endpointName);
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
// the requested internalformat asks for, but may supply more.
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
@@ -79,6 +79,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/XfbCaptureBufferReuseScenario.cpp
Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -0,0 +1,331 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.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 - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
//
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
//
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
//
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
// and extent.depth counts them; the layer range must stay (0, 1).
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
// them and layerCount counts them; offset.z stays 0.
//
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
// scenario per pair is the coverage that matters here.
//
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
// slices-landed check and fail this one.
//
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
// slice through the same call, so the two texture kinds are read back identically.
//
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
#include <array>
#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 kWidth = 4;
constexpr int kHeight = 4;
// Six is enough for a copy that starts and ends away from both edges of both endpoints
// while still leaving untouched slices on either side to assert against.
constexpr int kSlices = 6;
struct Rgba8 {
GLubyte r = 0, g = 0, b = 0, a = 0;
bool operator==(const Rgba8& other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
};
std::string Describe(const Rgba8& color) {
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
", " + std::to_string(color.a) + ")";
}
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
// in which SLICE the copy addresses, and a value that also varied within the slice would
// make the assertions depend on the framebuffer row order as well.
Rgba8 SourceColor(int slice) {
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
static_cast<GLubyte>(200 - slice * 15), 255};
}
Rgba8 DestinationFill(int slice) {
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
static_cast<GLubyte>(120 + slice * 5), 255};
}
class CopyImageLayeredScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (!CopyImageSubDataUsable()) {
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
}
}
void TearDown() override {
if (!Ready()) return;
for (const GLuint texture : m_textures) {
glDeleteTextures(1, &texture);
}
m_textures.clear();
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
}
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
// on any of the behaviour under test, so a driver (or a backend function table) that
// simply does not have the call skips instead of failing every case below.
bool CopyImageSubDataUsable() {
GLuint probe[2] = {0, 0};
glGenTextures(2, probe);
for (const GLuint texture : probe) {
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
}
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
while (glGetError() != GL_NO_ERROR) {
}
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
0, 1, 1, 1);
const bool usable = glGetError() == GL_NO_ERROR;
glDeleteTextures(2, probe);
return usable;
}
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
// names, so the level's own extent - a 3D level's depth included - has to be resolved
// rather than assumed to be the image's.
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(target, texture);
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// Fill every level, so nothing below can pass by reading a level that was never
// written and happened to hold the expected bytes.
for (int level = 0; level < levels; ++level) {
const int levelWidth = kWidth << (levels - 1 - level);
const int levelHeight = kHeight << (levels - 1 - level);
const int levelSlices =
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
for (int slice = 0; slice < levelSlices; ++slice) {
const Rgba8 color = colorForSlice(slice % kSlices);
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
GL_UNSIGNED_BYTE, texels.data());
}
}
glBindTexture(target, 0);
return texture;
}
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
// array layer and a 3D slice through the same argument, so both targets read back the
// same way.
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
if (m_fbo == 0) {
glGenFramebuffers(1, &m_fbo);
}
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "slice " << slice << " of level " << level << " is not attachable";
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// The fill is uniform within a slice, so any disagreement between texels is itself
// a failure - reported here rather than silently reduced to pixels[0].
for (size_t i = 1; i < pixels.size(); ++i) {
EXPECT_TRUE(pixels[i] == pixels[0])
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
}
return pixels[0];
}
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
// source slice they were fed, and every slice outside it still holds its own fill.
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
int dstZ, int depth, const char* what) {
for (int slice = 0; slice < sliceCount; ++slice) {
const bool inRange = slice >= dstZ && slice < dstZ + depth;
const Rgba8 expected =
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
EXPECT_TRUE(actual == expected)
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
<< " is " << Describe(actual) << ", expected " << Describe(expected);
}
}
std::vector<GLuint> m_textures;
GLuint m_fbo = 0;
};
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
// counts carry the depth and extent.depth must stay 1.
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
kWidth, kHeight, kSlices);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
}
// The same pair with the layer ranges offset differently on the two sides: the shape that
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
// the source range but wrote from layer 0 (or vice versa) passes the case above.
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 3;
constexpr int kDstZ = 1;
constexpr int kDepth = 2;
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
kDstZ, kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
"array->array, offset layer ranges");
}
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 1;
constexpr int kDstZ = 3;
constexpr int kDepth = 3;
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
}
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
// this is the only case where the slice count the copy may name is not the image's own -
// the bound a layered endpoint is checked against has to come from the level.
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kLevel = 1;
constexpr int kSrcZ = 2;
constexpr int kDstZ = 0;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
kDstZ, kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
"3d->3d at mip level 1");
}
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 2;
constexpr int kDstZ = 1;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
}
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
// while the source states it as extent.depth from a z offset.
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 1;
constexpr int kDstZ = 2;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
}
} // namespace
} // namespace MGITest
+490
View File
@@ -3179,6 +3179,193 @@ TEST_F(TextureTest, DecodeShadowDataToWideRGBACoversComponentAndPackedLayouts) {
}
}
// ---- GL_RGB9_E5 raw-preserving transfer --------------------------------------------------------
// RGB9_E5 packs three 9-bit mantissas against one shared 5-bit exponent, so a value has several
// legal encodings (shift the exponent up, shift every mantissa down). The spec's encode algorithm
// (GL 4.6 8.5.2) always emits the canonical one, which makes decode-to-float / re-encode
// value-preserving but NOT bit-preserving. glTexImage followed by glGetTexImage has to hand the
// application its own bits back, so a client (format, type) whose word already IS the storage word
// must move verbatim. GL CTS KHR-GL43.copy_image caught the round trip turning the uploaded
// 0xf8fc0000 into 0xe7e00000 ("CopyImageSubData modified contents of source image") and a copied-in
// 0x60000000 into 0x00000000 ("CopyImageSubData stored invalid data in copied region").
namespace {
Uint32 RoundTripSharedExponentWord(Uint32 word) {
Float rgb[3];
MG_Util::DecodeSharedExponentRGB9E5(word, rgb);
return MG_Util::EncodeSharedExponentRGB9E5(rgb);
}
} // namespace
TEST(SharedExponentRGB9E5Test, EncodeReproducesCanonicalWordsExactly) {
// Canonical encodings - the ones the spec algorithm emits - must survive a decode/encode round
// trip untouched, or every conversion INTO RGB9_E5 would be off as well.
const Uint32 canonical[] = {
0x00000000u, // all zero
0x0FFFFFFFu, // exponent 1, every mantissa saturated (smallest normalized exponent in use)
0x000003FFu, // exponent 0: the denormal range, mantissas 511 / 1 / 0
0x81010100u, // (1.0, 0.5, 0.25)
0xE7E00000u, // (0, 0, 8064) - what the CTS round trip produced
0xFFFFFFFFu, // exponent 31 with saturated mantissas = the largest representable texel
};
for (const Uint32 word : canonical) {
EXPECT_EQ(RoundTripSharedExponentWord(word), word) << "word 0x" << std::hex << word;
// Encoding is idempotent: a second pass may not drift either.
EXPECT_EQ(RoundTripSharedExponentWord(RoundTripSharedExponentWord(word)), word);
}
}
TEST(SharedExponentRGB9E5Test, EncodeCanonicalizesRedundantWords) {
// The exact QPA signatures. Both pairs hold the same value, so the encoder is not wrong - which
// is why the fix has to be a raw path rather than an encoder change.
Float observed[3];
MG_Util::DecodeSharedExponentRGB9E5(0xF8FC0000u, observed);
Float canonical[3];
MG_Util::DecodeSharedExponentRGB9E5(0xE7E00000u, canonical);
EXPECT_EQ(observed[2], 8064.0f);
EXPECT_EQ(canonical[2], 8064.0f);
EXPECT_EQ(RoundTripSharedExponentWord(0xF8FC0000u), 0xE7E00000u);
// Exponent 12 with all-zero mantissas is still the value zero, and canonicalizes to the
// all-zero word.
EXPECT_EQ(RoundTripSharedExponentWord(0x60000000u), 0x00000000u);
// Mantissa 1 at exponent 1 renormalizes down into the denormal range.
EXPECT_EQ(RoundTripSharedExponentWord(0x08000001u), 0x00000002u);
}
TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts) {
using MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer;
// The four pairs whose client word is bit-identical to the packed storage word.
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::R11FG11FB10F, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt101111Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2UI, TextureInputFormat::RGBAInteger,
TexturePixelDataType::UnsignedInt2101010Rev));
// A different packed float layout of the same width is still a conversion.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt101111Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::R11FG11FB10F, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
// So is a component client type, or the same word against a component internal format.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::Float));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB8, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGBA32F, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
// Integerness has to line up too: the normalized and integer 10/10/10/2 words are not the
// same client layout even though they are the same bit field.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2, TextureInputFormat::RGBAInteger,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2UI, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::Unknown, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
}
TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) {
// Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client
// words untouched, including the redundant encodings the CTS generates.
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint32 words[] = {0xF8FC0000u, 0x60000000u, 0x08000001u, 0x0FFFFFFFu};
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 4, 1, 0, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, words);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
Uint32 readBack[4] = {};
std::memcpy(readBack, stored, sizeof(readBack));
for (Int i = 0; i < 4; ++i) {
EXPECT_EQ(readBack[i], words[i]) << "texel " << i;
}
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, TexImage2DRGB9E5FromOtherPackedFloatTypeStillConverts) {
// Negative control for the raw path: a genuinely different client layout keeps the
// decode-to-float / re-encode conversion.
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
// 10F_11F_11F_REV word holding (1.0, 0.5, 0.25) - see the packed readback encode tests.
const Uint32 packedFloatWord = 0x681C03C0u;
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 1, 1, 0, GL_RGB, GL_UNSIGNED_INT_10F_11F_11F_REV,
&packedFloatWord);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
Uint32 word = 0;
std::memcpy(&word, stored, sizeof(word));
const Float rgb[3] = {1.0f, 0.5f, 0.25f};
EXPECT_EQ(word, MG_Util::EncodeSharedExponentRGB9E5(rgb));
EXPECT_NE(word, packedFloatWord) << "the raw path must not swallow a real conversion";
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams) {
// Readback direction: the raw store copies the words bit-for-bit while still honoring the
// client-side PACK addressing (alignment, skip rows/pixels) and GL_PACK_SWAP_BYTES.
namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl;
const Uint32 source[] = {0xF8FC0000u, 0x60000000u, 0x08000001u, // row 0
0x0FFFFFFFu, 0xFFFFFFFFu, 0x00000000u}; // row 1
constexpr Uint32 kFill = 0xDEADBEEFu;
Uint32 destination[16];
std::fill(std::begin(destination), std::end(destination), kFill);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 8); // rows of 3 words (12 B) pad to 16 B
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 1);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 1);
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
/*sliceHeight=*/2, /*sliceCount=*/1,
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
/*applyPackImageParams=*/false));
// Row 0 lands at SKIP_ROWS * 16 + SKIP_PIXELS * 4 = 20 bytes = word 5; row 1 one 16-byte
// stride further along, at word 9.
for (Int i = 0; i < 3; ++i) {
EXPECT_EQ(destination[5 + i], source[i]) << "row 0 texel " << i;
EXPECT_EQ(destination[9 + i], source[3 + i]) << "row 1 texel " << i;
}
// The skipped region and the row padding stay untouched.
EXPECT_EQ(destination[0], kFill);
EXPECT_EQ(destination[4], kFill);
EXPECT_EQ(destination[8], kFill);
EXPECT_EQ(destination[12], kFill);
// GL_PACK_SWAP_BYTES reverses each 4-byte word.
std::fill(std::begin(destination), std::end(destination), kFill);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 0);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 0);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 1);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE);
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
/*sliceHeight=*/1, /*sliceCount=*/1,
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
/*applyPackImageParams=*/false));
EXPECT_EQ(destination[0], 0x0000FCF8u); // byte-reversed 0xF8FC0000
EXPECT_EQ(destination[1], 0x00000060u);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 4);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core table 23.18: GL_TEXTURE_COMPARE_FUNC takes the whole eight-function depth-compare
// range. The validator used to start it at GL_LEQUAL, which sits in the middle of the contiguous
// GL_NEVER..GL_ALWAYS block, so NEVER/LESS/EQUAL were rejected while GREATER/NOTEQUAL/GEQUAL only
@@ -3846,3 +4033,306 @@ TEST_F(TextureTest, TexStorage2DLeavesAGenericCompressedFormatUncompressed) {
EXPECT_EQ(compressed, GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// ===================== glCopyImageSubData validation (KHR-GL43.copy_image) =====================
//
// Every case below is a mechanism the conformance group caught in the field, and each one is
// pinned here because the backend cannot: a wrongly ACCEPTED copy shows up only as wrong pixels
// on a device, and a wrongly REJECTED one shows up only as a conformance failure.
namespace {
struct CopyImageSubDataCall {
Bool Called = false;
GLenum SrcTarget = GL_NONE;
GLenum DstTarget = GL_NONE;
GLint SrcZ = -1;
GLint DstZ = -1;
GLsizei Depth = -1;
} g_copyImageSubDataCall;
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
GLsizei srcHeight, GLsizei srcDepth) {
(void)srcTexture;
(void)srcLevel;
(void)srcX;
(void)srcY;
(void)dstTexture;
(void)dstLevel;
(void)dstX;
(void)dstY;
(void)srcWidth;
(void)srcHeight;
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth};
}
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
// call in every respect except the one the test is about.
void MakeCopyImagePair(GLenum srcFormat, GLenum dstFormat, GLuint& srcTexture, GLuint& dstTexture,
GLsizei levels = 1, GLsizei extent = 8) {
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &srcTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &dstTexture);
MG_Impl::GLImpl::TextureStorage2D(srcTexture, levels, srcFormat, extent, extent);
MG_Impl::GLImpl::TextureStorage2D(dstTexture, levels, dstFormat, extent, extent);
}
} // namespace
// GL 4.6 core 18.3.2 compatibility is texel-block SIZE, not base internal format. RGB10_A2 and
// R11F_G11F_B10F are both 32-bit and their bases differ (RGBA vs RGB); the old exact-base-format
// predicate rejected the pair, which is what took down the whole cross-format half of the
// conformance matrix on both backends.
TEST_F(TextureTest, CopyImageSubDataAcceptsEqualTexelSizeAcrossDifferentBaseFormats) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGB10_A2, GL_R11F_G11F_B10F, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The other half of the same rule: equal base format is not sufficient either. RGBA8 and RGBA32F
// are both RGBA and 32 vs 128 bits, so the copy is illegal.
TEST_F(TextureTest, CopyImageSubDataRejectsDifferentTexelSizesWithTheSameBaseFormat) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA8, GL_RGBA32F, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
4, 4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// ...and the pairing that is legal purely because the sizes agree, across integer-ness too.
TEST_F(TextureTest, CopyImageSubDataAcceptsIntegerAndFloatOfTheSameTexelSize) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA32UI, GL_RGBA32F, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// 18.3.2 spells a name that is not an object INVALID_VALUE. The shared texture-object validator
// says INVALID_OPERATION, which is right for the entry points that reach an object through a
// BINDING - hence a rule local to this entry point rather than a change to the helper.
TEST_F(TextureTest, CopyImageSubDataNonExistentNameIsInvalidValue) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(4242, GL_TEXTURE_2D, 0, 0, 0, 0, 4243, GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// A target that disagrees with the object it names is INVALID_ENUM, not the INVALID_OPERATION the
// shared target-uniformity validator records for the upload paths.
TEST_F(TextureTest, CopyImageSubDataTargetNotMatchingTheObjectIsInvalidEnum) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D_ARRAY, 0, 0,
0, 0, 1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_ENUM);
}
// The eleven whole-image targets only: a cube FACE converts to a target the frontend knows, so the
// generic target validator lets it through, but 18.3.2 does not accept it here.
TEST_F(TextureTest, CopyImageSubDataRejectsTargetsOutsideTheSpecList) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0, 0, 0, dstTexture,
GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_ENUM);
}
// A level the image does not have is INVALID_VALUE; a single-level texture asked for level 1 used
// to reach the backend with whatever the storage layer answered for that level.
TEST_F(TextureTest, CopyImageSubDataRejectsLevelTheImageDoesNotHave) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 1, 0, 0, 0,
1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// Sample counts must match. A single-sample image reports zero, so this same comparison is also
// what refuses a copy between a multisample target and a non-multisample one.
TEST_F(TextureTest, CopyImageSubDataRejectsSampleCountMismatch) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
// Two DIFFERENT counts are the whole point, so the case needs a context that can actually
// create multisample storage - which this unit-test binary, with no backend behind the
// renderable-format and sample-count queries, may not be able to. The precondition is
// checked on the state objects rather than assumed, so this can only ever skip or test the
// real rule; it can never pass vacuously.
GLint maxSamples = 1;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &maxSamples);
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstTexture);
MG_Impl::GLImpl::TextureStorage2DMultisample(srcTexture, 1, GL_RGBA8, 8, 8, GL_FALSE);
MG_Impl::GLImpl::TextureStorage2DMultisample(dstTexture, std::max(maxSamples, 2), GL_RGBA8, 8, 8, GL_FALSE);
DrainPendingGlErrors();
const Int srcSamples = MG_State::pGLContext->GetTextureObject(srcTexture)->GetSamples();
const Int dstSamples = MG_State::pGLContext->GetTextureObject(dstTexture)->GetSamples();
if (srcSamples == dstSamples) {
GTEST_SKIP() << "this context could not give the two textures different sample counts (both " << srcSamples
<< "); nothing for the rule to reject";
}
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstTexture,
GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 1, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// The layer range has to survive the frontend intact. Both backends used to drop it - DirectVulkan
// pinned baseArrayLayer/layerCount at 0/1 - so a 12-layer copy moved one layer and said nothing;
// this pins the frontend half of that contract.
TEST_F(TextureTest, CopyImageSubDataForwardsTheWholeLayerRangeToTheBackend) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &srcTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &dstTexture);
MG_Impl::GLImpl::TextureStorage3D(srcTexture, 1, GL_RGBA8, 8, 8, 12);
MG_Impl::GLImpl::TextureStorage3D(dstTexture, 1, GL_RGBA8, 8, 8, 12);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, dstTexture, GL_TEXTURE_2D_ARRAY,
0, 0, 0, 5, 4, 4, 7);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(g_copyImageSubDataCall.SrcZ, 2);
EXPECT_EQ(g_copyImageSubDataCall.DstZ, 5);
EXPECT_EQ(g_copyImageSubDataCall.Depth, 7);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The shape KHR-GL43.copy_image.invalid_object ends on once the invalid-name cases are answered
// correctly: two ordinary glTexImage2D textures, no storage object, one texel copied from the
// origin. Nothing about it is exotic, which is exactly why it is worth a case of its own - every
// rule added to this validator is a new way to reject it.
TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MG_Impl::GLImpl::GenTextures(1, &srcTexture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, srcTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
MG_Impl::GLImpl::GenTextures(1, &dstTexture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, dstTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
1, 1, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// ...and again after the names have been through a delete/regenerate cycle, which is what the
// conformance case does between its sub-cases: it deletes an object to make it invalid, then
// builds the next pair from names the allocator hands straight back.
MG_Impl::GLImpl::DeleteTextures(1, &srcTexture);
MG_Impl::GLImpl::DeleteTextures(1, &dstTexture);
DrainPendingGlErrors();
g_copyImageSubDataCall = {};
GLuint reusedSrc = 0;
GLuint reusedDst = 0;
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedSrc);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::GenTextures(1, &reusedDst);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedDst);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
1, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// A rectangle target reaches the backend as itself. The translation to the GL_TEXTURE_2D the ES
// driver actually stores it in belongs to DirectGLES, not here - and putting it here would break
// DirectVulkan, which needs the real target to tell an array copy from a flat one.
TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcTexture = 0;
GLuint dstTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstTexture);
MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 8, 8);
MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstTexture, GL_TEXTURE_RECTANGLE,
0, 0, 0, 0, 4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(g_copyImageSubDataCall.SrcTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
@@ -259,6 +259,25 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
}
}
// The one client (format, type) pair whose word is bit-identical to the packed internal
// word, if any. Everything else has to go through the decode/encode conversion.
Bool IsRawPackedPixelPair(PackedInternalKind kind, TextureInputFormat format,
TexturePixelDataType type) {
switch (kind) {
case PackedInternalKind::UNorm2101010Rev:
return format == TextureInputFormat::RGBA && type == TexturePixelDataType::UnsignedInt2101010Rev;
case PackedInternalKind::UInt2101010Rev:
return format == TextureInputFormat::RGBAInteger &&
type == TexturePixelDataType::UnsignedInt2101010Rev;
case PackedInternalKind::FloatR11G11B10:
return format == TextureInputFormat::RGB && type == TexturePixelDataType::UnsignedInt101111Rev;
case PackedInternalKind::FloatRGB9E5:
return format == TextureInputFormat::RGB && type == TexturePixelDataType::UnsignedInt5999Rev;
default:
return false;
}
}
Uint32 EncodePackedInternalWordFloat(PackedInternalKind kind, const Float rgba[4]) {
switch (kind) {
case PackedInternalKind::UNorm2101010Rev: {
@@ -431,10 +450,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
return false;
}
// The client word already equals the packed internal word (memcpy fast path).
if (hasPackedInternal && type == TexturePixelDataType::UnsignedInt2101010Rev &&
(format == TextureInputFormat::RGBA || format == TextureInputFormat::RGBAInteger) &&
(packedInternal.kind == PackedInternalKind::UNorm2101010Rev ||
packedInternal.kind == PackedInternalKind::UInt2101010Rev)) {
if (hasPackedInternal && IsRawPackedPixelPair(packedInternal.kind, format, type)) {
return false;
}
} else {
@@ -458,11 +474,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
// GL_RGB, which the state layer already enforces.
if (mapping.isInteger || mapping.channelCount != 3) return false;
// The client word already equals the packed internal word.
if (hasPackedInternal &&
((packedInternal.kind == PackedInternalKind::FloatRGB9E5 &&
type == TexturePixelDataType::UnsignedInt5999Rev) ||
(packedInternal.kind == PackedInternalKind::FloatR11G11B10 &&
type == TexturePixelDataType::UnsignedInt101111Rev))) {
if (hasPackedInternal && IsRawPackedPixelPair(packedInternal.kind, format, type)) {
return false;
}
break;
@@ -765,6 +777,15 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
}
} // namespace
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
TexturePixelDataType clientType) {
InternalPackedLayout packedInternal{};
if (!GetInternalPackedLayout(internalFormat, packedInternal)) {
return false;
}
return IsRawPackedPixelPair(packedInternal.kind, clientFormat, clientType);
}
// assume 8 bit per channel
// swizzle.size() == channel count
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) {
@@ -23,6 +23,21 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
IntVec3 dimension, Bool isBitmap, SizeT& outSize);
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle);
// True when a packed internal format's 32-bit storage word IS the client (format, type) word,
// so the transfer has to move the words verbatim in both directions.
//
// Decoding such a texel to float and re-encoding it is NOT a no-op: RGB9_E5 stores a shared
// exponent with redundant encodings, and the spec's encode algorithm (GL 4.6 8.5.2) always
// emits the canonical one - 0xf8fc0000 and 0xe7e00000 are the same value 8064, but only the
// latter is canonical. glTexImage followed by glGetTexImage must hand back the bits the
// application uploaded, which is what GL CTS KHR-GL43.copy_image compares
// ("CopyImageSubData modified contents of source image").
//
// Only the pairs whose bit layouts are identical qualify; a genuinely different client format
// or type still needs the decode/encode conversion.
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
TexturePixelDataType clientType);
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
// readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with
// 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set