mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
[Merge] (CTS): land the GL43 copy_image and clear_tex_image fixes
This commit is contained in:
@@ -14,6 +14,7 @@ namespace MobileGL {
|
||||
namespace MG_State::GLState {
|
||||
class FramebufferObject;
|
||||
class ITextureObject;
|
||||
class RenderbufferObject;
|
||||
}
|
||||
|
||||
enum class BackendType {
|
||||
@@ -24,6 +25,19 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
namespace MG_Backend {
|
||||
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
|
||||
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
|
||||
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
|
||||
// one of the two pointers is set; neither is set when the name named nothing, which is
|
||||
// the INVALID_VALUE the frontend validator reports.
|
||||
struct CopyImageEndpoint {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> Texture;
|
||||
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
|
||||
|
||||
Bool IsRenderbuffer() const { return Renderbuffer != nullptr; }
|
||||
Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; }
|
||||
};
|
||||
|
||||
enum class FormatCapability : Uint64 {
|
||||
Creatable = 1ull << 0,
|
||||
|
||||
@@ -160,9 +174,9 @@ namespace MobileGL {
|
||||
GLsizei height, GLint border);
|
||||
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height);
|
||||
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void (*CopyImageSubData)(const CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void (*GenerateMipmap)(GLenum target);
|
||||
|
||||
@@ -5708,27 +5708,87 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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.
|
||||
//
|
||||
// GL_RENDERBUFFER is the exception that must NOT be translated: ES 3.2 core (and
|
||||
// GL_EXT_copy_image) take it as a srcTarget/dstTarget verbatim, while
|
||||
// ConvertGLEnumToTextureTarget answers Unknown for it and the translation below would hand
|
||||
// the driver GL_UNKNOWN_MGL.
|
||||
struct GLESCopyImageEndpoint {
|
||||
GLenum target = GL_TEXTURE_2D;
|
||||
// Exactly one of the two is set. The backend object is kept rather than its id, because
|
||||
// the id is only stable until the OTHER endpoint syncs (a sync can re-mint a texture),
|
||||
// so it is read at the point of use.
|
||||
SharedPtr<TextureImpl::BackendTextureObject> texture;
|
||||
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> renderbuffer;
|
||||
GLint x = 0;
|
||||
GLint y = 0;
|
||||
GLint z = 0;
|
||||
|
||||
Bool IsRenderbuffer() const { return renderbuffer != nullptr; }
|
||||
GLuint Name() const {
|
||||
if (renderbuffer) return renderbuffer->GetBackendRenderbufferId();
|
||||
return texture ? texture->GetBackendTextureId() : 0u;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
|
||||
// find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
|
||||
// reachable from a path that has a renderbuffer but no framebuffer.
|
||||
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
|
||||
if (!renderbufferObject) return nullptr;
|
||||
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
|
||||
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
|
||||
backendRenderbufferObject = *slot;
|
||||
} else {
|
||||
auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
|
||||
if (!newSlot) {
|
||||
newSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
||||
}
|
||||
backendRenderbufferObject = newSlot;
|
||||
}
|
||||
endpoint.x = x;
|
||||
endpoint.y = y;
|
||||
endpoint.z = z;
|
||||
return endpoint;
|
||||
backendRenderbufferObject->SyncToBackend(renderbufferObject);
|
||||
return backendRenderbufferObject;
|
||||
}
|
||||
|
||||
static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y,
|
||||
GLint z, GLESCopyImageEndpoint& out) {
|
||||
if (endpoint.IsRenderbuffer()) {
|
||||
out.renderbuffer = SyncRenderbufferObjectToBackend(endpoint.Renderbuffer);
|
||||
if (!out.renderbuffer) return false;
|
||||
out.target = GL_RENDERBUFFER;
|
||||
out.x = x;
|
||||
out.y = y;
|
||||
out.z = z;
|
||||
return true;
|
||||
}
|
||||
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
|
||||
// slot inside the backend texture registry, and the second call mutates that very map:
|
||||
// GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by
|
||||
// robin-hood displacement well under the load factor), and Find drops any
|
||||
// entry whose state object has expired - which, with the map open-addressed and erasing
|
||||
// by shifting the probe cluster backwards, relocates entries other than the erased one.
|
||||
// Either way a reference taken by the first call is stale by the time the second returns,
|
||||
// and it is read four more times below. Copying the SharedPtr costs two refcount bumps on
|
||||
// a path that is already doing a texture copy.
|
||||
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
|
||||
// reaches here - but the assertion that says so is compiled out of a release build, and
|
||||
// SyncTextureObjectToBackend would register a null state object.
|
||||
if (!endpoint.Texture) return false;
|
||||
out.texture = TextureImpl::SyncTextureObjectToBackend(endpoint.Texture);
|
||||
if (!out.texture) return false;
|
||||
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
|
||||
out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||
out.x = x;
|
||||
out.y = 0;
|
||||
out.z = y;
|
||||
return true;
|
||||
}
|
||||
out.x = x;
|
||||
out.y = y;
|
||||
out.z = z;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
|
||||
@@ -5744,85 +5804,172 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
std::swap(height, depth);
|
||||
}
|
||||
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) {
|
||||
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
|
||||
return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown;
|
||||
}
|
||||
|
||||
// Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one
|
||||
// upload target (so not a cube map, whose six chains the z axis selects between) and layers on
|
||||
// the z axis (GL_TEXTURE_1D_ARRAY carries them on y).
|
||||
static Bool CanMirrorCopyImageShadow(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
||||
if (!texture) return false;
|
||||
if (texture->GetTarget() == TextureTarget::Texture1DArray) return false;
|
||||
return texture->GetUploadTargets().size() == 1;
|
||||
}
|
||||
|
||||
// glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU
|
||||
// shadow has to stay authoritative - a packed format with redundant encodings, where a GPU
|
||||
// readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) -
|
||||
// the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already
|
||||
// put these texels on the GPU, and flagging the level would only schedule a redundant upload
|
||||
// back over them.
|
||||
//
|
||||
// Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot
|
||||
// address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level
|
||||
// whose shadow is missing or not a plain texel grid, a region outside either level, or a
|
||||
// self-copy within one level, where the row copies could overlap.
|
||||
static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX,
|
||||
GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint,
|
||||
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei width, GLsizei height, GLsizei depth) {
|
||||
if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return;
|
||||
if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return;
|
||||
if (width <= 0 || height <= 0 || depth <= 0) return;
|
||||
if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) {
|
||||
return;
|
||||
}
|
||||
auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get());
|
||||
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
|
||||
if (!srcMipmap || !dstMipmap) return;
|
||||
|
||||
const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0];
|
||||
const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0];
|
||||
const IntVec3 srcSize = srcMipmap->GetMipmapTexelSize(srcUploadTarget, static_cast<Uint>(srcLevel));
|
||||
const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast<Uint>(dstLevel));
|
||||
const SizeT srcSlices = static_cast<SizeT>(std::max(srcSize.z(), 1));
|
||||
const SizeT dstSlices = static_cast<SizeT>(std::max(dstSize.z(), 1));
|
||||
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return;
|
||||
const SizeT srcTexels = static_cast<SizeT>(srcSize.x()) * static_cast<SizeT>(srcSize.y()) * srcSlices;
|
||||
const SizeT dstTexels = static_cast<SizeT>(dstSize.x()) * static_cast<SizeT>(dstSize.y()) * dstSlices;
|
||||
const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast<Uint>(srcLevel));
|
||||
const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast<Uint>(dstLevel));
|
||||
// A shadow that is not exactly texels x texelSize bytes is one this cannot index (a
|
||||
// compressed blob, or a level whose allocation disagrees with its recorded extent).
|
||||
const SizeT texelBytes = srcTexels == 0 ? 0 : srcBytes / srcTexels;
|
||||
if (texelBytes == 0 || srcBytes != srcTexels * texelBytes || dstTexels == 0 ||
|
||||
dstBytes != dstTexels * texelBytes) {
|
||||
return;
|
||||
}
|
||||
if (static_cast<SizeT>(srcX) + width > static_cast<SizeT>(srcSize.x()) ||
|
||||
static_cast<SizeT>(srcY) + height > static_cast<SizeT>(srcSize.y()) ||
|
||||
static_cast<SizeT>(srcZ) + depth > srcSlices ||
|
||||
static_cast<SizeT>(dstX) + width > static_cast<SizeT>(dstSize.x()) ||
|
||||
static_cast<SizeT>(dstY) + height > static_cast<SizeT>(dstSize.y()) ||
|
||||
static_cast<SizeT>(dstZ) + depth > dstSlices) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* srcBase = static_cast<const Uint8*>(
|
||||
srcMipmap->MapMipmapData(srcUploadTarget, static_cast<Uint>(srcLevel)));
|
||||
auto* dstBase = static_cast<Uint8*>(dstMipmap->MapMipmapData(dstUploadTarget, static_cast<Uint>(dstLevel)));
|
||||
if (!srcBase || !dstBase) return;
|
||||
|
||||
const SizeT rowBytes = static_cast<SizeT>(width) * texelBytes;
|
||||
for (GLsizei slice = 0; slice < depth; ++slice) {
|
||||
for (GLsizei row = 0; row < height; ++row) {
|
||||
const SizeT srcOffset = ((static_cast<SizeT>(srcZ + slice) * static_cast<SizeT>(srcSize.y()) +
|
||||
static_cast<SizeT>(srcY + row)) *
|
||||
static_cast<SizeT>(srcSize.x()) +
|
||||
static_cast<SizeT>(srcX)) *
|
||||
texelBytes;
|
||||
const SizeT dstOffset = ((static_cast<SizeT>(dstZ + slice) * static_cast<SizeT>(dstSize.y()) +
|
||||
static_cast<SizeT>(dstY + row)) *
|
||||
static_cast<SizeT>(dstSize.x()) +
|
||||
static_cast<SizeT>(dstX)) *
|
||||
texelBytes;
|
||||
Memcpy(dstBase + dstOffset, srcBase + srcOffset, rowBytes);
|
||||
}
|
||||
}
|
||||
MGLOG_D("CopyImageSubData: mirrored %dx%dx%d texels into the destination's CPU shadow", width, height,
|
||||
depth);
|
||||
}
|
||||
|
||||
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dstEndpoint,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
|
||||
// slot inside the backend texture registry, and the second call mutates that very map:
|
||||
// GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by
|
||||
// robin-hood displacement well under the load factor), and Find drops any
|
||||
// entry whose state object has expired - which, with the map open-addressed and erasing
|
||||
// by shifting the probe cluster backwards, relocates entries other than the erased one.
|
||||
// Either way a reference taken by the first call is stale by the time the second returns,
|
||||
// and it is read four more times below. Copying the SharedPtr costs two refcount bumps on
|
||||
// a path that is already doing a texture copy.
|
||||
const SharedPtr<TextureImpl::BackendTextureObject> srcBackendTexture =
|
||||
TextureImpl::SyncTextureObjectToBackend(srcTexture);
|
||||
const SharedPtr<TextureImpl::BackendTextureObject> dstBackendTexture =
|
||||
TextureImpl::SyncTextureObjectToBackend(dstTexture);
|
||||
GLESCopyImageEndpoint src{};
|
||||
GLESCopyImageEndpoint dst{};
|
||||
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync
|
||||
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was
|
||||
// supposed to catch it expands to nothing. The four GetBackendTextureId() calls below
|
||||
// are the same dereference. The frontend validator is what keeps this unreachable and
|
||||
// what reports the error the application is owed; declining is only how a future gap up
|
||||
// there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
|
||||
if (!srcBackendTexture || !dstBackendTexture) {
|
||||
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
|
||||
// supposed to catch it expands to nothing. The four Name() calls below are the same
|
||||
// dereference. The frontend validator is what keeps this unreachable and what reports
|
||||
// the error the application is owed; declining is only how a future gap up there stops
|
||||
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
|
||||
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
|
||||
!MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
|
||||
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
|
||||
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());
|
||||
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstTexture->GetFormat());
|
||||
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
|
||||
const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint);
|
||||
const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint);
|
||||
// Both emulation fallbacks below are written against TEXTURE ids and texture targets, so
|
||||
// an endpoint that is a renderbuffer takes the native ES copy - which accepts
|
||||
// GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver
|
||||
// turns it down.
|
||||
const Bool anyRenderbuffer = src.IsRenderbuffer() || dst.IsRenderbuffer();
|
||||
|
||||
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcFormat);
|
||||
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstFormat);
|
||||
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcFormat);
|
||||
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstFormat);
|
||||
if (!anyRenderbuffer && (srcIsDepth || dstIsDepth || srcStencil || dstStencil)) {
|
||||
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
|
||||
"DirectGLES CopyImageSubData only supports depth-only image copies.");
|
||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
|
||||
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
||||
BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
if (srcTexture->GetFormat() == TextureInternalFormat::R32F ||
|
||||
dstTexture->GetFormat() == TextureInternalFormat::R32F) {
|
||||
if (!anyRenderbuffer &&
|
||||
(srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) {
|
||||
// The single glGetError below decides the fallback dispatch, and
|
||||
// ErrorLopper::Clear is compiled out at the default log level - drain
|
||||
// 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(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dst.Name(), 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()),
|
||||
MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat),
|
||||
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
|
||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||
"DirectGLES color CopyImageSubData only supports single-layer copies.");
|
||||
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
|
||||
CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dst.Name(), dst.target, dstLevel, dst.x, dst.y);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearGLErrors();
|
||||
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,
|
||||
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dst.Name(), 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
|
||||
@@ -5838,6 +5985,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
|
||||
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
|
||||
return;
|
||||
}
|
||||
// The copy landed on the GPU. For a destination whose readback cannot be bit-exact the
|
||||
// CPU shadow is what glGetTexImage answers from, so it has to follow the same move -
|
||||
// otherwise it hands back whatever the level held before this copy.
|
||||
if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) {
|
||||
MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel,
|
||||
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7761,6 +7916,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
|
||||
const GLsizei sliceCount = std::max(size.z(), 1);
|
||||
const Bool multiSlice = size.z() > 1;
|
||||
// glGetTexImage answers with the STORED texels, and for a packed format whose encoding
|
||||
// is not unique the GPU route below cannot: it reads GL_RGBA/GL_FLOAT and re-encodes,
|
||||
// which canonicalizes an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000 - the same
|
||||
// value 8064, different words), and the conformance suite compares the words
|
||||
// ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT
|
||||
// decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the
|
||||
// shadow branch further down was unreachable. Serve the verbatim-word pairs from the
|
||||
// shadow first and keep the GPU attempts as the fallback for a level the shadow never
|
||||
// received. Every other format still prefers the GPU, so a rendered-into texture is
|
||||
// unaffected; RGB9_E5 is not colour-renderable, so its shadow stays authoritative -
|
||||
// and the one path that GPU-writes it, CopyImageSubData, mirrors itself into the
|
||||
// shadow for exactly this reason.
|
||||
const Bool verbatimPackedShadowRead =
|
||||
MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) &&
|
||||
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (verbatimPackedShadowRead &&
|
||||
GetTexImageViaShadowConversion(textureMipmapObject,
|
||||
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
|
||||
size.y(), sliceCount, format, type, pixels, applyPackImageParams)) {
|
||||
MGLOG_D("GetTexImage: finished via shadow conversion (verbatim packed words)");
|
||||
return;
|
||||
}
|
||||
// A multi-slice read used to go to the CPU shadow outright, on the grounds that the
|
||||
// scratch FBO can only expose one layer at a time. But the shadow only holds what was
|
||||
// uploaded, so every slice that was rendered to came back stale - which is exactly what
|
||||
|
||||
@@ -76,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height);
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
|
||||
@@ -2427,6 +2427,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return packedData.data();
|
||||
}
|
||||
|
||||
// "Some level of this texture holds an image", which is all the sync gate below actually
|
||||
// needs to know. Deliberately weaker than ITextureObject::IsComplete(): that predicate also
|
||||
// answers whether the texture SAMPLES as complete, so it must keep rejecting a chain with
|
||||
// undefined lower levels - but such a texture still has to be uploaded, or the level that
|
||||
// IS defined never reaches the driver at all.
|
||||
static Bool HasAnyDefinedMipmapLevel(const MG_State::GLState::ITextureObject* stateTextureObject) {
|
||||
const auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject);
|
||||
if (mipmapObject == nullptr) return false;
|
||||
const auto levelCount = mipmapObject->GetMipmapLevelCount();
|
||||
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
|
||||
for (Uint level = 0; level < levelCount; ++level) {
|
||||
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
if (levelTexelSize.x() > 0 && levelTexelSize.y() > 0 && levelTexelSize.z() > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BackendTextureObject::SyncMipmapsToBackend(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
if (!stateTextureObject) {
|
||||
@@ -2474,8 +2494,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 3. Size changed
|
||||
// 4. Mipmap levels changed
|
||||
|
||||
if (!stateTextureObject->IsComplete()) {
|
||||
MGLOG_D("Texture object with ID: %u is not complete, skipping sync.",
|
||||
// IsComplete() is the sampling predicate, and it calls a chain whose lower levels are
|
||||
// undefined incomplete - which is what a top-down build (upload level N, then level 0)
|
||||
// and ARB_clear_texture's conformance cases both produce. Bailing out on that shape
|
||||
// left the backend name with no levels whatsoever, so the level that WAS defined could
|
||||
// never be sampled or read back. Sync whenever some level holds an image; the per-level
|
||||
// loops below skip the degenerate ones individually.
|
||||
if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) {
|
||||
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
@@ -2577,6 +2603,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (auto& uploadTarget : uploadTargets) {
|
||||
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
|
||||
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
// A level the application never defined reads back as {0, 0, 0}; now that a
|
||||
// sparse chain is synced rather than skipped whole, leave those undefined on
|
||||
// the driver instead of giving the name a 0x0 image at that index.
|
||||
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) {
|
||||
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
|
||||
continue;
|
||||
}
|
||||
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
|
||||
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
|
||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
@@ -2804,6 +2837,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (auto& uploadTarget : uploadTargets) {
|
||||
for (SizeT level = 0; level < mipmapCount; ++level) {
|
||||
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
// See the append-mips loop: an undefined level stays undefined on the
|
||||
// driver rather than becoming a 0x0 image.
|
||||
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 ||
|
||||
levelTexelSize.z() <= 0) {
|
||||
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
|
||||
continue;
|
||||
}
|
||||
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
|
||||
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
|
||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
|
||||
@@ -632,15 +632,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
||||
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
|
||||
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
void GenerateMipmap(GLenum target) {
|
||||
|
||||
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height);
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
|
||||
@@ -8869,7 +8869,7 @@ void main() {
|
||||
// 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 {
|
||||
struct CopyImageSliceMapping {
|
||||
// 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.
|
||||
@@ -8883,13 +8883,35 @@ void main() {
|
||||
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) {
|
||||
// The Vulkan image one glCopyImageSubData endpoint names, after the two object kinds GL
|
||||
// 4.6 core 18.3.2 allows have been collapsed onto the fields this copy reads. A
|
||||
// renderbuffer is a single-level, single-layer 2D image, so its shape answers are
|
||||
// constants rather than a mip walk. `trackedLayout` points AT the owning resource's own
|
||||
// layout field - both resource maps are node-based, so the pointer survives the further
|
||||
// lookups the clear materialization below makes.
|
||||
struct CopyImageVkImage {
|
||||
Bool isRenderbuffer = false;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkImageLayout* trackedLayout = nullptr;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
Uint32 mipLevels = 1;
|
||||
VkExtent2D extent = {0, 0};
|
||||
Uint32 depth = 1;
|
||||
Uint32 arrayLayers = 1;
|
||||
};
|
||||
|
||||
Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel,
|
||||
GLint glZ, GLsizei glDepth, CopyImageSliceMapping& outMapping) {
|
||||
if (glZ < 0 || glDepth <= 0) {
|
||||
return false;
|
||||
}
|
||||
const Uint32 baseSlice = static_cast<Uint32>(glZ);
|
||||
if (image.isRenderbuffer) {
|
||||
// A renderbuffer holds one 2D image and nothing else; GL still requires the
|
||||
// z/depth pair and it can only name that one slice.
|
||||
outMapping = {};
|
||||
return baseSlice == 0 && glDepth == 1;
|
||||
}
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::Texture2D:
|
||||
@@ -8897,12 +8919,12 @@ void main() {
|
||||
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 = {};
|
||||
outMapping = {};
|
||||
return baseSlice == 0 && glDepth == 1;
|
||||
case TextureTarget::Texture3D:
|
||||
outEndpoint.slicesAreDepth = true;
|
||||
outEndpoint.baseSlice = baseSlice;
|
||||
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel);
|
||||
outMapping.slicesAreDepth = true;
|
||||
outMapping.baseSlice = baseSlice;
|
||||
outMapping.availableSlices = std::max(1u, image.depth >> mipLevel);
|
||||
return true;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
@@ -8911,9 +8933,9 @@ void main() {
|
||||
// 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;
|
||||
outMapping.slicesAreDepth = false;
|
||||
outMapping.baseSlice = baseSlice;
|
||||
outMapping.availableSlices = image.arrayLayers;
|
||||
return true;
|
||||
default:
|
||||
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
|
||||
@@ -8923,15 +8945,20 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) {
|
||||
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex();
|
||||
return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void VulkanRenderer::CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void VulkanRenderer::CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dstEndpoint,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr,
|
||||
"CopyImageSubData requires valid source and destination textures.");
|
||||
MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(),
|
||||
"CopyImageSubData requires valid source and destination images.");
|
||||
// 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.
|
||||
@@ -8948,9 +8975,9 @@ void main() {
|
||||
// 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());
|
||||
if (srcEndpoint.Texture == dstEndpoint.Texture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
|
||||
MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__,
|
||||
CopyImageEndpointName(srcEndpoint));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8963,8 +8990,42 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
auto* srcResource = m_textureManager->SyncTextureAndGetDescriptor(*srcTexture);
|
||||
auto* dstResource = m_textureManager->SyncTextureAndGetDescriptor(*dstTexture);
|
||||
// One resolver for both object kinds. The texture arm is the same
|
||||
// SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the
|
||||
// render-pass manager, which is where a renderbuffer's VkImage lives.
|
||||
const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) {
|
||||
if (endpoint.IsRenderbuffer()) {
|
||||
auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer);
|
||||
if (resource == nullptr) return false;
|
||||
out.isRenderbuffer = true;
|
||||
out.image = resource->image;
|
||||
out.trackedLayout = &resource->layout;
|
||||
out.aspect = resource->aspect;
|
||||
out.mipLevels = 1;
|
||||
out.extent = resource->extent;
|
||||
out.depth = 1;
|
||||
out.arrayLayers = 1;
|
||||
return out.image != VK_NULL_HANDLE;
|
||||
}
|
||||
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
|
||||
// reaches here - but the assertion that says so is compiled out of a release build.
|
||||
if (endpoint.Texture == nullptr) return false;
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*endpoint.Texture);
|
||||
if (resource == nullptr) return false;
|
||||
out.isRenderbuffer = false;
|
||||
out.image = resource->image;
|
||||
out.trackedLayout = &resource->layout;
|
||||
out.aspect = resource->aspect;
|
||||
out.mipLevels = resource->mipLevels;
|
||||
out.extent = resource->extent;
|
||||
out.depth = resource->depth;
|
||||
out.arrayLayers = resource->arrayLayers;
|
||||
return true;
|
||||
};
|
||||
CopyImageVkImage srcImage{};
|
||||
CopyImageVkImage dstImage{};
|
||||
const Bool srcResolved = resolveImage(srcEndpoint, srcImage);
|
||||
const Bool dstResolved = resolveImage(dstEndpoint, dstImage);
|
||||
// Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in
|
||||
// a release build, which is where both observed failures happened - a null resource
|
||||
// dereferenced right below (lavapipe) and a mip level the VkImage does not have handed
|
||||
@@ -8980,29 +9041,29 @@ void main() {
|
||||
// The frontend validator (ValidateTextureLevelExists) is what produces the
|
||||
// GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap
|
||||
// up there declines a copy instead of taking the process down.
|
||||
if (srcResource == nullptr || dstResource == nullptr) {
|
||||
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
|
||||
if (!srcResolved || !dstResolved) {
|
||||
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
|
||||
return;
|
||||
}
|
||||
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcResource->mipLevels ||
|
||||
static_cast<Uint32>(dstLevel) >= dstResource->mipLevels) {
|
||||
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
|
||||
static_cast<Uint32>(dstLevel) >= dstImage.mipLevels) {
|
||||
MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__,
|
||||
srcLevel, srcResource->mipLevels, dstLevel, dstResource->mipLevels);
|
||||
srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
|
||||
return;
|
||||
}
|
||||
const VkImageAspectFlags copyAspectMask =
|
||||
srcResource->aspect & dstResource->aspect &
|
||||
srcImage.aspect & dstImage.aspect &
|
||||
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
MOBILEGL_ASSERT(copyAspectMask != 0 &&
|
||||
(srcResource->aspect & copyAspectMask) == srcResource->aspect &&
|
||||
(dstResource->aspect & copyAspectMask) == dstResource->aspect,
|
||||
(srcImage.aspect & copyAspectMask) == srcImage.aspect &&
|
||||
(dstImage.aspect & copyAspectMask) == dstImage.aspect,
|
||||
"CopyImageSubData source and destination aspects are incompatible.");
|
||||
const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel);
|
||||
const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel);
|
||||
const Uint32 srcMipWidth = std::max(1u, srcResource->extent.width >> srcMipLevel);
|
||||
const Uint32 srcMipHeight = std::max(1u, srcResource->extent.height >> srcMipLevel);
|
||||
const Uint32 dstMipWidth = std::max(1u, dstResource->extent.width >> dstMipLevel);
|
||||
const Uint32 dstMipHeight = std::max(1u, dstResource->extent.height >> dstMipLevel);
|
||||
const Uint32 srcMipWidth = std::max(1u, srcImage.extent.width >> srcMipLevel);
|
||||
const Uint32 srcMipHeight = std::max(1u, srcImage.extent.height >> srcMipLevel);
|
||||
const Uint32 dstMipWidth = std::max(1u, dstImage.extent.width >> dstMipLevel);
|
||||
const Uint32 dstMipHeight = std::max(1u, dstImage.extent.height >> dstMipLevel);
|
||||
// Promoted for the same reason as the level range above, and it is the same bug class:
|
||||
// a VkImageCopy whose region runs past the image is an out-of-bounds promise to the
|
||||
// driver, and the frontend does not check the region at all (there is a CTS sibling,
|
||||
@@ -9025,10 +9086,10 @@ void main() {
|
||||
// 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)) {
|
||||
CopyImageSliceMapping srcSlices;
|
||||
CopyImageSliceMapping dstSlices;
|
||||
if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) ||
|
||||
!TryResolveCopyImageSliceMapping(dstTextureTarget, dstImage, dstMipLevel, dstZ, srcDepth, dstSlices)) {
|
||||
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);
|
||||
@@ -9039,40 +9100,53 @@ void main() {
|
||||
// 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) {
|
||||
if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices ||
|
||||
dstSlices.baseSlice + copySliceCount > dstSlices.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);
|
||||
__func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.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());
|
||||
const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) {
|
||||
if (endpoint.IsRenderbuffer()) {
|
||||
return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer);
|
||||
}
|
||||
return MaterializePendingClearForTexture(frame.commandBuffer, *endpoint.Texture);
|
||||
};
|
||||
const Bool clearReady = materializeClear(srcEndpoint);
|
||||
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source objectId=%u",
|
||||
__func__, CopyImageEndpointName(srcEndpoint));
|
||||
// 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 Bool dstClearReady = materializeClear(dstEndpoint);
|
||||
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
|
||||
__func__, CopyImageEndpointName(dstEndpoint));
|
||||
|
||||
const VkImageLayout srcOriginalLayout = srcResource->layout;
|
||||
const VkImageLayout dstOriginalLayout = dstResource->layout;
|
||||
const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
|
||||
const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
|
||||
// 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) {
|
||||
// A renderbuffer settles on its ATTACHMENT layout instead: it is never sampled, and that is
|
||||
// the layout MaterializePendingClearForRenderbuffer leaves it in.
|
||||
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout, Bool isRenderbuffer) {
|
||||
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;
|
||||
const Bool depthStencil =
|
||||
(copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0;
|
||||
if (isRenderbuffer) {
|
||||
return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
|
||||
: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
}
|
||||
return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
|
||||
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
};
|
||||
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout);
|
||||
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout);
|
||||
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout, srcImage.isRenderbuffer);
|
||||
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer);
|
||||
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
@@ -9083,15 +9157,15 @@ void main() {
|
||||
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
|
||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
srcResource->aspect, 0, srcResource->mipLevels);
|
||||
srcImage.aspect, 0, srcImage.mipLevels);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
|
||||
srcCopyLayout = srcResource->layout;
|
||||
srcCopyLayout = *srcImage.trackedLayout;
|
||||
} else {
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
frame.commandBuffer, srcImage.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__);
|
||||
@@ -9103,15 +9177,15 @@ void main() {
|
||||
VkImageLayout dstCopyLayout = dstOriginalLayout;
|
||||
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
dstResource->aspect, 0, dstResource->mipLevels);
|
||||
dstImage.aspect, 0, dstImage.mipLevels);
|
||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
|
||||
dstCopyLayout = dstResource->layout;
|
||||
dstCopyLayout = *dstImage.trackedLayout;
|
||||
} else {
|
||||
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
frame.commandBuffer, dstImage.image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
|
||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
|
||||
@@ -9121,18 +9195,18 @@ void main() {
|
||||
// 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;
|
||||
const Bool copyCrossesDepthAxis = srcSlices.slicesAreDepth || dstSlices.slicesAreDepth;
|
||||
VkImageCopy copyRegion{};
|
||||
copyRegion.srcSubresource.aspectMask = copyAspectMask;
|
||||
copyRegion.srcSubresource.mipLevel = srcMipLevel;
|
||||
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer();
|
||||
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()};
|
||||
copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer();
|
||||
copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()};
|
||||
copyRegion.dstSubresource.aspectMask = copyAspectMask;
|
||||
copyRegion.dstSubresource.mipLevel = dstMipLevel;
|
||||
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer();
|
||||
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()};
|
||||
copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer();
|
||||
copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.dstOffset = {dstX, dstY, dstSlices.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 "
|
||||
@@ -9143,8 +9217,8 @@ void main() {
|
||||
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,
|
||||
srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, ©Region);
|
||||
|
||||
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
@@ -9152,14 +9226,14 @@ void main() {
|
||||
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
|
||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
|
||||
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, srcRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
|
||||
srcResource->aspect, 0, srcResource->mipLevels);
|
||||
srcImage.aspect, 0, srcImage.mipLevels);
|
||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
|
||||
} else {
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
|
||||
frame.commandBuffer, srcImage.image, srcCopyLayout, srcRestoreLayout,
|
||||
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__);
|
||||
@@ -9170,14 +9244,14 @@ void main() {
|
||||
GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask);
|
||||
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
|
||||
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, dstRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
|
||||
dstResource->aspect, 0, dstResource->mipLevels);
|
||||
dstImage.aspect, 0, dstImage.mipLevels);
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
|
||||
} else {
|
||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
|
||||
frame.commandBuffer, dstImage.image, dstCopyLayout, dstRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "VkTimerQueryManager.h"
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
@@ -197,9 +198,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLbitfield mask, GLenum filter);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
||||
GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dstEndpoint,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
|
||||
@@ -43,4 +43,5 @@ set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
||||
add_subdirectory(Program)
|
||||
add_subdirectory(Buffer)
|
||||
add_subdirectory(Driver)
|
||||
add_subdirectory(Container)
|
||||
add_subdirectory(Container)
|
||||
add_subdirectory(Transpile)
|
||||
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
|
||||
# Deliberately NOT a google-benchmark target: the interesting quantity is a per-stage
|
||||
# breakdown of one program build, which needs its own clock around sub-steps that share
|
||||
# set-up, and a plain main() keeps the output a table this can be read straight out of.
|
||||
add_executable(
|
||||
TranspileProfile
|
||||
TranspileProfile.cpp
|
||||
)
|
||||
|
||||
target_include_directories(TranspileProfile PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
TranspileProfile PRIVATE
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -691,6 +691,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return textureObject;
|
||||
}
|
||||
|
||||
// Whether a raw internalformat enum names a compressed format - the question GL asks whenever an
|
||||
// entry point is forbidden on a compressed image: glTexStorage3D on TEXTURE_3D (no
|
||||
// block-compressed format is defined for a three-dimensional image, so it is INVALID_OPERATION
|
||||
// rather than the INVALID_ENUM an unknown sized format gets - GL 4.6 core 8.19 / Khronos bug
|
||||
// 11239, KHR-GLxx.texture_storage.compressed_data) and the clear-texture pair (8.19 again).
|
||||
// Written against the enum ranges rather than a name list because the families are contiguous
|
||||
// and MobileGL's own internal-format enum drops the ones it cannot carry, which would make this
|
||||
// check silently narrower than the API surface.
|
||||
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
|
||||
switch (internalformat) {
|
||||
case 0x8225: // GL_COMPRESSED_RED
|
||||
case 0x8226: // GL_COMPRESSED_RG
|
||||
case 0x84ED: // GL_COMPRESSED_RGB
|
||||
case 0x84EE: // GL_COMPRESSED_RGBA
|
||||
case 0x8C48: // GL_COMPRESSED_SRGB
|
||||
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
|
||||
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
|
||||
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
|
||||
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
|
||||
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
|
||||
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
|
||||
}
|
||||
|
||||
namespace {
|
||||
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -726,6 +754,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture level {} is not defined.", level));
|
||||
return nullptr;
|
||||
}
|
||||
// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear
|
||||
// entry points. Two tags to ask, because they answer different questions: the stored
|
||||
// one covers a level glCompressedTexImage* or a SPECIFIC compressed internalformat
|
||||
// defined, the requested one covers the six generic GL_COMPRESSED_* enums that MobileGL
|
||||
// deliberately backs with uncompressed storage (see MipmapStorage) and that would
|
||||
// otherwise look like an ordinary RGBA8 image by the time the clear runs.
|
||||
const auto& uploadTargets = mipmapTexture->GetUploadTargets();
|
||||
if (!uploadTargets.empty() &&
|
||||
(mipmapTexture->GetMipmapCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) != GL_NONE ||
|
||||
mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) !=
|
||||
GL_NONE)) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||
"Compressed textures cannot be cleared.");
|
||||
return nullptr;
|
||||
}
|
||||
return mipmapTexture;
|
||||
}
|
||||
|
||||
@@ -2197,6 +2240,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
} else {
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
||||
// The same specific-compressed-format tag glTexImage2D records (see TexImage2D_State):
|
||||
// GL 4.6 core 8.5 commits the level to that format, so GL_TEXTURE_COMPRESSED and
|
||||
// GL_TEXTURE_INTERNAL_FORMAT must report it - and, less obviously, glCopyImageSubData
|
||||
// sizes the level's texel BLOCK from it. Without the tag a GL_COMPRESSED_RG_RGTC2
|
||||
// array level measured as the RG8 storage it resolved to, 2 bytes instead of 16, and
|
||||
// the copy-compatibility rule refused a pairing 18.3.2 requires. AllocateStorage above
|
||||
// clears the tag, so this has to follow it.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
|
||||
if (compressedInfo.blockWidth != 0) {
|
||||
textureMipmapObject->SetMipmapCompressedImage(
|
||||
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth}));
|
||||
}
|
||||
// Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
|
||||
// enums too, which the tag above deliberately skips - glClearTexImage has to refuse
|
||||
// them all (GL 4.6 core 8.19).
|
||||
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
|
||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
|
||||
static_cast<GLenum>(internalformat));
|
||||
}
|
||||
}
|
||||
|
||||
if (!originalPixels) {
|
||||
@@ -2343,6 +2406,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
|
||||
}
|
||||
// Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
|
||||
// enums too, which the tag above deliberately skips - glClearTexImage has to refuse
|
||||
// them all (GL 4.6 core 8.19).
|
||||
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
|
||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
|
||||
static_cast<GLenum>(internalformat));
|
||||
}
|
||||
}
|
||||
|
||||
if (!originalPixels) {
|
||||
@@ -2431,6 +2501,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!isProxy) {
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
|
||||
// After AllocateStorage, which clears the tag. No block-compressed format has a 1D
|
||||
// layout, so only the specific-format tag the 2D/3D paths record is skipped here - the
|
||||
// request itself still has to be remembered for glClearTexImage (GL 4.6 core 8.19).
|
||||
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalFormat))) {
|
||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
|
||||
static_cast<GLenum>(internalFormat));
|
||||
}
|
||||
}
|
||||
|
||||
if (!originalPixels) {
|
||||
@@ -3412,9 +3489,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
|
||||
void CopyImageSubData_Backend(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData_Backend(const MG_Backend::CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const MG_Backend::CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
|
||||
@@ -3425,7 +3502,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support image-to-image copies."));
|
||||
return;
|
||||
}
|
||||
copyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, dstX,
|
||||
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
@@ -3472,9 +3549,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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,
|
||||
Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint,
|
||||
const char* endpointName) {
|
||||
if (textureObject) return true;
|
||||
if (endpoint.Exists()) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -3498,21 +3575,106 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||
// ---- The questions ValidateCopyImageSubData_State asks of one endpoint. ---------------
|
||||
// A renderbuffer answers all of them directly: it has exactly one image, no mip chain and
|
||||
// no sampler state, and it carries its own internal format and extent.
|
||||
|
||||
Int GetCopyImageEndpointSamples(const MG_Backend::CopyImageEndpoint& endpoint) {
|
||||
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetSamples();
|
||||
return endpoint.Texture->GetSamples();
|
||||
}
|
||||
|
||||
TextureInternalFormat GetCopyImageEndpointFormat(const MG_Backend::CopyImageEndpoint& endpoint) {
|
||||
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
|
||||
return endpoint.Texture->GetFormat();
|
||||
}
|
||||
|
||||
// A renderbuffer has level 0 and nothing else, and the failure is the same INVALID_VALUE
|
||||
// ValidateTextureLevelExists records for a level a texture does not have.
|
||||
Bool ValidateCopyImageEndpointLevelExists(const MG_Backend::CopyImageEndpoint& endpoint, GLint level,
|
||||
const char* caller) {
|
||||
if (!endpoint.IsRenderbuffer()) {
|
||||
return TextureImpl::ValidateTextureLevelExists(endpoint.Texture, level, caller);
|
||||
}
|
||||
if (level == 0) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "A renderbuffer has only level 0."));
|
||||
return false;
|
||||
}
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
|
||||
if (!TextureImpl::ValidateTextureTarget(srcTextureTarget) ||
|
||||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||
|
||||
// Targets with no mip chain have q == level_base by definition (GL 4.6 core 8.17), so no
|
||||
// minification filter can make them mipmap incomplete - while the shared predicate derives
|
||||
// q from the base level's size alone and would call a 16x16 multisample image incomplete.
|
||||
Bool CopyImageTargetHasMipmapChain(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::TextureBuffer:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsCopyImageEndpointComplete(const MG_Backend::CopyImageEndpoint& endpoint) {
|
||||
// A renderbuffer is complete exactly when it has storage - there is nothing else it
|
||||
// could be missing.
|
||||
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->IsAllocated();
|
||||
const auto* texture = endpoint.Texture.get();
|
||||
if (!texture) return false;
|
||||
// 18.3.2 asks for TEXTURE completeness, which GL 4.6 core 8.17 defines to include the
|
||||
// MIP CHAIN whenever the minification filter samples it - and ITextureObject::
|
||||
// IsComplete() only answers the storage half (an internal format, and no zero-size
|
||||
// level in the middle of the chain). A texture with level 0 alone and the default
|
||||
// NEAREST_MIPMAP_LINEAR filter is incomplete, which is exactly how
|
||||
// KHR-GL43.copy_image.incomplete_tex builds its subject.
|
||||
//
|
||||
// The filter is the texture's OWN: copy-image never goes through a texture unit, so no
|
||||
// sampler object is in play. An immutable texture is unaffected - glTexStorage clamps
|
||||
// TEXTURE_MAX_LEVEL to levels-1, which is what makes a single-level immutable texture
|
||||
// mipmap complete under any filter.
|
||||
const auto& sampler = texture->GetSamplerObject();
|
||||
const Bool mipmapped = CopyImageTargetHasMipmapChain(texture->GetTarget()) && sampler &&
|
||||
sampler->GetMipmapMode() != SamplerMipmapMode::None;
|
||||
return MG_State::GLState::IsMipmapCompleteForFilter(texture, mipmapped);
|
||||
}
|
||||
|
||||
GLenum GetCopyImageEndpointCompressedFormat(const MG_Backend::CopyImageEndpoint& endpoint,
|
||||
TextureUploadTarget uploadTarget, GLint level) {
|
||||
if (endpoint.IsRenderbuffer()) return GL_NONE;
|
||||
return GetCompressedLevelFormat(endpoint.Texture, uploadTarget, level);
|
||||
}
|
||||
|
||||
IntVec3 GetCopyImageEndpointLevelSize(const MG_Backend::CopyImageEndpoint& endpoint,
|
||||
TextureUploadTarget uploadTarget, GLint level) {
|
||||
if (endpoint.IsRenderbuffer()) {
|
||||
return {endpoint.Renderbuffer->GetWidth(), endpoint.Renderbuffer->GetHeight(), 1};
|
||||
}
|
||||
return GetCopyImageLevelSize(endpoint.Texture, uploadTarget, level);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const MG_Backend::CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!ValidateCopyImageObjectExists(src, "source") ||
|
||||
!ValidateCopyImageObjectExists(dst, "destination")) {
|
||||
return false;
|
||||
}
|
||||
// GL_RENDERBUFFER has no TextureTarget to convert to, and it needs none: it is its own
|
||||
// whole-image target, and the endpoint that carries it was resolved from the renderbuffer
|
||||
// namespace, so it matches its object by construction.
|
||||
const auto srcTextureTarget =
|
||||
src.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
const auto dstTextureTarget =
|
||||
dst.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
|
||||
if ((!src.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(srcTextureTarget)) ||
|
||||
(!dst.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(dstTextureTarget))) {
|
||||
return false;
|
||||
}
|
||||
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||
@@ -3520,8 +3682,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
|
||||
if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||
@@ -3535,8 +3697,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
|
||||
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
|
||||
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
|
||||
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
|
||||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
|
||||
if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) ||
|
||||
!ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) {
|
||||
return false;
|
||||
}
|
||||
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
|
||||
@@ -3552,37 +3714,41 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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()) {
|
||||
const Int srcSamples = GetCopyImageEndpointSamples(src);
|
||||
const Int dstSamples = GetCopyImageEndpointSamples(dst);
|
||||
if (srcSamples != dstSamples) {
|
||||
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())));
|
||||
srcSamples, dstSamples)));
|
||||
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()) {
|
||||
const Bool srcComplete = IsCopyImageEndpointComplete(src);
|
||||
const Bool dstComplete = IsCopyImageEndpointComplete(dst);
|
||||
if (!srcComplete || !dstComplete) {
|
||||
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())));
|
||||
srcComplete, dstComplete)));
|
||||
return false;
|
||||
}
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture);
|
||||
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
|
||||
GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel));
|
||||
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
|
||||
GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel));
|
||||
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
||||
return false;
|
||||
}
|
||||
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
|
||||
const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel);
|
||||
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
||||
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
||||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
||||
@@ -4098,8 +4264,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For a cube map this is exactly cube completeness: IsComplete() wants all six faces.
|
||||
if (!textureObject->IsComplete()) {
|
||||
// GL 4.6 core 8.11.4 names cube completeness as the only completeness a readback requires,
|
||||
// and for a cube map that is exactly what IsComplete() answers (all six faces defined at
|
||||
// every level). It must not speak for any other target: on a mip chain it also rejects
|
||||
// "level N defined, the levels below it not", which is a perfectly readable texture at
|
||||
// level N - and the shape glClearTexImage's conformance cases build, since they define
|
||||
// only the level they clear. The requested level's own existence is checked below.
|
||||
if ((target == TextureTarget::TextureCubeMap || target == TextureTarget::TextureCubeMapArray) &&
|
||||
!textureObject->IsComplete()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete"));
|
||||
@@ -4131,8 +4303,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
// Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch,
|
||||
// integer-ness). Also rejects STENCIL_INDEX readback, which needs GL_ARB_texture_stencil8
|
||||
// (not advertised by MobileGL).
|
||||
// integer-ness). Also rejects a STENCIL_INDEX readback of anything but stencil-only
|
||||
// storage, which is the only pairing GL 4.4 / ARB_texture_stencil8 ever made legal.
|
||||
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
|
||||
textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
|
||||
return false;
|
||||
@@ -4158,33 +4330,48 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto* textureMipmapObject =
|
||||
static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
const auto& uploadTargets = textureObject->GetUploadTargets();
|
||||
if (!uploadTargets.empty() && static_cast<Uint>(level) < textureMipmapObject->GetMipmapLevelCount()) {
|
||||
// Tightly packed, and summed over every face because a cube map query returns all
|
||||
// six. Pack pixel-store state only ever grows this, so a request rejected here
|
||||
// could not have fit under any packing.
|
||||
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
|
||||
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
|
||||
texturePixelDataType, texelSize) *
|
||||
uploadTargets.size();
|
||||
// The half of the completeness gate above that GL does keep: the REQUESTED level has
|
||||
// to hold an image. A name that was never given one carries no levels at all (which is
|
||||
// also what an Unknown internal format answers), and a chain grown to reach level N
|
||||
// leaves every level below it at {0, 0, 0}.
|
||||
if (uploadTargets.empty() || static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
|
||||
return false;
|
||||
}
|
||||
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
|
||||
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
|
||||
// Tightly packed, and summed over every face because a cube map query returns all
|
||||
// six. Pack pixel-store state only ever grows this, so a request rejected here
|
||||
// could not have fit under any packing.
|
||||
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
|
||||
texturePixelDataType, texelSize) *
|
||||
uploadTargets.size();
|
||||
|
||||
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT bufferSize = pixelPackBufferObject->GetSize();
|
||||
const SizeT offset = reinterpret_cast<SizeT>(pixels);
|
||||
if (offset > bufferSize || required > bufferSize - offset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Packing would write past the end of the pixel pack buffer."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT bufferSize = pixelPackBufferObject->GetSize();
|
||||
const SizeT offset = reinterpret_cast<SizeT>(pixels);
|
||||
if (offset > bufferSize || required > bufferSize - offset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Packing would write past the end of the pixel pack buffer."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4419,6 +4606,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||
if (IsCompressedGLInternalFormat(internalformat)) {
|
||||
// After AllocateStorage, which clears the tag. See TexImage1D_State: no compressed
|
||||
// format has a 1D block layout, but glClearTexImage still has to refuse the request.
|
||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
|
||||
static_cast<Uint>(level), internalformat);
|
||||
}
|
||||
}
|
||||
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
|
||||
// longer pre-existing chain has to be dropped explicitly.
|
||||
@@ -4487,6 +4680,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
|
||||
{levelWidth, levelHeight, 1}));
|
||||
}
|
||||
if (IsCompressedGLInternalFormat(internalformat)) {
|
||||
// Also after AllocateStorage. The generic enums land here and nowhere above,
|
||||
// and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
|
||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(uploadTarget,
|
||||
static_cast<Uint>(level), internalformat);
|
||||
}
|
||||
}
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
|
||||
@@ -4494,32 +4693,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
// No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on
|
||||
// TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown
|
||||
// sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage
|
||||
// .compressed_data). Written against the enum ranges rather than a name list because the
|
||||
// families are contiguous and MobileGL's own internal-format enum drops the ones it cannot
|
||||
// carry, which would make this check silently narrower than the API surface.
|
||||
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
|
||||
switch (internalformat) {
|
||||
case 0x8225: // GL_COMPRESSED_RED
|
||||
case 0x8226: // GL_COMPRESSED_RG
|
||||
case 0x84ED: // GL_COMPRESSED_RGB
|
||||
case 0x84EE: // GL_COMPRESSED_RGBA
|
||||
case 0x8C48: // GL_COMPRESSED_SRGB
|
||||
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
|
||||
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
|
||||
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
|
||||
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
|
||||
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
|
||||
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
|
||||
}
|
||||
|
||||
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
GLsizei depth) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
@@ -4562,6 +4735,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Array targets keep their layer count constant across levels; only true 3D
|
||||
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
|
||||
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
|
||||
// The same specific-compressed-format tag glTexStorage2D records, for the array targets a
|
||||
// compressed glTexStorage3D is legal on (GL_TEXTURE_3D was refused above). Zero width means
|
||||
// a generic format, which MobileGL answers with uncompressed storage, so it is not tagged.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
|
||||
for (GLsizei level = 0; level < levels; ++level) {
|
||||
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
|
||||
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
|
||||
@@ -4571,6 +4748,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||
{{levelWidth, levelHeight, levelDepth}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||
if (compressedInfo.blockWidth != 0) {
|
||||
// After AllocateStorage, which clears the tag.
|
||||
textureMipmapObject->SetMipmapCompressedImage(
|
||||
textureUploadTarget, static_cast<Uint>(level), internalformat, nullptr,
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
|
||||
{levelWidth, levelHeight, levelDepth}));
|
||||
}
|
||||
if (IsCompressedGLInternalFormat(internalformat)) {
|
||||
// Also after AllocateStorage. The generic enums land here and nowhere above,
|
||||
// and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
|
||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
|
||||
static_cast<Uint>(level), internalformat);
|
||||
}
|
||||
}
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
@@ -5780,17 +5970,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei 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
|
||||
// INVALID_OPERATION - so resolve through the plain lookups, which answer 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,
|
||||
//
|
||||
// The TARGET picks the namespace: GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER, and a
|
||||
// renderbuffer name has nothing to do with a texture name. Resolving both through
|
||||
// GetTextureObject made every renderbuffer endpoint INVALID_VALUE - or, when the number
|
||||
// happened to collide with a live texture, INVALID_ENUM from the target check.
|
||||
const auto resolveEndpoint = [](GLuint name, GLenum target) {
|
||||
MG_Backend::CopyImageEndpoint endpoint{};
|
||||
if (target == GL_RENDERBUFFER) {
|
||||
endpoint.Renderbuffer = MG_State::pGLContext->GetRenderbufferObject(name);
|
||||
} else {
|
||||
endpoint.Texture = MG_State::pGLContext->GetTextureObject(name);
|
||||
}
|
||||
return endpoint;
|
||||
};
|
||||
const MG_Backend::CopyImageEndpoint src = resolveEndpoint(srcName, srcTarget);
|
||||
const MG_Backend::CopyImageEndpoint dst = resolveEndpoint(dstName, dstTarget);
|
||||
if (!ValidateCopyImageSubData_State(src, srcTarget, srcLevel, srcX, srcY, dst, dstTarget,
|
||||
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||
return;
|
||||
}
|
||||
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
||||
CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
|
||||
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
|
||||
@@ -313,9 +313,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
|
||||
if (format == TextureInputFormat::StencilIndex) {
|
||||
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
|
||||
// The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
|
||||
// pairs with stencil-only storage: against a depth, depth-stencil or colour internal format
|
||||
// STENCIL_INDEX keeps the pre-4.4 answer (GL CTS packed_pixels feeds exactly that pairing
|
||||
// and expects INVALID_OPERATION).
|
||||
if (format == TextureInputFormat::StencilIndex &&
|
||||
internalFormat != TextureInternalFormat::StencilIndex8) {
|
||||
return recordInvalidOperation("STENCIL_INDEX requires a stencil-only internal format");
|
||||
}
|
||||
|
||||
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
|
||||
|
||||
@@ -247,6 +247,19 @@ endif()
|
||||
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
|
||||
if (MOBILEGL_ITEST_VK_ICD)
|
||||
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}")
|
||||
# The three iterationRP repairs are tri-state quirks that default to device
|
||||
# auto-detection, and lavapipe is not on any auto list - so on lavapipe the
|
||||
# iterationRP scenarios run unrepaired and Program 203 misses its golden
|
||||
# output. CI's integration-gpu job exports these three by hand; pinning them
|
||||
# to the ICD instead means a local `ctest -L integration-gpu` measures the
|
||||
# same thing the gate does, with no environment to remember.
|
||||
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
|
||||
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
|
||||
list(APPEND MGL_ITEST_VULKAN_ENV
|
||||
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
|
||||
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
|
||||
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace MobileGL {
|
||||
m_dirtyRects.resize(requiredLevelCount);
|
||||
m_compressedData.resize(requiredLevelCount);
|
||||
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||
m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||
}
|
||||
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
@@ -79,6 +80,9 @@ namespace MobileGL {
|
||||
m_compressedFormats[level] = GL_NONE;
|
||||
m_compressedData[level].clear();
|
||||
m_compressedData[level].shrink_to_fit();
|
||||
// Same story for the requested-format tag: a respecified level is whatever this
|
||||
// call asked for, and the compressed entry points re-arm it right afterwards.
|
||||
m_requestedCompressedFormats[level] = GL_NONE;
|
||||
}
|
||||
|
||||
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
|
||||
@@ -110,6 +114,16 @@ namespace MobileGL {
|
||||
return m_compressedData[level].data();
|
||||
}
|
||||
|
||||
void MipmapStorage::SetRequestedCompressedFormat(Uint level, GLenum internalFormat) {
|
||||
if (level >= m_requestedCompressedFormats.size()) return;
|
||||
m_requestedCompressedFormats[level] = internalFormat;
|
||||
}
|
||||
|
||||
GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const {
|
||||
if (level >= m_requestedCompressedFormats.size()) return GL_NONE;
|
||||
return m_requestedCompressedFormats[level];
|
||||
}
|
||||
|
||||
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
|
||||
if (levelCount >= m_data.size()) return;
|
||||
|
||||
@@ -120,6 +134,7 @@ namespace MobileGL {
|
||||
m_dirtyRects.resize(levelCount);
|
||||
m_compressedData.resize(levelCount);
|
||||
m_compressedFormats.resize(levelCount);
|
||||
m_requestedCompressedFormats.resize(levelCount);
|
||||
}
|
||||
|
||||
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
||||
|
||||
@@ -96,6 +96,18 @@ namespace MobileGL {
|
||||
SizeT GetCompressedByteSize(Uint level) const;
|
||||
const void* MapCompressedData(Uint level) const;
|
||||
|
||||
// The compressed internalformat the application ASKED for, which is not the same
|
||||
// question as the one above: the six generic GL_COMPRESSED_* enums let the
|
||||
// implementation choose, MobileGL chooses uncompressed storage, and the level is
|
||||
// deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and
|
||||
// glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry
|
||||
// points that must refuse a compressed image outright (glClearTexImage /
|
||||
// glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is
|
||||
// recorded separately. Set right after AllocateLevel, which clears it.
|
||||
void SetRequestedCompressedFormat(Uint level, GLenum internalFormat);
|
||||
// GL_NONE when the level was not requested with a compressed internalformat.
|
||||
GLenum GetRequestedCompressedFormat(Uint level) const;
|
||||
|
||||
protected:
|
||||
// Insert one clamped, non-empty write box, keeping the list disjoint
|
||||
// and bounded (see kMaxDirtyRects).
|
||||
@@ -115,6 +127,7 @@ namespace MobileGL {
|
||||
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
|
||||
Vector<Vector<Uint8>> m_compressedData;
|
||||
Vector<GLenum> m_compressedFormats;
|
||||
Vector<GLenum> m_requestedCompressedFormats;
|
||||
};
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -111,6 +111,16 @@ namespace MobileGL {
|
||||
return m_storage[targetIndex].MapCompressedData(level);
|
||||
}
|
||||
|
||||
void SetRequestedCompressedFormat(Uint targetIndex, Uint level, GLenum internalFormat) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetRequestedCompressedFormat: target invalid");
|
||||
m_storage[targetIndex].SetRequestedCompressedFormat(level, internalFormat);
|
||||
}
|
||||
|
||||
GLenum GetRequestedCompressedFormat(Uint targetIndex, Uint level) const {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetRequestedCompressedFormat: target invalid");
|
||||
return m_storage[targetIndex].GetRequestedCompressedFormat(level);
|
||||
}
|
||||
|
||||
protected:
|
||||
Array<MipmapStorage, TargetCount> m_storage;
|
||||
};
|
||||
|
||||
@@ -373,6 +373,18 @@ namespace MobileGL {
|
||||
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel, GLenum internalFormat) {
|
||||
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
internalFormat);
|
||||
}
|
||||
|
||||
GLenum TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
|
||||
mipmapLevel);
|
||||
}
|
||||
|
||||
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
|
||||
if (m_textureStorage.GetLevelCount() == 0) {
|
||||
return {0, 0, 0};
|
||||
|
||||
@@ -220,6 +220,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
|
||||
// The compressed internalformat the level was REQUESTED with, recorded even when MobileGL
|
||||
// answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see
|
||||
// MipmapStorage. Only the entry points GL forbids on a compressed image read it.
|
||||
virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat) = 0;
|
||||
// GL_NONE when the level was not requested with a compressed internalformat.
|
||||
virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const = 0;
|
||||
};
|
||||
|
||||
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
|
||||
@@ -286,6 +295,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat) override;
|
||||
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
IntVec3 GetBaseSize() const override;
|
||||
Bool IsComplete() const override;
|
||||
|
||||
@@ -96,6 +96,18 @@ namespace MobileGL {
|
||||
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel, GLenum internalFormat) {
|
||||
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
internalFormat);
|
||||
}
|
||||
|
||||
GLenum TextureObject2DCube::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
|
||||
mipmapLevel);
|
||||
}
|
||||
|
||||
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
|
||||
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
|
||||
target <= TextureUploadTarget::CubeMapNegativeZ,
|
||||
|
||||
@@ -39,6 +39,10 @@ namespace MobileGL {
|
||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const override;
|
||||
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat) override;
|
||||
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const override;
|
||||
|
||||
IntVec3 GetBaseSize() const override;
|
||||
Bool IsComplete() const override;
|
||||
|
||||
@@ -377,6 +377,40 @@ TEST_F(TextureTest, ClearTexImageErrorContracts) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_ENUM));
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear entry points.
|
||||
// The generic GL_COMPRESSED_* enums are the half that needs its own tag - MobileGL answers them
|
||||
// with uncompressed storage on purpose, so by the time the clear runs the level looks like any
|
||||
// other RGBA8 image unless the REQUEST was recorded alongside it.
|
||||
TEST_F(TextureTest, ClearTexImageRejectsCompressedTextures) {
|
||||
GLuint genericTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &genericTexture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, genericTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::ClearTexImage(genericTexture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
MG_Impl::GLImpl::ClearTexSubImage(genericTexture, 0, 0, 0, 0, 4, 4, 1, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// A specific compressed internalformat is refused through the tag the level already carried...
|
||||
GLuint specificTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &specificTexture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, specificTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// ...and respecifying the level with an uncompressed format makes it clearable again, because
|
||||
// AllocateStorage clears both tags.
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R8, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
|
||||
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
|
||||
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
|
||||
@@ -1023,6 +1057,32 @@ TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL_STENCIL_INDEX is the unsized base format for stencil-only storage, and refusing it as an
|
||||
// internal format killed the ARB_clear_texture stencil case in its own setup - before it could
|
||||
// reach the calls it actually tests. The stencil-only transfer format stays paired with
|
||||
// stencil-only storage in both directions, which is what keeps those clears erroring.
|
||||
TEST_F(TextureTest, StencilIndexIsATextureInternalFormatPairedOnlyWithStencilStorage) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_STENCIL_INDEX, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
ASSERT_NE(textureObject, nullptr);
|
||||
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::StencilIndex8);
|
||||
|
||||
// A colour transfer format against stencil storage is still INVALID_OPERATION, so the clear
|
||||
// the conformance case makes next fails the way it is supposed to.
|
||||
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// ...and the other direction: GL_STENCIL_INDEX against colour storage stays illegal.
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// Desktop GL table 3.3 lists GREEN and BLUE as TexImage client formats (GL CTS packed_pixels
|
||||
// rgba8_format_green/blue upload with them and verify the readback): the single input component
|
||||
// feeds the named channel, the other color channels default to 0 and alpha to 1.
|
||||
@@ -1317,6 +1377,56 @@ TEST_F(TextureTest, GetTextureImageReadsNamedObjectWithoutBinding) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.11.4 asks a readback for cube completeness and nothing else, so a mip chain whose
|
||||
// levels BELOW the requested one were never defined is still readable at that level - which is
|
||||
// exactly the shape ARB_clear_texture's conformance cases build (they define only the level they
|
||||
// clear). The whole-chain completeness gate used to answer INVALID_OPERATION here.
|
||||
TEST_F(TextureTest, GetTexImageReadsALevelWhoseLowerLevelsWereNeverDefined) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
const Uint8 pixels[] = {
|
||||
61, 62, 63, 64,
|
||||
71, 72, 73, 74,
|
||||
};
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
Uint8 output[sizeof(pixels)] = {};
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 2, GL_RGBA, GL_UNSIGNED_BYTE, output);
|
||||
|
||||
EXPECT_EQ(std::memcmp(output, pixels, sizeof(pixels)), 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The other half of the same rule: loosening the chain-wide check must not let a level that holds
|
||||
// no image at all through. Level 0 exists as a chain slot once level 2 is defined, but nothing ever
|
||||
// gave it an image, so it stays INVALID_OPERATION - as does a level past the end of the chain and a
|
||||
// texture that was never given any image whatsoever.
|
||||
TEST_F(TextureTest, GetTexImageStillRejectsALevelThatHoldsNoImage) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
Uint8 output[4] = {};
|
||||
|
||||
// No image at all yet: the chain carries no levels.
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Inside the chain, but never defined.
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// Past the end of the chain.
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 3, GL_RGBA, GL_UNSIGNED_BYTE, output);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, GetTextureSubImageReadsFullNamedLevelWithoutBinding) {
|
||||
GLuint texture = 0;
|
||||
GLuint boundTexture = 0;
|
||||
@@ -1651,6 +1761,56 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same rule for the 3D entry points, which never recorded the tag at all. Besides the two
|
||||
// level queries this decides the level's texel BLOCK SIZE, which glCopyImageSubData compares
|
||||
// against the other endpoint's - an untagged GL_COMPRESSED_RG_RGTC2 array level measured as the
|
||||
// RG8 storage it resolves to, 2 bytes instead of 16.
|
||||
TEST_F(TextureTest, TexImage3DAndTexStorage3DTagASpecificCompressedInternalFormat) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 2, 0, GL_RG,
|
||||
GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint compressed = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed);
|
||||
EXPECT_EQ(compressed, GL_TRUE);
|
||||
|
||||
GLint internalFormat = 0;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
|
||||
EXPECT_EQ(internalFormat, static_cast<GLint>(GL_COMPRESSED_RG_RGTC2));
|
||||
|
||||
// 8x8 in 4x4 blocks of 16 bytes each is 64 bytes a layer, and both layers count.
|
||||
GLint imageSize = 0;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
|
||||
EXPECT_EQ(imageSize, 128);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The texel shadow behind the tag keeps the uncompressed storage the format resolves to.
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
ASSERT_NE(textureObject, nullptr);
|
||||
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::RG8);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
// glTexStorage3D has the same gap and the same fix; immutable storage plus
|
||||
// glCompressedTexSubImage3D is the modern way to upload a compressed array texture.
|
||||
GLuint storageTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &storageTexture);
|
||||
MG_Impl::GLImpl::TextureStorage3D(storageTexture, 1, GL_COMPRESSED_RG_RGTC2, 8, 8, 2);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, storageTexture);
|
||||
|
||||
compressed = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed);
|
||||
EXPECT_EQ(compressed, GL_TRUE);
|
||||
imageSize = 0;
|
||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
|
||||
EXPECT_EQ(imageSize, 128);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
|
||||
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
|
||||
@@ -3320,6 +3480,29 @@ TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts)
|
||||
TexturePixelDataType::UnsignedInt5999Rev));
|
||||
}
|
||||
|
||||
TEST(SharedExponentRGB9E5Test, RedundantPackedEncodingIsRGB9E5Only) {
|
||||
using MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding;
|
||||
|
||||
// This is the predicate that decides whether the CPU shadow has to answer glGetTexImage
|
||||
// instead of a GPU readback, so it must be as narrow as the defect: only the shared exponent
|
||||
// has several legal encodings of one value.
|
||||
EXPECT_TRUE(HasRedundantPackedEncoding(TextureInternalFormat::RGB9E5));
|
||||
|
||||
// The other three packed 32-bit layouts round-trip through float32 bit-exactly (each field is
|
||||
// either an integer or a unique float encoding), so a GPU readback still serves them - which
|
||||
// matters because RGB10_A2 and R11F_G11F_B10F ARE colour-renderable and their shadow can
|
||||
// legitimately be stale.
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2UI));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::R11FG11FB10F));
|
||||
|
||||
// Nothing unpacked qualifies, and neither does an unknown format.
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA8));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA32F));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB8));
|
||||
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::Unknown));
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -4100,24 +4283,25 @@ namespace {
|
||||
GLint SrcZ = -1;
|
||||
GLint DstZ = -1;
|
||||
GLsizei Depth = -1;
|
||||
Bool SrcIsRenderbuffer = false;
|
||||
Bool DstIsRenderbuffer = false;
|
||||
} g_copyImageSubDataCall;
|
||||
|
||||
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
|
||||
void RecordCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget,
|
||||
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
|
||||
const MG_Backend::CopyImageEndpoint& dst, 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};
|
||||
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ,
|
||||
dstZ, srcDepth, src.IsRenderbuffer(), dst.IsRenderbuffer()};
|
||||
}
|
||||
|
||||
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
|
||||
@@ -4354,9 +4538,13 @@ TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
|
||||
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::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, &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);
|
||||
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(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
|
||||
@@ -4388,3 +4576,236 @@ TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated)
|
||||
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER as an endpoint target, and a renderbuffer name lives
|
||||
// in its own namespace. Resolving BOTH names through the texture namespace answered a null object
|
||||
// for every renderbuffer endpoint, so all 74 conformance cases that name one - the whole
|
||||
// texture<->renderbuffer half of KHR-GL43.copy_image, plus its smoke test - reported
|
||||
// GL_INVALID_VALUE. The endpoint is a sum type now; the target picks the namespace.
|
||||
TEST_F(TextureTest, CopyImageSubDataResolvesARenderbufferEndpointInTheRenderbufferNamespace) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8);
|
||||
GLuint renderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.SrcIsRenderbuffer);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_RENDERBUFFER));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// ...and back the other way, which is the second half of the conformance case's two-copy
|
||||
// shape (texture -> renderbuffer -> texture).
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.DstIsRenderbuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Renderbuffer to renderbuffer, the shape neither endpoint could take before, plus the negative
|
||||
// that pins which table was consulted: with GL_RENDERBUFFER named, a number that is not a live
|
||||
// RENDERBUFFER is INVALID_VALUE - the texture table is never asked.
|
||||
TEST_F(TextureTest, CopyImageSubDataKeepsTheTwoNameNamespacesApart) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcRenderbuffer = 0;
|
||||
GLuint dstRenderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &srcRenderbuffer);
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &dstRenderbuffer);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(srcRenderbuffer, GL_RGBA8, 8, 8);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(dstRenderbuffer, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, dstRenderbuffer,
|
||||
GL_RENDERBUFFER, 0, 0, 0, 0, 4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, 4243, GL_RENDERBUFFER, 0, 0, 0,
|
||||
0, 4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// A renderbuffer has exactly one image, so any level above zero is the same INVALID_VALUE a
|
||||
// texture gets for a level it does not have - and an unallocated one is an incomplete image,
|
||||
// which 18.3.2 spells INVALID_OPERATION.
|
||||
TEST_F(TextureTest, CopyImageSubDataChecksARenderbufferLevelAndStorage) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8);
|
||||
GLuint renderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 1, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
GLuint emptyRenderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &emptyRenderbuffer);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, emptyRenderbuffer, GL_RENDERBUFFER, 0, 0,
|
||||
0, 0, 4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// A 16-byte RGTC2 block and a 16-byte RGBA32UI texel are in the same size class, so GL 4.6 core
|
||||
// 18.3.2 requires this copy to succeed. It did not for an ARRAY source: glTexImage3D recorded no
|
||||
// specific-compressed-format tag, so the level was measured as the 2-byte RG8 storage RGTC2
|
||||
// resolves to and the compatibility rule saw 2 against 16.
|
||||
TEST_F(TextureTest, CopyImageSubDataSizesACompressedArrayLevelByItsBlock) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint compressedSource = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &compressedSource);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, compressedSource);
|
||||
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 1, 0, GL_RG,
|
||||
GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
GLuint uncompressedDestination = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &uncompressedDestination);
|
||||
MG_Impl::GLImpl::TextureStorage3D(uncompressedDestination, 1, GL_RGBA32UI, 8, 8, 1);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(compressedSource, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, uncompressedDestination,
|
||||
GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 8, 8, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// 18.3.2 requires INVALID_OPERATION when either object is an INCOMPLETE TEXTURE, and completeness
|
||||
// is GL 4.6 core 8.17's - which includes the mip chain whenever the minification filter reads it.
|
||||
// A mutable texture with level 0 alone still carries the default NEAREST_MIPMAP_LINEAR filter, so
|
||||
// it is mipmap incomplete; the storage-only IsComplete() this used to ask called it complete and
|
||||
// let the copy through, which is the whole of KHR-GL43.copy_image.incomplete_tex.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsAMipmapIncompleteTexture) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint incomplete = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &incomplete);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
GLuint complete = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &complete);
|
||||
MG_Impl::GLImpl::TextureStorage2D(complete, 1, GL_RGBA8, 16, 16);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
|
||||
4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// The destination side is checked the same way.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(complete, GL_TEXTURE_2D, 0, 0, 0, 0, incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
|
||||
4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// Capping TEXTURE_MAX_LEVEL at the one level that exists is what the conformance suite's
|
||||
// makeTextureComplete does, and it is enough to make the same object complete.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete);
|
||||
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::BindTexture(GL_TEXTURE_2D, 0);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, 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 targets that have no mip chain must not be dragged in: GL 4.6 core 8.17 makes q equal to
|
||||
// level_base for them, so no filter can make them mipmap incomplete. A rectangle texture gets a
|
||||
// non-mipmapping default filter from the object itself, so it would survive a predicate that
|
||||
// trusted the sampler alone - it is here because the whole texture path is one branch and this is
|
||||
// the cheap half of pinning it.
|
||||
TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToRectangleTextures) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcRectangle = 0;
|
||||
GLuint dstRectangle = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcRectangle);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstRectangle);
|
||||
MG_Impl::GLImpl::TextureStorage2D(srcRectangle, 1, GL_RGBA8, 8, 8);
|
||||
MG_Impl::GLImpl::TextureStorage2D(dstRectangle, 1, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcRectangle, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstRectangle,
|
||||
GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, 4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The multisample half, which is the one the target guard actually exists for: a multisample
|
||||
// texture keeps the shared NEAREST_MIPMAP_LINEAR default in its own sampler state (only the
|
||||
// rectangle constructor overrides it), so asking the mipmap predicate about it without the target
|
||||
// guard would report every 8x8 multisample image incomplete and refuse a legal copy.
|
||||
TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToMultisampleTextures) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcMultisample = 0;
|
||||
GLuint dstMultisample = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcMultisample);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstMultisample);
|
||||
MG_Impl::GLImpl::TextureStorage2DMultisample(srcMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE);
|
||||
MG_Impl::GLImpl::TextureStorage2DMultisample(dstMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// This unit-test binary has no backend behind the renderable-format and sample-count queries,
|
||||
// so the storage may not have been created at all. Checked on the state objects rather than
|
||||
// assumed, so the case can only skip or test the real rule.
|
||||
const auto srcObject = MG_State::pGLContext->GetTextureObject(srcMultisample);
|
||||
const auto dstObject = MG_State::pGLContext->GetTextureObject(dstMultisample);
|
||||
ASSERT_NE(srcObject, nullptr);
|
||||
ASSERT_NE(dstObject, nullptr);
|
||||
if (!srcObject->IsComplete() || !dstObject->IsComplete()) {
|
||||
GTEST_SKIP() << "this context could not give the multisample textures storage";
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcMultisample, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstMultisample,
|
||||
GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -253,6 +253,12 @@ namespace MobileGL {
|
||||
return TextureInternalFormat::Depth32FStencil8;
|
||||
case GL_STENCIL_INDEX8:
|
||||
return TextureInternalFormat::StencilIndex8;
|
||||
// The unsized stencil base format resolves to the only stencil storage there is, the
|
||||
// same way the unsized colour and depth base formats below resolve to theirs. Returning
|
||||
// Unknown made glTexImage2D(GL_STENCIL_INDEX) an error, which killed the negative
|
||||
// clear-texture cases in their own setup before they could reach the call they test.
|
||||
case GL_STENCIL_INDEX:
|
||||
return TextureInternalFormat::StencilIndex8;
|
||||
case GL_DEPTH_COMPONENT:
|
||||
return TextureInternalFormat::DepthComponent;
|
||||
case GL_DEPTH_STENCIL:
|
||||
|
||||
@@ -124,6 +124,9 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::DepthComponent32F:
|
||||
case TextureInternalFormat::Depth24Stencil8:
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
// Already sized: both GL_STENCIL_INDEX8 and the unsized GL_STENCIL_INDEX resolve here,
|
||||
// and there is only one stencil storage to infer.
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
return internalformat;
|
||||
// probably we should assume unorm here?
|
||||
case TextureInternalFormat::RGBA: {
|
||||
|
||||
@@ -138,6 +138,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
case TextureInternalFormat::DepthComponent32F:
|
||||
out = {1, ShadowComponent::Float32, false};
|
||||
return true;
|
||||
// Stencil is the one single-channel INTEGER shadow that is not a colour format: eight
|
||||
// bits, held as an unsigned index rather than a normalized value.
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
out = {1, ShadowComponent::UInt8, true};
|
||||
return true;
|
||||
|
||||
case TextureInternalFormat::R8:
|
||||
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
|
||||
@@ -336,8 +341,13 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
|
||||
// A depth value converts like a single normalized/float channel.
|
||||
case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true;
|
||||
// A stencil index is a single INTEGER channel (GL 4.6 core 8.4.4.3). Without this the
|
||||
// upload fell to the raw-memcpy branch, which copies the client element width into the
|
||||
// one-byte STENCIL_INDEX8 shadow verbatim - right for GL_UNSIGNED_BYTE and wrong for
|
||||
// every wider type. The state layer keeps this paired with stencil-only storage.
|
||||
case TextureInputFormat::StencilIndex: out = {{0, -1, -1, -1}, 1, true}; return true;
|
||||
default:
|
||||
return false; // stencil / packed depth-stencil / unknown
|
||||
return false; // packed depth-stencil / unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,6 +816,14 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
return IsRawPackedPixelPair(packedInternal.kind, clientFormat, clientType);
|
||||
}
|
||||
|
||||
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat) {
|
||||
InternalPackedLayout packedInternal{};
|
||||
if (!GetInternalPackedLayout(internalFormat, packedInternal)) {
|
||||
return false;
|
||||
}
|
||||
return packedInternal.kind == PackedInternalKind::FloatRGB9E5;
|
||||
}
|
||||
|
||||
// assume 8 bit per channel
|
||||
// swizzle.size() == channel count
|
||||
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) {
|
||||
@@ -990,6 +1008,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
const void* inputPixel,
|
||||
Vector<Uint8>& outputPixel) {
|
||||
outputPixel.clear();
|
||||
// A stencil index became a transferable format when STENCIL_INDEX8 texture storage did (see
|
||||
// GetUnpackChannelMapping), but this helper serves glClearBufferData, whose internal formats
|
||||
// are all colour (GL 4.6 core table 8.20): a stencil pattern would otherwise pass the size
|
||||
// check and land silently in an equally-sized colour store.
|
||||
if (textureInputFormat == TextureInputFormat::StencilIndex) return false;
|
||||
if (inputPixel == nullptr || !IsValidUnpackPixelPair(textureInputFormat, inputDataType)) return false;
|
||||
|
||||
PixelStoreParameters params{};
|
||||
|
||||
@@ -44,6 +44,17 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
|
||||
TexturePixelDataType clientType);
|
||||
|
||||
// True when a packed internal format has REDUNDANT encodings, so decoding a texel and
|
||||
// re-encoding it keeps the VALUE but not the BITS. Only RGB9_E5 does: its shared exponent can
|
||||
// be lowered with the mantissas shifted up to match, and the spec's encoder always emits the
|
||||
// canonical form. RGB10_A2, RGB10_A2UI and R11F_G11F_B10F round-trip through float32
|
||||
// bit-exactly, so a GPU readback can answer for them.
|
||||
//
|
||||
// This is what decides whether the CPU shadow has to stay authoritative for a format: a
|
||||
// readback of an RGB9_E5 level through a colour attachment cannot return the stored words, no
|
||||
// matter how well behaved the driver is.
|
||||
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat);
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user