diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 35bcc6bb..342d9924 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1036,7 +1036,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get()); if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue; - GLenum targetGL = MG_Util::ConvertTextureTargetToGLEnum(target); + GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); backendTextureIt->second->Bind(targetGL, unit); } @@ -1847,7 +1847,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!exist) { backendObj = MakeShared(); } - backendObj->Bind(target, unit); + backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit); } return true; } @@ -2137,10 +2137,10 @@ namespace MobileGL::MG_Backend::DirectGLES { continue; } - GLenum textureTarget = - MG_Util::ConvertTextureUploadTargetToGLEnum(attachmentObject.GetTextureUploadTarget()); + GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( + attachmentObject.GetTextureUploadTarget()); if (textureTarget == GL_UNKNOWN_MGL) { - textureTarget = MG_Util::ConvertTextureTargetToGLEnum(texture->GetTarget()); + textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(texture->GetTarget()); } const GLuint backendFBOId = backendFBO->GetBackendFramebufferId(); @@ -2637,7 +2637,9 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - backendTexture->Bind(target, unitIndex); + const GLenum backendTarget = + TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target)); + backendTexture->Bind(backendTarget, unitIndex); DebugImpl::ErrorLopper::Clear(); // ANGLE/Mesa may validate the currently bound FBO while generating mipmaps. // Also detach the source texture from synced FBO objects for ANGLE's validation. @@ -2645,8 +2647,8 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Clear(); // Bind a complete internal FBO that does not reference the source texture. ScopedCompleteFramebufferBinding completeFramebuffer; - g_GLESFuncs.glGenerateMipmap(target); - RecordGLError("glGenerateMipmap", target, texture->GetFormat()); + g_GLESFuncs.glGenerateMipmap(backendTarget); + RecordGLError("glGenerateMipmap", backendTarget, texture->GetFormat()); } const GLubyte* GetString(GLenum name) { @@ -3068,88 +3070,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); } - static Int GetFloatReadbackChannelCount(GLenum format) { - switch (format) { - case GL_RED: - return 1; - case GL_RGBA: - return 4; - default: - return 0; - } - } - - static Bool ReadPixelsFloatViaUnsignedByte(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, - void* pixels) { - if (width <= 0 || height <= 0) { - return true; - } - const Int dstChannels = GetFloatReadbackChannelCount(format); - if (dstChannels == 0) { - return false; - } - - const GLenum readFormat = format == GL_RED ? GL_RED : GL_RGBA; - const Int readChannels = format == GL_RED ? 1 : 4; - Vector raw(static_cast(width) * static_cast(height) * - static_cast(readChannels)); - - GLint prevPixelPackBuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - g_GLESFuncs.glReadPixels(x, y, width, height, readFormat, GL_UNSIGNED_BYTE, raw.data()); - const GLenum readError = g_GLESFuncs.glGetError(); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); - if (readError != GL_NO_ERROR) { - MGLOG_E("ReadPixels: GL_FLOAT fallback read failed: %s", - MG_Util::ConvertGLEnumToString(readError).c_str()); - return true; - } - - const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); - const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); - const SizeT dstPixelBytes = static_cast(dstChannels) * sizeof(Float); - const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT dstOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + - static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; - const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + - static_cast(width) * dstPixelBytes; - Vector packed(packedSize, 0); - - for (GLsizei row = 0; row < height; ++row) { - const Uint8* srcRow = raw.data() + static_cast(row) * static_cast(width) * - static_cast(readChannels); - auto* dstRow = reinterpret_cast(packed.data() + dstOffset + - static_cast(row) * dstRowStride); - for (GLsizei col = 0; col < width; ++col) { - const Uint8* src = srcRow + static_cast(col) * static_cast(readChannels); - Float* dst = dstRow + static_cast(col) * static_cast(dstChannels); - // TODO: extend readback packing to all desktop GL read formats instead of only normalized RED/RGBA. - for (Int component = 0; component < dstChannels; ++component) { - dst[component] = static_cast(src[component]) / 255.0f; - } - } - } - - const auto& pixelPackBufferObject = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (pixelPackBufferObject) { - const SizeT pboOffset = reinterpret_cast(pixels); - if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) { - MGLOG_E("ReadPixels: GL_FLOAT fallback PBO is too small"); - return true; - } - pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); - } else if (pixels != nullptr && !packed.empty()) { - Memcpy(pixels, packed.data(), packed.size()); - } - return true; - } - static Bool ReadPixelsDepthFloatViaUnsignedInt(GLint x, GLint y, GLsizei width, GLsizei height, void* pixels) { if (width <= 0 || height <= 0) { return true; @@ -3180,29 +3100,29 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + static_cast(width) * dstPixelBytes; - Vector packed(packedSize, 0); - - for (GLsizei row = 0; row < height; ++row) { - const Uint32* srcRow = raw.data() + static_cast(row) * static_cast(width); - auto* dstRow = reinterpret_cast(packed.data() + dstOffset + - static_cast(row) * dstRowStride); - for (GLsizei col = 0; col < width; ++col) { - // TODO: preserve native depth precision when GLES exposes float depth readback directly. - dstRow[col] = static_cast(static_cast(srcRow[col]) / 4294967295.0); - } - } - + // Only actual pixel rows are written so PACK skip/row-length gap regions stay untouched. const auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (pixelPackBufferObject) { - const SizeT pboOffset = reinterpret_cast(pixels); - if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) { - MGLOG_E("ReadPixels: depth GL_FLOAT fallback PBO is too small"); - return true; + const SizeT pboOffset = reinterpret_cast(pixels); + if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { + MGLOG_E("ReadPixels: depth GL_FLOAT fallback PBO is too small"); + return true; + } + Vector rowBuf(static_cast(width)); + for (GLsizei row = 0; row < height; ++row) { + const Uint32* srcRow = raw.data() + static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + // TODO: preserve native depth precision when GLES exposes float depth readback directly. + rowBuf[col] = static_cast(static_cast(srcRow[col]) / 4294967295.0); + } + const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend( + {rowBuf.data(), static_cast(width) * sizeof(Float)}, pboOffset + rowOffset); + } else if (pixels != nullptr) { + Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), + static_cast(width) * sizeof(Float)); } - pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); - } else if (pixels != nullptr && !packed.empty()) { - Memcpy(pixels, packed.data(), packed.size()); } return true; } @@ -3237,29 +3157,29 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + static_cast(width) * dstPixelBytes; - Vector packed(packedSize, 0); - - for (GLsizei row = 0; row < height; ++row) { - const Uint8* srcRow = raw.data() + static_cast(row) * static_cast(width); - auto* dstRow = reinterpret_cast(packed.data() + dstOffset + - static_cast(row) * dstRowStride); - for (GLsizei col = 0; col < width; ++col) { - // TODO: switch to native uint stencil readback if the GLES backend exposes it. - dstRow[col] = srcRow[col]; - } - } - + // Only actual pixel rows are written so PACK skip/row-length gap regions stay untouched. const auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (pixelPackBufferObject) { - const SizeT pboOffset = reinterpret_cast(pixels); - if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) { - MGLOG_E("ReadPixels: stencil GL_UNSIGNED_INT fallback PBO is too small"); - return true; + const SizeT pboOffset = reinterpret_cast(pixels); + if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { + MGLOG_E("ReadPixels: stencil GL_UNSIGNED_INT fallback PBO is too small"); + return true; + } + Vector rowBuf(static_cast(width)); + for (GLsizei row = 0; row < height; ++row) { + const Uint8* srcRow = raw.data() + static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + // TODO: switch to native uint stencil readback if the GLES backend exposes it. + rowBuf[col] = srcRow[col]; + } + const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend( + {rowBuf.data(), static_cast(width) * sizeof(Uint32)}, pboOffset + rowOffset); + } else if (pixels != nullptr) { + Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), + static_cast(width) * sizeof(Uint32)); } - pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); - } else if (pixels != nullptr && !packed.empty()) { - Memcpy(pixels, packed.data(), packed.size()); } return true; } @@ -3291,6 +3211,94 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + // Component-array read formats usable as (possibly narrow) wide-read sources. + static Int GetWideReadChannelCount(GLenum format) { + switch (format) { + case GL_RED: + case GL_RED_INTEGER: + return 1; + case GL_RG: + case GL_RG_INTEGER: + return 2; + case GL_RGB: + case GL_RGB_INTEGER: + return 3; + case GL_RGBA: + case GL_RGBA_INTEGER: + return 4; + default: + return 0; + } + } + + static Bool IsIntegerReadFormat(GLint format) { + return format == GL_RED_INTEGER || format == GL_RG_INTEGER || format == GL_RGB_INTEGER || + format == GL_RGBA_INTEGER; + } + + // Expands a tightly-packed narrow read (1-3 channels per texel) into the 4-channel wide RGBA + // layout ConvertWideReadbackRow expects. Missing G/B read zero; missing A reads one, encoded in + // the source component type. + static void ExpandNarrowWideRead(Vector& data, SizeT pixelCount, Int srcChannels, GLenum componentType) { + const SizeT componentSize = GetReadbackComponentSize(componentType); + if (componentSize == 0 || srcChannels <= 0 || srcChannels >= 4) { + return; + } + Uint8 zeroBits[4] = {0, 0, 0, 0}; + Uint8 oneBits[4] = {0, 0, 0, 0}; + switch (componentType) { + case GL_UNSIGNED_BYTE: + oneBits[0] = 0xFF; + break; + case GL_BYTE: + oneBits[0] = 0x7F; + break; + case GL_UNSIGNED_SHORT: { + const Uint16 one = 0xFFFF; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_SHORT: { + const Int16 one = 0x7FFF; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_HALF_FLOAT: { + const Uint16 one = 0x3C00; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_FLOAT: { + const Float one = 1.0f; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_UNSIGNED_INT: + case GL_INT: { + const Uint32 one = 1; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + default: + break; + } + + Vector expanded(pixelCount * 4 * componentSize); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* src = data.data() + i * static_cast(srcChannels) * componentSize; + Uint8* dst = expanded.data() + i * 4 * componentSize; + for (Int ch = 0; ch < 4; ++ch) { + if (ch < srcChannels) { + Memcpy(dst + static_cast(ch) * componentSize, src + static_cast(ch) * componentSize, + componentSize); + } else { + Memcpy(dst + static_cast(ch) * componentSize, ch == 3 ? oneBits : zeroBits, componentSize); + } + } + } + data = std::move(expanded); + } + static void DrainESErrors() { for (Int i = 0; i < 32 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { } @@ -3314,16 +3322,14 @@ namespace MobileGL::MG_Backend::DirectGLES { return componentType != 0 ? static_cast(componentType) : GL_UNSIGNED_NORMALIZED; } - // Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's - // (format, type) layout. Returns false when the combination is not convertible (the caller keeps its - // "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op. - static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, - GLenum type, void* pixels) { - ReadbackChannelMapping mapping{}; - if (!GetReadbackChannelMapping(format, mapping)) { - return false; - } - // Covers unknown types, packed field-count/format mismatches and float types on integer formats. + // Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the + // client-side PACK parameters and the bound pixel-pack buffer. `wide` holds `height` rows of + // `width` texels, 4 components x GetReadbackComponentSize(wideType) bytes each. + // honorPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply to GetTexImage of 3D + // images only; ReadPixels ignores them (GL 3.3 section 4.3.1). + static Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei height, + const ReadbackChannelMapping& mapping, GLenum type, void* pixels, + Bool honorPackImageParams) { const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); if (dstPixelBytes == 0) { return false; @@ -3332,91 +3338,20 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool isPackedType = ReadbackImpl::GetPackedReadbackLayout(type, packedLayout); const SizeT dstComponentSize = GetReadbackComponentSize(type); - if (width <= 0 || height <= 0) { - return true; - } const auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (!pixelPackBufferObject && pixels == nullptr) { - return true; - } - - const GLenum attachmentComponentType = QueryReadAttachmentComponentType(); - const Bool integerAttachment = - attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT; - if (mapping.isInteger != integerAttachment) { - MGLOG_E("Readback conversion: integer-ness of format %s does not match the read buffer, skipping", - MG_Util::ConvertGLEnumToString(format).c_str()); - return true; - } - - // Prefer the implementation-defined pair (full precision on e.g. norm16 buffers), then the - // spec-guaranteed pair for the attachment class. - GLint implFormat = 0; - GLint implType = 0; - g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); - g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); - - const GLenum wideFormat = mapping.isInteger ? GL_RGBA_INTEGER : GL_RGBA; - GLenum wideTypeCandidates[3]; - Int wideTypeCandidateCount = 0; - if (mapping.isInteger) { - if (implFormat == GL_RGBA_INTEGER && (implType == GL_INT || implType == GL_UNSIGNED_INT)) { - wideTypeCandidates[wideTypeCandidateCount++] = static_cast(implType); - } - wideTypeCandidates[wideTypeCandidateCount++] = - attachmentComponentType == GL_INT ? GL_INT : GL_UNSIGNED_INT; - } else { - if (implFormat == GL_RGBA && CanDecodeWideSourceType(static_cast(implType))) { - wideTypeCandidates[wideTypeCandidateCount++] = static_cast(implType); - } - if (attachmentComponentType == GL_FLOAT) { - wideTypeCandidates[wideTypeCandidateCount++] = GL_FLOAT; - } - wideTypeCandidates[wideTypeCandidateCount++] = GL_UNSIGNED_BYTE; - } - - GLint prevPixelPackBuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - - Vector wide; - GLenum wideType = GL_NONE; - DrainESErrors(); - for (Int i = 0; i < wideTypeCandidateCount; ++i) { - const GLenum candidate = wideTypeCandidates[i]; - Bool alreadyTried = false; - for (Int j = 0; j < i; ++j) { - alreadyTried = alreadyTried || wideTypeCandidates[j] == candidate; - } - if (alreadyTried) { - continue; - } - const SizeT candidateComponentSize = GetReadbackComponentSize(candidate); - wide.resize(static_cast(width) * static_cast(height) * 4 * candidateComponentSize); - g_GLESFuncs.glReadPixels(x, y, width, height, wideFormat, candidate, wide.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - wideType = candidate; - break; - } - } - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); - if (wideType == GL_NONE) { - MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback", - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); - return true; - } // Destination layout is computed from the client-side PACK parameters; only the actual pixel - // rows are written so skip regions of the destination stay untouched. + // rows are written so skip regions of the destination stay untouched. GL_PACK_SKIP_IMAGES + // skips whole 2D images of GL_PACK_IMAGE_HEIGHT (or `height`) rows for 3D readbacks. const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT dstSkipOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + + const SizeT imageRows = static_cast(packParams.ImageHeight > 0 ? packParams.ImageHeight : height); + const SizeT skipImages = + honorPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; + const SizeT dstSkipOffset = skipImages * imageRows * dstRowStride + + static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; @@ -3435,7 +3370,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector convertedRow(dstRowBytes); for (GLsizei row = 0; row < height; ++row) { - const Uint8* srcRow = wide.data() + static_cast(row) * static_cast(width) * srcPixelBytes; + const Uint8* srcRow = wide + static_cast(row) * static_cast(width) * srcPixelBytes; ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast(width), wideType, mapping, type); @@ -3456,13 +3391,207 @@ namespace MobileGL::MG_Backend::DirectGLES { Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); } } + return true; + } + + // Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's + // (format, type) layout. Returns false when the combination is not convertible (the caller keeps its + // "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op. + static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, + GLenum type, void* pixels, Bool honorPackImageParams = false) { + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) { + return false; + } + // Covers unknown types, packed field-count/format mismatches and float types on integer formats. + const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); + if (dstPixelBytes == 0) { + return false; + } + + if (width <= 0 || height <= 0) { + return true; + } + const auto& pixelPackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); + if (!pixelPackBufferObject && pixels == nullptr) { + return true; + } + + const GLenum attachmentComponentType = QueryReadAttachmentComponentType(); + const Bool integerAttachment = + attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT; + if (mapping.isInteger != integerAttachment) { + MGLOG_E("Readback conversion: integer-ness of format %s does not match the read buffer, skipping", + MG_Util::ConvertGLEnumToString(format).c_str()); + return true; + } + + // Prefer the implementation-defined pair (full precision on e.g. norm16 buffers, and possibly + // a narrow format like GL_RED/GL_UNSIGNED_SHORT), then the spec/extension-guaranteed pair for + // the attachment class. Narrow reads are expanded to RGBA on the CPU afterwards. + GLint implFormat = 0; + GLint implType = 0; + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); + + struct WideReadCandidate { + GLenum format; + GLenum type; + }; + WideReadCandidate candidates[4]; + Int candidateCount = 0; + if (mapping.isInteger) { + if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && IsIntegerReadFormat(implFormat) && + (implType == GL_INT || implType == GL_UNSIGNED_INT)) { + candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; + } + candidates[candidateCount++] = { + GL_RGBA_INTEGER, + attachmentComponentType == GL_INT ? static_cast(GL_INT) : static_cast(GL_UNSIGNED_INT)}; + } else { + if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && !IsIntegerReadFormat(implFormat) && + (CanDecodeWideSourceType(static_cast(implType)) || + (implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) { + candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; + } + if (attachmentComponentType == GL_FLOAT) { + candidates[candidateCount++] = {GL_RGBA, GL_FLOAT}; + } + if (attachmentComponentType == GL_SIGNED_NORMALIZED) { + // EXT_render_snorm attachments read back as RGBA/BYTE (8-bit) or RGBA/SHORT (16-bit). + candidates[candidateCount++] = {GL_RGBA, GL_SHORT}; + candidates[candidateCount++] = {GL_RGBA, GL_BYTE}; + } else { + candidates[candidateCount++] = {GL_RGBA, GL_UNSIGNED_BYTE}; + } + } + + GLint prevPixelPackBuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); + g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); + g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + + Vector wide; + GLenum wideType = GL_NONE; + GLenum readFormat = GL_NONE; + Int readChannels = 0; + DrainESErrors(); + for (Int i = 0; i < candidateCount; ++i) { + const WideReadCandidate candidate = candidates[i]; + Bool alreadyTried = false; + for (Int j = 0; j < i; ++j) { + alreadyTried = + alreadyTried || (candidates[j].format == candidate.format && candidates[j].type == candidate.type); + } + if (alreadyTried) { + continue; + } + const Int channels = GetWideReadChannelCount(candidate.format); + const SizeT candidateComponentSize = GetReadbackComponentSize(candidate.type); + wide.resize(static_cast(width) * static_cast(height) * + static_cast(channels) * candidateComponentSize); + g_GLESFuncs.glReadPixels(x, y, width, height, candidate.format, candidate.type, wide.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + wideType = candidate.type; + readFormat = candidate.format; + readChannels = channels; + break; + } + } + g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); + if (wideType == GL_NONE) { + MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); + return true; + } + + if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) { + // Unpack the packed words into a float wide buffer (full 10-bit precision on e.g. + // GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV). + const SizeT pixelCount = static_cast(width) * static_cast(height); + Vector floatWide(pixelCount * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(floatWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + Uint32 word; + Memcpy(&word, wide.data() + i * 4, sizeof(word)); + dst[i * 4 + 0] = static_cast(word & 0x3FFu) / 1023.0f; + dst[i * 4 + 1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; + dst[i * 4 + 2] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; + dst[i * 4 + 3] = static_cast((word >> 30) & 0x3u) / 3.0f; + } + wide = std::move(floatWide); + wideType = GL_FLOAT; + readChannels = 4; + } + if (readChannels < 4) { + ExpandNarrowWideRead(wide, static_cast(width) * static_cast(height), readChannels, wideType); + } + + if (!StoreWideRowsToClient(wide.data(), wideType, width, height, mapping, type, pixels, + honorPackImageParams)) { + return false; + } MGLOG_D("Readback conversion: converted %s/%s from wide %s/%s", MG_Util::ConvertGLEnumToString(format).c_str(), - MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(wideFormat).c_str(), + MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(readFormat).c_str(), MG_Util::ConvertGLEnumToString(wideType).c_str()); return true; } + // GetTexImage fallback for internal formats the ES driver cannot attach to a framebuffer + // (SNORM, RGB16, RGB9_E5, ...): decodes the canonical CPU shadow-mip storage into wide RGBA + // rows and repacks them into the client layout. Only valid while the shadow copy is + // authoritative, which holds for non-renderable formats (they can never be GPU-written). + static Bool GetTexImageViaShadowConversion(MG_State::GLState::TextureObjectMipmap* textureMipmapObject, + TextureUploadTarget uploadTarget, GLint level, GLsizei width, + GLsizei height, GLenum format, GLenum type, void* pixels) { + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) { + return false; + } + if (GetReadbackDstPixelSize(mapping, type) == 0) { + return false; + } + if (width <= 0 || height <= 0) { + return true; + } + const auto& pixelPackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); + if (!pixelPackBufferObject && pixels == nullptr) { + return true; + } + + const void* shadow = textureMipmapObject->MapMipmapData(uploadTarget, level); + if (!shadow) { + return false; + } + + Vector wide; + Bool isInteger = false; + Bool isSigned = false; + if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA( + textureMipmapObject->GetFormat(), shadow, static_cast(width) * static_cast(height), + wide, isInteger, isSigned)) { + return false; + } + if (mapping.isInteger != isInteger) { + // Spec-invalid combinations are rejected with GL errors at the state layer already. + return false; + } + const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT; + if (!StoreWideRowsToClient(wide.data(), wideType, width, height, mapping, type, pixels, + /*honorPackImageParams=*/true)) { + return false; + } + MGLOG_D("GetTexImage: converted %s/%s from the CPU shadow copy", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); + return true; + } + static Bool IsLegacyNativeReadPixelsFormat(GLenum format) { return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER || format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX; @@ -3509,7 +3638,16 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("ReadPixels: bound READ FBO is not complete"); return; } - if (!useNativeReadback) { + // ES only guarantees GL_RGBA/GL_UNSIGNED_BYTE and GL_RGBA_INTEGER/GL_(UNSIGNED_)INT for the + // matching attachment class; every other convertible color layout (including GL_RGBA/GL_FLOAT + // and legacy GL_RED reads) goes through the wide-format conversion, which picks a wide type + // the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so + // it always takes the conversion path (which swaps on the CPU). + const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes; + const Bool nativeFastPair = !packSwapBytes && + ((format == GL_RGBA && type == GL_UNSIGNED_BYTE) || + (format == GL_RGBA_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT))); + if (convertible && !nativeFastPair) { if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels)) { MGLOG_D("ReadPixels: finished via client-format conversion"); return; @@ -3528,10 +3666,6 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ReadPixels: finished via stencil GL_UNSIGNED_INT fallback"); return; } - if (type == GL_FLOAT && ReadPixelsFloatViaUnsignedByte(x, y, width, height, format, pixels)) { - MGLOG_D("ReadPixels: finished via GL_FLOAT fallback"); - return; - } // Handle PBO auto& pixelPackBufferObject = @@ -3667,18 +3801,29 @@ namespace MobileGL::MG_Backend::DirectGLES { TempFBOBinder tempFBOBinder(true); MGLOG_D("GetTexImage: glFramebufferTexture2D(level=%d)", level); - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, backendTexId, level); + // The temp FBO is reused across GetTexImage calls: detach the previous color attachment first + // so a failed attach below leaves the FBO incomplete instead of silently reading stale contents. + g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + const GLenum backendAttachTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( + MG_Util::ConvertGLEnumToTextureUploadTarget(target)); + if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) { + // ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads + // of deeper slices are served from the CPU shadow instead (see the shadow-first branch). + g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, backendTexId, level, 0); + } else { + g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, + backendTexId, level); + } MGLOG_D("GetTexImage: glReadBuffer(GL_COLOR_ATTACHMENT0)"); g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); MGLOG_D("GetTexImage: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str()); - if (fbStatus != GL_FRAMEBUFFER_COMPLETE) { - MGLOG_E("GetTexImage: READ FBO incomplete"); - MGLOG_E("GetTexImage: bound READ FBO is not complete"); - return; - } + // Non-renderable internal formats (SNORM, RGB16, RGB9_E5, ...) leave the temp FBO incomplete; + // those readbacks are served from the CPU shadow copy below instead of bailing out. + const Bool tempFBOComplete = fbStatus == GL_FRAMEBUFFER_COMPLETE; DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); @@ -3716,15 +3861,45 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y()); - if (!useNativeReadback) { - if (ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels)) { + // Prefer the client-format conversion for every convertible combination: the "native" ES pairs + // are only guaranteed for matching attachment classes (e.g. GL_RGBA/GL_UNSIGNED_INT is invalid + // for normalized attachments), while the conversion path reads a wide format that is always + // accepted and repacks on the CPU. + if (convertible) { + // 3D/array images read back every slice, but the FBO path can only read one layer: + // multi-slice reads are served from the CPU shadow (depth as extra rows, tight layout). + const GLsizei shadowRows = size.y() * std::max(size.z(), 1); + const Bool multiSlice = size.z() > 1; + if (multiSlice && + GetTexImageViaShadowConversion(textureMipmapObject, + MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(), + shadowRows, format, type, pixels)) { + MGLOG_D("GetTexImage: finished via shadow conversion"); + return; + } + if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels, + /*honorPackImageParams=*/true)) { MGLOG_D("GetTexImage: finished via client-format conversion"); return; } + if (GetTexImageViaShadowConversion(textureMipmapObject, + MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(), + shadowRows, format, type, pixels)) { + MGLOG_D("GetTexImage: finished via shadow conversion"); + return; + } + if (!tempFBOComplete) { + MGLOG_E("GetTexImage: READ FBO incomplete and no shadow copy available, skipping readback"); + return; + } MGLOG_E("GetTexImage: format %s with type %s is not implemented yet, skipping readback", MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); return; } + if (!tempFBOComplete) { + MGLOG_E("GetTexImage: bound READ FBO is not complete"); + return; + } // Handle PBO auto& pixelPackBufferObject = diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index bf3aaa7d..7c8035b3 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1476,10 +1476,13 @@ namespace MobileGL::MG_Backend::DirectGLES { return 2; case TextureInternalFormat::RGB8Snorm: case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow) + case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow) case TextureInternalFormat::RGB16Snorm: return 3; case TextureInternalFormat::RGBA8Snorm: case TextureInternalFormat::RGBA16: + case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow) case TextureInternalFormat::RGBA16Snorm: return 4; default: @@ -1574,7 +1577,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); - GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget()); + GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); auto targetInternal = stateTextureObject->GetTarget(); MGLOG_D(" Texture target for syncing is %s", MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); @@ -1695,7 +1698,7 @@ namespace MobileGL::MG_Backend::DirectGLES { auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level); bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level); - auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto* pData = (levelDirty && levelByteSize != 0) ? textureMipmapObject->MapMipmapData(uploadTarget, level) : nullptr; @@ -1706,19 +1709,22 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - switch (stateTextureObject->GetTarget()) { + const IntVec3 uploadSize = + GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize); + switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: g_GLESFuncs.glTexImage2D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(levelTexelSize.x()), static_cast(levelTexelSize.y()), + static_cast(uploadSize.x()), static_cast(uploadSize.y()), 0, glFormat, glType, uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexImage3D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(levelTexelSize.x()), static_cast(levelTexelSize.y()), - static_cast(levelTexelSize.z()), 0, glFormat, glType, uploadData); + static_cast(uploadSize.x()), static_cast(uploadSize.y()), + static_cast(uploadSize.z()), 0, glFormat, glType, uploadData); break; default: MGLOG_E("Unhandled texture target %s", @@ -1782,18 +1788,20 @@ namespace MobileGL::MG_Backend::DirectGLES { } else if (stateTextureObject->IsImmutable() || m_imageBindableStorageRequired) { DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - switch (targetInternal) { + const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize); + switch (MapToBackendTextureTarget(targetInternal)) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: g_GLESFuncs.glTexStorage2D(target, static_cast(mipmapCount), glInternalFormat, - static_cast(baseSize.x()), - static_cast(baseSize.y())); + static_cast(storageSize.x()), + static_cast(storageSize.y())); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexStorage3D(target, static_cast(mipmapCount), glInternalFormat, - static_cast(baseSize.x()), - static_cast(baseSize.y()), - static_cast(baseSize.z())); + static_cast(storageSize.x()), + static_cast(storageSize.y()), + static_cast(storageSize.z())); break; default: MGLOG_E("Unhandled immutable texture target %s", @@ -1817,7 +1825,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (levelDirty && levelByteSize != 0) { auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); - auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level); Vector convertedUploadData; const void* uploadData = PrepareNormFloatFallbackUpload( @@ -1826,20 +1834,23 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - switch (targetInternal) { + const IntVec3 uploadSize = + GetBackendUploadSize(targetInternal, levelTexelSize); + switch (MapToBackendTextureTarget(targetInternal)) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: g_GLESFuncs.glTexSubImage2D( glUploadTarget, static_cast(level), 0, 0, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), glFormat, glType, uploadData); + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), glFormat, glType, uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexSubImage3D( glUploadTarget, static_cast(level), 0, 0, 0, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), - static_cast(levelTexelSize.z()), glFormat, glType, uploadData); + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), glFormat, glType, uploadData); break; default: break; @@ -1866,7 +1877,7 @@ namespace MobileGL::MG_Backend::DirectGLES { auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level); bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level); - auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto* pData = (levelDirty && levelByteSize != 0) ? textureMipmapObject->MapMipmapData(uploadTarget, level) : nullptr; @@ -1883,22 +1894,23 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); auto textureTarget = stateTextureObject->GetTarget(); - // TODO: handle more texture types - switch (textureTarget) { + const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize); + switch (MapToBackendTextureTarget(textureTarget)) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: { g_GLESFuncs.glTexImage2D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), 0, glFormat, glType, uploadData); + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), 0, glFormat, glType, uploadData); break; } - case TextureTarget::Texture3D: { + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: { g_GLESFuncs.glTexImage3D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), - static_cast(levelTexelSize.z()), 0, glFormat, glType, uploadData); + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), 0, glFormat, glType, uploadData); break; } default: { @@ -1965,7 +1977,7 @@ namespace MobileGL::MG_Backend::DirectGLES { textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).x(), textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize); - auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); DebugImpl::ErrorLopper::Loop( [file = __FILE__, line = __LINE__, func = __func__](GLenum err) { @@ -1978,19 +1990,22 @@ namespace MobileGL::MG_Backend::DirectGLES { const void* uploadData = PrepareNormFloatFallbackUpload( textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType, convertedUploadData); - switch (stateTextureObject->GetTarget()) { + const IntVec3 uploadSize = + GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize); + switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast(level), 0, 0, - static_cast(texelSize.x()), - static_cast(texelSize.y()), glFormat, glType, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), glFormat, glType, uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast(level), 0, 0, 0, - static_cast(texelSize.x()), - static_cast(texelSize.y()), - static_cast(texelSize.z()), glFormat, glType, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), glFormat, glType, uploadData); break; default: @@ -2083,7 +2098,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); - GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget()); + GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); auto targetInternal = stateTextureObject->GetTarget(); MGLOG_D(" Texture target for syncing is %s", MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); @@ -2181,7 +2196,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); - GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget()); + GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); auto targetInternal = stateTextureObject->GetTarget(); MGLOG_D(" Texture target for syncing is %s", MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); @@ -2338,11 +2353,21 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment, backendTextureObject->GetBackendTextureId(), static_cast(attachmentObject.GetTextureLevel())); + } else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget(); + uploadTarget == TextureUploadTarget::Texture3D || + uploadTarget == TextureUploadTarget::Texture2DArray || + uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) { + // Single slice/layer of a 3D or array texture: ES has no + // glFramebufferTexture3D, layers attach via glFramebufferTextureLayer. + g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment, + backendTextureObject->GetBackendTextureId(), + static_cast(attachmentObject.GetTextureLevel()), + static_cast(attachmentObject.GetTextureLayer())); } else { - auto glTextureTarget = - MG_Util::ConvertTextureUploadTargetToGLEnum(attachmentObject.GetTextureUploadTarget()); + auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( + attachmentObject.GetTextureUploadTarget()); if (glTextureTarget == GL_UNKNOWN_MGL) { - glTextureTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()); + glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget()); } backendTextureObject->Bind(glTextureTarget); g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget, diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index c1a82b6e..3e1e9b0a 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -15,6 +15,7 @@ #include "MG_State/GLState/TextureState/TextureEnum.h" #include #include +#include namespace MobileGL::MG_Backend::DirectGLES { String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType); @@ -254,10 +255,48 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace TextureImpl { inline Bool IsSupportedTextureTarget(TextureTarget target) { - if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle || - target == TextureTarget::Texture1DArray || target == TextureTarget::Texture2DArray) - return false; - return true; + // Rectangle textures need non-normalized sampling ES cannot express; everything else is + // either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see + // MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and + // coordinate padding for 1D/1D-array shaders. + return target != TextureTarget::TextureRectangle; + } + + // ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays + // (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation. + inline TextureTarget MapToBackendTextureTarget(TextureTarget target) { + switch (target) { + case TextureTarget::Texture1D: + return TextureTarget::Texture2D; + case TextureTarget::Texture1DArray: + return TextureTarget::Texture2DArray; + default: + return target; + } + } + + inline GLenum ConvertTextureTargetToBackendGLEnum(TextureTarget target) { + return MG_Util::ConvertTextureTargetToGLEnum(MapToBackendTextureTarget(target)); + } + + inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) { + switch (uploadTarget) { + case TextureUploadTarget::Texture1D: + return GL_TEXTURE_2D; + case TextureUploadTarget::Texture1DArray: + return GL_TEXTURE_2D_ARRAY; + default: + return MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + } + } + + // 1D arrays store layers in the state-side height; the ES 2D-array image keeps height 1 and + // moves the layer count into depth. + inline IntVec3 GetBackendUploadSize(TextureTarget stateTarget, const IntVec3& texelSize) { + if (stateTarget == TextureTarget::Texture1DArray) { + return {texelSize.x(), 1, texelSize.y()}; + } + return texelSize; } inline Bool IsMultisampleTextureTarget(TextureTarget target) { diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index f446e25a..17f33db9 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -557,38 +558,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } namespace { - // Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa - // bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero, - // +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value - // clamp to it. The mantissa is truncated (rounding mode is implementation-defined). - Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) { - const Uint32 bits = std::bit_cast(value); - const Bool negative = (bits & 0x80000000u) != 0; - const Uint32 exponent = (bits >> 23) & 0xFFu; - const Uint32 mantissa = bits & 0x7FFFFFu; - const Uint32 exponentMask = 0x1Fu << mantissaBits; - if (exponent == 0xFFu) { - if (mantissa != 0) { - return exponentMask | 1u; // NaN keeps NaN - } - return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf - } - if (negative) { - return 0u; - } - const Int32 smallExponent = static_cast(exponent) - 127 + 15; - if (smallExponent >= 31) { // above the largest finite value -> clamp to it - return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u); - } - if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero - const Uint32 fullMantissa = mantissa | 0x800000u; - const Int32 shift = (23 - mantissaBits) + 1 - smallExponent; - return shift > 23 ? 0u : fullMantissa >> shift; - } - return (static_cast(smallExponent) << mantissaBits) | - (mantissa >> (23u - static_cast(mantissaBits))); - } - void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) { switch (byteSize) { case 1: { @@ -608,43 +577,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } } // namespace - Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); } - Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); } - - // RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm - // (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31). - Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { - constexpr Int kMantissaBits = 9; - constexpr Int kExponentBias = 15; - constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B) - - Float clamped[3]; - for (Int i = 0; i < 3; ++i) { - const Float v = rgb[i]; - clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax); - } - const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2])); - - Int sharedExponent = 0; // all-zero input keeps the all-zero word - if (maxComponent > 0.0f) { - sharedExponent = std::max(-kExponentBias - 1, static_cast(std::floor(std::log2(maxComponent)))) + - 1 + kExponentBias; - const Float maxScaled = std::floor( - maxComponent / std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)) + - 0.5f); - if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead - ++sharedExponent; - } - } - - const Float scale = std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)); - Uint32 word = static_cast(sharedExponent) << 27; - for (Int i = 0; i < 3; ++i) { - const auto field = static_cast(std::floor(clamped[i] / scale + 0.5f)); - word |= std::min(field, 511u) << (i * kMantissaBits); - } - return word; - } + // Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion + // (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests. + Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); } + Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); } + Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); } void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType, const ReadbackChannelMapping& mapping, GLenum type) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index ef48452e..67ff8eed 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2150,8 +2150,8 @@ void main() { if (!supported) { // SetupDraw's pre-flight should have rejected this already; never upload a null payload. MGLOG_E("UploadAndBindVertexStreams skipped: unsupported current generic vertex attribute type: " - "program=%u location=%u type=0x%x", - program.GetExternalIndex(), location, glType); + "location=%u type=0x%x", + location, glType); return false; } diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index f987cdfa..8e00e400 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -60,6 +60,87 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } + // Whether the backend can actually attach this color format to a framebuffer. Preferred + // source of truth is the backend's probed format-capability cache (real glCheckFramebufferStatus + // probes, so extensions like EXT_render_snorm are respected). Formats a probe-less backend + // cannot answer for fall back to a conservative static list of formats no ES driver renders to: + // shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats. + // Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so + // reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal. + Bool IsColorInternalFormatRenderable(TextureInternalFormat format) { + const SizeT formatIndex = static_cast(format); + if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) { + const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities(); + const SizeT sentinelFormat = static_cast(TextureInternalFormat::RGBA8); + Bool cachePopulated = false; + for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount && !cachePopulated; + ++targetIndex) { + cachePopulated = MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][sentinelFormat], + MG_Backend::FormatCapability::Creatable); + } + if (cachePopulated) { + for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount; + ++targetIndex) { + if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex], + MG_Backend::FormatCapability::FramebufferRenderable) || + MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex], + MG_Backend::FormatCapability::FramebufferRenderable)) { + return true; + } + } + return false; + } + } + switch (format) { + case TextureInternalFormat::RGB9E5: + case TextureInternalFormat::R8Snorm: + case TextureInternalFormat::RG8Snorm: + case TextureInternalFormat::RGB8Snorm: + case TextureInternalFormat::RGBA8Snorm: + case TextureInternalFormat::R16Snorm: + case TextureInternalFormat::RG16Snorm: + case TextureInternalFormat::RGB16Snorm: + case TextureInternalFormat::RGBA16Snorm: + case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGB10: // stored as RGB16 + case TextureInternalFormat::RGB12: // stored as RGB16 + case TextureInternalFormat::RGB16F: + case TextureInternalFormat::RGB32F: + case TextureInternalFormat::RGB8I: + case TextureInternalFormat::RGB8UI: + case TextureInternalFormat::RGB16I: + case TextureInternalFormat::RGB16UI: + case TextureInternalFormat::RGB32I: + case TextureInternalFormat::RGB32UI: + case TextureInternalFormat::SRGB8: + return false; + default: + return true; + } + } + + Bool HasNonRenderableColorAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) { + const auto& attachments = framebufferObject.GetAllAttachmentObjects(); + for (SizeT i = 0; i < attachments.size(); ++i) { + const auto type = static_cast(i); + if (type < FramebufferAttachmentType::Color0 || type > FramebufferAttachmentType::Color31) { + continue; + } + const auto& attachment = attachments[i]; + if (!attachment.IsValid()) continue; + TextureInternalFormat format = TextureInternalFormat::Unknown; + if (attachment.IsTexture() && attachment.GetTexture()) { + format = attachment.GetTexture()->GetFormat(); + } else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) { + format = attachment.GetRenderbuffer()->GetInternalFormat(); + } + if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) { + return true; + } + } + return false; + } + void RecordUnsupportedFramebufferTextureAttachmentError(const char* functionName, const char* detail) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -556,6 +637,62 @@ namespace MobileGL::MG_Impl::GLImpl { return framebufferObject; } + // Attaches a single layer/slice of a 3D or array texture. The attachment model stores the layer + // index; the DirectGLES backend attaches it with glFramebufferTextureLayer. + static void AttachFramebufferTextureLayer(const char* functionName, GLenum target, GLenum attachment, + GLuint texture, GLint level, GLint layer, + TextureUploadTarget textureUploadTarget) { + if (target == GL_FRAMEBUFFER) { + target = GL_DRAW_FRAMEBUFFER; + } + if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) { + AttachFramebufferTextureLayer(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, layer, + textureUploadTarget); + AttachFramebufferTextureLayer(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, layer, + textureUploadTarget); + return; + } + + const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment); + const FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target); + if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return; + if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return; + if (!TextureImpl::ValidateTextureName(texture, true)) return; + + auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget); + auto& framebufferObject = bindingSlot.GetBoundObject(); + if (!framebufferObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", functionName, + "Framebuffer target is bound to no framebuffer object.")); + return; + } + + if (texture == 0) { + framebufferObject->Detach(attachmentType); + return; + } + + auto& textureObject = MG_State::pGLContext->GetTextureObject(texture); + if (!textureObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", functionName, + std::format("Texture object {} is not valid.", texture))); + return; + } + if (layer < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, "Layer must be non-negative.")); + return; + } + + framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer, + /*layered=*/false); + } + void FramebufferTextureLayer_State(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) { if (texture == 0) { const TextureUploadTarget detachTarget = TextureUploadTarget::Texture2D; @@ -563,10 +700,31 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - static_cast(layer); - RecordUnsupportedFramebufferTextureAttachmentError( - __func__, - "Layered framebuffer texture attachments are not represented by the current framebuffer attachment model."); + auto& textureObject = MG_State::pGLContext->GetTextureObject(texture); + if (!textureObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + std::format("Texture object {} is not valid.", texture))); + return; + } + TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown; + switch (textureObject->GetTarget()) { + case TextureTarget::Texture3D: + textureUploadTarget = TextureUploadTarget::Texture3D; + break; + case TextureTarget::Texture2DArray: + textureUploadTarget = TextureUploadTarget::Texture2DArray; + break; + case TextureTarget::Texture2DMultisampleArray: + textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray; + break; + default: + RecordUnsupportedFramebufferTextureAttachmentError( + __func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture."); + return; + } + AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget); } void FramebufferTexture3D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level, @@ -578,10 +736,15 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - static_cast(zoffset); - RecordUnsupportedFramebufferTextureAttachmentError( - __func__, - "3D framebuffer texture slice attachments are not represented by the current framebuffer attachment model."); + if (textarget != GL_TEXTURE_3D) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "FramebufferTexture3D requires GL_TEXTURE_3D.")); + return; + } + AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, zoffset, + TextureUploadTarget::Texture3D); } void FramebufferTexture2D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) { @@ -1252,6 +1415,9 @@ namespace MobileGL::MG_Impl::GLImpl { GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; } + if (HasNonRenderableColorAttachment(*framebufferObject)) { + return GL_FRAMEBUFFER_UNSUPPORTED; + } if (IsActiveBackendDirectVulkan() && IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) { return GL_FRAMEBUFFER_UNSUPPORTED; @@ -1277,6 +1443,9 @@ namespace MobileGL::MG_Impl::GLImpl { GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; } + if (HasNonRenderableColorAttachment(*framebufferObject)) { + return GL_FRAMEBUFFER_UNSUPPORTED; + } if (IsActiveBackendDirectVulkan() && IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) { return GL_FRAMEBUFFER_UNSUPPORTED; @@ -1626,8 +1795,8 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } - // Check framebuffer completeness - if (!framebufferObject->CheckCompleteness()) { + // Check framebuffer completeness (including formats the ES pipeline cannot attach) + if (!framebufferObject->CheckCompleteness() || HasNonRenderableColorAttachment(*framebufferObject)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidFramebufferOperation, MakeUnique("MG_Impl/GLImpl", "ReadPixels_State", "Framebuffer is incomplete")); @@ -1679,6 +1848,26 @@ namespace MobileGL::MG_Impl::GLImpl { "No color buffer for color format")); return false; } + + // GL 3.3 section 4.3.1: GL_INVALID_OPERATION if format is an integer format and the read + // buffer is not an integer format, or vice versa (GL CTS packed_pixels expects the error + // for every *_INTEGER readback from a normalized attachment). + const auto& readAttachment = framebufferObject->GetAttachment(readBuffer); + TextureInternalFormat attachmentFormat = TextureInternalFormat::Unknown; + if (readAttachment.IsTexture() && readAttachment.GetTexture()) { + attachmentFormat = readAttachment.GetTexture()->GetFormat(); + } else if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) { + attachmentFormat = readAttachment.GetRenderbuffer()->GetInternalFormat(); + } + if (attachmentFormat != TextureInternalFormat::Unknown && + TextureImpl::IsIntegerColorInputFormat(textureInputFormat) != + TextureImpl::IsIntegerColorInternalFormat(attachmentFormat)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "ReadPixels_State", + "Integer-ness of format does not match the read buffer")); + return false; + } } // Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp index 0ae604b6..360c8bc3 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp @@ -76,7 +76,8 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl { } Bool ValidateRenderbufferName(Uint index, Bool allowZero) { - if (index == 0 && !allowZero) { + if (index == 0) { + if (allowZero) return true; // unbind / detach never needs a live object MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName", diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 6ebe9a6a..816f6135 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -1894,7 +1894,11 @@ namespace MobileGL::MG_Impl::GLImpl { *params = dynamicParameters.MaxTextureSize; break; case GL_MAX_UNIFORM_BUFFER_BINDINGS: - *params = std::max(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings); + // Never advertise more bindings than the state layer's indexed-binding array can track + // (BufferState::BufferBindingPointCount): the GL CTS state reset calls glBindBufferBase + // on every advertised index and expects no error. + *params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings, + static_cast(MG_State::GLState::BufferBindingPointCount)); break; case GL_MAX_UNIFORM_BLOCK_SIZE: *params = dynamicParameters.MaxUniformBlockSize; diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index d0e958ea..8d74332b 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -404,13 +404,8 @@ namespace MobileGL::MG_Impl::GLImpl { "2D multisample textures must use depth 1.")); return false; } - if (textureTarget == TextureTarget::Texture2DMultisampleArray && depth == 0) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidValue, - MakeUnique("MG_Impl/GLImpl", caller, - "2D multisample array textures must have at least one layer.")); - return false; - } + // depth == 0 (like width/height == 0) deallocates the image and is not an error: + // the GL CTS state reset calls TexImage3DMultisample with all-zero sizes. const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat); if (samples > maxSamples) { @@ -557,6 +552,14 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_A: { auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname); auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param); + if (swizzleValue == TextureSwizzleParam::Unknown) { + // GL CTS texture_swizzle.api_errors: single-value TexParameter* with a value outside + // [RED, GREEN, BLUE, ALPHA, ZERO, ONE] must raise GL_INVALID_ENUM. + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", caller, "Invalid texture swizzle value.")); + return; + } textureObject->SetSwizzleParam(swizzleParam, swizzleValue); break; } @@ -734,6 +737,22 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture + // (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset + // sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay + // clean). Parameters set on default textures are accepted as a silent no-op. + const SharedPtr& GetTextureObjectByTargetForParameter( + TextureUploadTarget textureUploadTarget, TextureTarget textureTarget) { + if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) { + return TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget); + } + if (textureTarget == TextureTarget::Unknown) { + return nullTextureObject; + } + auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); + return activeUnit.GetBindingSlot(textureTarget).GetBoundObject(); + } + void GenerateMipmap_Backend(GLenum target) { MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target); } @@ -1041,7 +1060,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; switch (pname) { @@ -1074,6 +1093,12 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_A: { auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname); auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param); + if (swizzleValue == TextureSwizzleParam::Unknown) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, "Invalid texture swizzle value.")); + return; + } textureObject->SetSwizzleParam(swizzleParam, swizzleValue); break; } @@ -1120,7 +1145,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; TextureParameterObject_State(textureObject, pname, param, __func__); @@ -1136,7 +1161,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; SetTextureBorderColorFromFloats(textureObject, params); break; @@ -1144,7 +1169,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_RGBA: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; GLint signedParams[4] = {static_cast(params[0]), static_cast(params[1]), static_cast(params[2]), static_cast(params[3])}; @@ -1167,7 +1192,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; SetTextureBorderColorFromInts(textureObject, params); break; @@ -1175,7 +1200,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_RGBA: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) { return; @@ -1193,7 +1218,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_BORDER_COLOR: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; SetTextureBorderColorFromIntegerInts(textureObject, params); break; @@ -1201,7 +1226,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_RGBA: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; GLint signedParams[4] = {static_cast(params[0]), static_cast(params[1]), static_cast(params[2]), static_cast(params[3])}; @@ -1221,7 +1246,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_BORDER_COLOR: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; SetTextureBorderColorFromUnsignedInts(textureObject, params); break; @@ -1232,7 +1257,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; Vec4 swizzleParams; @@ -1283,6 +1308,9 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureObject = isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) : bindingSlot.GetBoundObject(); + // Respecifying the default texture (name 0) is legal GL (the GL CTS state reset resets it to + // zero size); accept it as a silent no-op since default textures carry no storage here. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (textureObject->GetStorageType() != TextureStorageType::Mipmap) { MG_State::pGLContext->RecordError( @@ -1324,6 +1352,9 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureObject = isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) : bindingSlot.GetBoundObject(); + // Respecifying the default texture (name 0) is legal GL (the GL CTS state reset resets it to + // zero size); accept it as a silent no-op since default textures carry no storage here. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (textureObject->GetStorageType() != TextureStorageType::Mipmap) { MG_State::pGLContext->RecordError( @@ -1402,6 +1433,9 @@ namespace MobileGL::MG_Impl::GLImpl { : bindingSlot.GetBoundObject(); // ===================== Error Checking ============================== + // Respecifying the default texture (name 0) is legal GL (the GL CTS state reset resets every + // default texture to zero size); accept it as a silent no-op. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!ValidateTextureMutable(textureObject, __func__)) return; @@ -1521,6 +1555,9 @@ namespace MobileGL::MG_Impl::GLImpl { : bindingSlot.GetBoundObject(); // ===================== Error Checking ============================== + // Respecifying the default texture (name 0) is legal GL (the GL CTS state reset resets every + // default texture to zero size); accept it as a silent no-op. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!ValidateTextureMutable(textureObject, __func__)) return; @@ -1625,6 +1662,8 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureObject = isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) : bindingSlot.GetBoundObject(); + // Respecifying the default texture (name 0) is legal GL; accept it as a silent no-op. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!ValidateTextureMutable(textureObject, __func__)) return; @@ -1680,13 +1719,19 @@ namespace MobileGL::MG_Impl::GLImpl { if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return; // TODO: make sure `internalformat` is in one of supported format for TexBuffer - auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer); - if (!bufferObject) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", __func__, - "`buffer` is not zero and is not the name of an existing buffer object.")); - return; + // buffer == 0 is a legal detach (the GL CTS state reset calls glTexBuffer(..., 0) and + // expects no error); only a non-zero name that does not exist is GL_INVALID_OPERATION. + SharedPtr bufferObject; + if (buffer != 0) { + bufferObject = MG_State::pGLContext->GetBufferObject(buffer); + if (!bufferObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", __func__, + "`buffer` is not zero and is not the name of an existing buffer object.")); + return; + } } // ======================= Processing ================================ @@ -1695,6 +1740,8 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureObject = bindingSlot.GetBoundObject(); // ===================== Error Checking ============================== + // Detaching from the default texture (name 0) is a silent no-op. + if (!textureObject && buffer == 0) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (textureObject->GetStorageType() != TextureStorageType::Buffer) { MG_State::pGLContext->RecordError( @@ -2572,14 +2619,22 @@ namespace MobileGL::MG_Impl::GLImpl { void ActiveTexture_State(GLenum texture) { // ===================== Error Checking ============================== - if (texture < GL_TEXTURE0 || texture > GL_TEXTURE31) { + // The valid range is [GL_TEXTURE0, GL_TEXTURE0 + GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS): + // the GL CTS state reset iterates every advertised combined unit, so rejecting units the + // implementation itself reports would leave a sticky GL_INVALID_ENUM behind. + Int maxCombinedUnits = static_cast(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); + if (MG_Backend::pActiveBackendObject) { + maxCombinedUnits = std::min( + maxCombinedUnits, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits); + } + if (texture < GL_TEXTURE0 || static_cast(texture - GL_TEXTURE0) >= maxCombinedUnits) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique( "MG_Impl/GLImpl", "ActiveTexture_State", - std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to 31, but got " + std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, but got " "invalid enum: 0x{:X}, which may stand for unit {}.", - texture, texture - GL_TEXTURE0))); + maxCombinedUnits - 1, texture, texture - GL_TEXTURE0))); return; } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index f9713609..783b8e6c 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -175,7 +175,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } - static Bool IsIntegerColorInputFormat(TextureInputFormat format) { + Bool IsIntegerColorInputFormat(TextureInputFormat format) { return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger || format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger || format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger || @@ -183,7 +183,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { format == TextureInputFormat::AlphaInteger; } - static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) { + Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) { switch (internalFormat) { case TextureInternalFormat::R8I: case TextureInternalFormat::R8UI: diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index 32291304..e1152df6 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -23,6 +23,8 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { Bool ValidateTextureSizeRange(Int width, Int height, Int depth); Bool ValidateTextureInternalFormat(TextureInternalFormat format); Bool ValidateTextureBorderNumber(Int border); + Bool IsIntegerColorInputFormat(TextureInputFormat format); + Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat); Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type); Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format, TextureInternalFormat internalFormat, diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp index 9e40c3b0..c72e7442 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp @@ -78,15 +78,10 @@ namespace MobileGL::MG_Impl::GLImpl { } static bool ValidateCurrentVertexAttribIndex(GLuint index, const char* funcName) { - if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return false; - if (index == 0) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", funcName, - "Generic vertex attribute 0 current value cannot be modified.")); - return false; - } - return true; + // Core GL allows setting the current value of every generic attribute, including 0 + // (the GL CTS state reset calls glVertexAttrib4f(0, ...) and expects no error). + static_cast(funcName); + return VertexArrayImpl::ValidateVertexAttributeIndex(index); } static bool TryGetVertexAttribute(GLuint index, const MG_State::GLState::VertexAttribute** outAttr) { diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index c3b24c5e..c5d2c062 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -674,3 +674,86 @@ TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) { EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_5_9_9_9_REV), 0u); EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_10F_11F_11F_REV), 0u); } + +// ---- GL CTS packed_pixels readback root-cause regressions -------------------------------------- + +TEST_F(FramebufferTest, ReadPixelsRejectsIntegerFormatMismatchWithReadBuffer) { + GLuint framebuffer = 0; + GLuint texture = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); + MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8UI, 4, 4); + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0); + MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + + MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels; + Uint8 pixelStorage[4 * 4 * 4] = {}; + + // GL 3.3 section 4.3.1: normalized format on an integer read buffer -> GL_INVALID_OPERATION + // (GL CTS packed_pixels expects the error for every mismatched combination). + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // The matching integer readback stays valid. + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // And the inverse mismatch: integer format on a normalized attachment. + GLuint normalizedTexture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &normalizedTexture); + MG_Impl::GLImpl::TextureStorage2D(normalizedTexture, 1, GL_RGBA8, 4, 4); + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, normalizedTexture, 0); + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); +} + +TEST_F(FramebufferTest, BindRenderbufferZeroUnbindsWithoutError) { + // The GL CTS state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) and expects no error; + // name 0 used to be reported as an invalid renderbuffer name. + MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(FramebufferTest, FramebufferTexture3DAttachesSliceWithLayerTracking) { + // glFramebufferTexture3D with zoffset used to be rejected outright, leaving a sticky + // GL_INVALID_OPERATION behind (GL CTS packed_pixels varied_rectangle runs on GL_TEXTURE_3D). + GLuint framebuffer = 0; + GLuint texture = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture); + MG_Impl::GLImpl::TextureStorage3D(texture, 1, GL_RGBA8, 4, 4, 2); + + MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer); + MG_Impl::GLImpl::FramebufferTexture3D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, 0, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer); + ASSERT_NE(framebufferObject, nullptr); + const auto& attachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Color0); + ASSERT_TRUE(attachment.IsTexture()); + EXPECT_EQ(attachment.GetTextureLayer(), 1); + EXPECT_FALSE(attachment.IsLayered()); +} + +TEST_F(FramebufferTest, NonRenderableColorFormatsReportUnsupportedFramebuffer) { + // Without a probing backend the conservative list applies: RGB9_E5 is texture-only, so + // attaching it must not report GL_FRAMEBUFFER_COMPLETE (GL CTS packed_pixels rgb9_e5 expects + // read errors instead of silent unwritten readbacks). + GLuint framebuffer = 0; + GLuint texture = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); + MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGB9_E5, 4, 4); + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0); + MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + + EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_READ_FRAMEBUFFER), + static_cast(GL_FRAMEBUFFER_UNSUPPORTED)); + + Uint8 pixelStorage[4 * 4 * 4] = {}; + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_FRAMEBUFFER_OPERATION); +} diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index ab950d29..3cdb734c 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -17,7 +17,11 @@ #include #include #include +#include +#include +#include #include +#include using namespace MobileGL; @@ -1174,3 +1178,221 @@ TEST_F(TextureTest, NormalizeDepth24Stencil8UsesPackedDepthStencilType) { EXPECT_EQ(format, GL_DEPTH_STENCIL); EXPECT_EQ(type, GL_UNSIGNED_INT_24_8); } + +// ---- GL CTS packed_pixels / texture_swizzle readback root-cause regressions -------------------- + +TEST_F(TextureTest, NormalizeLegacySizedFormatsMapToCanonicalShadowLayouts) { + struct Case { + GLenum requested; + GLenum internalFormat; + GLenum format; + GLenum type; + }; + const Case cases[] = { + // Legacy <=8-bit-per-channel formats store as UNorm8 component arrays. + {GL_R3_G3_B2, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE}, + {GL_RGB4, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE}, + {GL_RGB5, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE}, + {GL_RGBA2, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE}, + {GL_RGBA4, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE}, + {GL_RGB5_A1, GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_BYTE}, + // 10/12-bit channels store as UNorm16 component arrays. + {GL_RGB10, GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT}, + {GL_RGB12, GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT}, + {GL_RGBA12, GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT}, + // RGB10_A2UI keeps its native packed layout (was previously unhandled -> broken uploads). + {GL_RGB10_A2UI, GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV}, + }; + for (const auto& testCase : cases) { + GLenum internalFormat = 0; + GLenum format = 0; + GLenum type = 0; + MG_Util::TextureFormatProcessor::NormalizePixelFormat(testCase.requested, + PixelFormatNormalizeOptionBit::None, + &internalFormat, &format, &type); + EXPECT_EQ(internalFormat, testCase.internalFormat) << "requested 0x" << std::hex << testCase.requested; + EXPECT_EQ(format, testCase.format) << "requested 0x" << std::hex << testCase.requested; + EXPECT_EQ(type, testCase.type) << "requested 0x" << std::hex << testCase.requested; + } +} + +TEST_F(TextureTest, ConvertsUnsignedInt1010102PixelDataType) { + // GL CTS packed_pixels uploads/reads GL_UNSIGNED_INT_10_10_10_2; the GL->MG mapping was missing, + // rejecting every valid combination as GL_INVALID_ENUM. + EXPECT_EQ(MG_Util::ConvertGLEnumToTexturePixelDataType(GL_UNSIGNED_INT_10_10_10_2), + TexturePixelDataType::UnsignedInt1010102); +} + +TEST_F(TextureTest, TexParameteriRejectsInvalidSwizzleValue) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + // GL CTS texture_swizzle.api_errors: values outside [RED, GREEN, BLUE, ALPHA, ZERO, ONE] + // must raise GL_INVALID_ENUM through the single-value TexParameteri path. + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RGB); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, -1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM); + + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_ALPHA); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, DefaultTextureOperationsAreSilentNoOps) { + // The GL CTS state reset drives texture name 0 through TexParameter*/TexImage*/TexBuffer for + // every unit and target and expects glGetError() to stay clean. + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage2DEncodesPackedInternalShadowWords) { + // RGB10_A2 / RGB9_E5 / R11F_G11F_B10F shadow bytes hold the ES upload word; uploads from + // component client data must encode instead of raw-copying (GL CTS packed_pixels rgb10_a2, + // rgb9_e5, r11f_g11f_b10f data comparisons). + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 rgba8[] = {255, 0, 0, 255}; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba8); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + { + const auto* stored = GetBoundTexture2DLevelBytes(texture); + Uint32 word = 0; + std::memcpy(&word, stored, sizeof(word)); + EXPECT_EQ(word & 0x3FFu, 1023u); // red = 1.0 + EXPECT_EQ((word >> 10) & 0x3FFu, 0u); // green = 0 + EXPECT_EQ((word >> 20) & 0x3FFu, 0u); // blue = 0 + EXPECT_EQ((word >> 30) & 0x3u, 3u); // alpha = 1.0 + } + + const Float rgb[] = {1.0f, 0.5f, 0.25f}; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 1, 1, 0, GL_RGB, GL_FLOAT, rgb); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + { + const auto* stored = GetBoundTexture2DLevelBytes(texture); + Uint32 word = 0; + std::memcpy(&word, stored, sizeof(word)); + EXPECT_EQ(word, MG_Util::EncodeSharedExponentRGB9E5(rgb)); + } + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R11F_G11F_B10F, 1, 1, 0, GL_RGB, GL_FLOAT, rgb); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + { + const auto* stored = GetBoundTexture2DLevelBytes(texture); + Uint32 word = 0; + std::memcpy(&word, stored, sizeof(word)); + const Uint32 expected = MG_Util::EncodeFloatToUnsignedF11(rgb[0]) | + (MG_Util::EncodeFloatToUnsignedF11(rgb[1]) << 11) | + (MG_Util::EncodeFloatToUnsignedF10(rgb[2]) << 22); + EXPECT_EQ(word, expected); + } + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, BoundTexImage2DDecodesPackedFloatSourceTypes) { + // 5_9_9_9_REV / 10F_11F_11F_REV client data uploaded into a component internal format must be + // decoded per texel (GL CTS packed_pixels uploads every RGB internal format with these types). + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Float rgb[] = {1.0f, 0.5f, 0.25f}; + const Uint32 word = MG_Util::EncodeSharedExponentRGB9E5(rgb); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, &word); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + EXPECT_EQ(stored[0], 255); // 1.0 + EXPECT_EQ(stored[1], 128); // 0.5 + EXPECT_EQ(stored[2], 64); // 0.25 + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, UnpackSwapBytesSwapsComponentsNotWholePixels) { + // GL_UNPACK_SWAP_BYTES on the identity-layout copy path used to reverse the whole pixel + // (4 bytes for GL_RG16), garbling multi-component rows (GL CTS packed_pixels varied_rectangle + // GL_UNPACK_SWAP_BYTES cases). + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint16 swapped[] = {0x3412, 0x7856}; // byte-swapped {0x1234, 0x5678} + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SWAP_BYTES, GL_TRUE); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RG16, 1, 1, 0, GL_RG, GL_UNSIGNED_SHORT, swapped); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + Uint16 red = 0; + Uint16 green = 0; + std::memcpy(&red, stored, sizeof(red)); + std::memcpy(&green, stored + 2, sizeof(green)); + EXPECT_EQ(red, 0x1234); + EXPECT_EQ(green, 0x5678); + + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SWAP_BYTES, GL_FALSE); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, DecodeShadowDataToWideRGBACoversComponentAndPackedLayouts) { + // GetTexImage of non-renderable formats reads the CPU shadow; the decode must cover both + // component-array and packed internal layouts. + Vector wide; + Bool isInteger = false; + Bool isSigned = false; + + const Uint8 r8[] = {128}; + ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::R8, r8, 1, wide, + isInteger, isSigned)); + EXPECT_FALSE(isInteger); + { + Float rgba[4]; + std::memcpy(rgba, wide.data(), sizeof(rgba)); + EXPECT_NEAR(rgba[0], 128.0f / 255.0f, 1e-6f); + EXPECT_EQ(rgba[1], 0.0f); + EXPECT_EQ(rgba[2], 0.0f); + EXPECT_EQ(rgba[3], 1.0f); + } + + const Float rgb[] = {1.0f, 0.5f, 0.25f}; + const Uint32 e5Word = MG_Util::EncodeSharedExponentRGB9E5(rgb); + ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::RGB9E5, &e5Word, 1, + wide, isInteger, isSigned)); + EXPECT_FALSE(isInteger); + { + Float rgba[4]; + std::memcpy(rgba, wide.data(), sizeof(rgba)); + EXPECT_NEAR(rgba[0], 1.0f, 1.0f / 256.0f); + EXPECT_NEAR(rgba[1], 0.5f, 1.0f / 256.0f); + EXPECT_NEAR(rgba[2], 0.25f, 1.0f / 256.0f); + EXPECT_EQ(rgba[3], 1.0f); + } + + const Uint32 uiWord = 1023u | (511u << 10) | (255u << 20) | (2u << 30); // RGB10_A2UI + ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::RGB10A2UI, &uiWord, 1, + wide, isInteger, isSigned)); + EXPECT_TRUE(isInteger); + EXPECT_FALSE(isSigned); + { + Uint32 rgba[4]; + std::memcpy(rgba, wide.data(), sizeof(rgba)); + EXPECT_EQ(rgba[0], 1023u); + EXPECT_EQ(rgba[1], 511u); + EXPECT_EQ(rgba[2], 255u); + EXPECT_EQ(rgba[3], 2u); + } +} diff --git a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp index 0ce7ba85..dc9736a8 100644 --- a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp +++ b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp @@ -1107,9 +1107,10 @@ TEST_F(GeneralVertexArrayTest, CurrentAttrib_PackedValidation) { GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out); EXPECT_FLOAT_EQ(out[0], 42.0f); // unchanged by the failed call - // Attribute 0 is rejected by MobileGL policy (GL_INVALID_OPERATION). + // Attribute 0's current value is settable in core GL (the GL CTS state reset calls + // glVertexAttrib4f(0, ...) on every attribute and expects no error). VertexAttribP4ui(0, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u); - EXPECT_EQ(GetError(), GL_INVALID_OPERATION); + EXPECT_EQ(GetError(), GL_NO_ERROR); // Out-of-range index -> GL_INVALID_VALUE. VertexAttribP4ui(VertexArrayImpl::GetMaxVertexAttribs(), GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u); diff --git a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp index 42ad434f..1b1dc8f5 100644 --- a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp @@ -308,6 +308,8 @@ namespace MobileGL { return TexturePixelDataType::UnsignedInt8888; case GL_UNSIGNED_INT_8_8_8_8_REV: return TexturePixelDataType::UnsignedInt8888Rev; + case GL_UNSIGNED_INT_10_10_10_2: + return TexturePixelDataType::UnsignedInt1010102; case GL_UNSIGNED_INT_10F_11F_11F_REV: return TexturePixelDataType::UnsignedInt101111Rev; case GL_UNSIGNED_INT_2_10_10_10_REV: diff --git a/MobileGL/MG_Util/Math/SmallFloat.h b/MobileGL/MG_Util/Math/SmallFloat.h new file mode 100644 index 00000000..e63c2f20 --- /dev/null +++ b/MobileGL/MG_Util/Math/SmallFloat.h @@ -0,0 +1,116 @@ +// MobileGL - MobileGL/MG_Util/Math/SmallFloat.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include + +#include +#include +#include + +namespace MobileGL::MG_Util { + // Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa + // bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero, + // +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value + // clamp to it. The mantissa is truncated (rounding mode is implementation-defined). + inline Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) { + const Uint32 bits = std::bit_cast(value); + const Bool negative = (bits & 0x80000000u) != 0; + const Uint32 exponent = (bits >> 23) & 0xFFu; + const Uint32 mantissa = bits & 0x7FFFFFu; + const Uint32 exponentMask = 0x1Fu << mantissaBits; + if (exponent == 0xFFu) { + if (mantissa != 0) { + return exponentMask | 1u; // NaN keeps NaN + } + return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf + } + if (negative) { + return 0u; + } + const Int32 smallExponent = static_cast(exponent) - 127 + 15; + if (smallExponent >= 31) { // above the largest finite value -> clamp to it + return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u); + } + if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero + const Uint32 fullMantissa = mantissa | 0x800000u; + const Int32 shift = (23 - mantissaBits) + 1 - smallExponent; + return shift > 23 ? 0u : fullMantissa >> shift; + } + return (static_cast(smallExponent) << mantissaBits) | + (mantissa >> (23u - static_cast(mantissaBits))); + } + + inline Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); } + inline Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); } + + // Decodes an unsigned small float (5-bit exponent, bias 15, mantissaBits mantissa bits). + inline Float DecodeUnsignedSmallFloatToFloat(Uint32 field, Int mantissaBits) { + const Uint32 exponent = (field >> mantissaBits) & 0x1Fu; + const Uint32 mantissa = field & ((1u << mantissaBits) - 1u); + const Float mantissaScale = 1.0f / static_cast(1u << mantissaBits); + if (exponent == 0) { + return std::exp2(-14.0f) * static_cast(mantissa) * mantissaScale; + } + if (exponent == 31) { + return mantissa == 0 ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + } + return std::exp2(static_cast(exponent) - 15.0f) * + (1.0f + static_cast(mantissa) * mantissaScale); + } + + inline Float DecodeUnsignedF11ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 6); } + inline Float DecodeUnsignedF10ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 5); } + + // RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm + // (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31). + inline Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { + constexpr Int kMantissaBits = 9; + constexpr Int kExponentBias = 15; + constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B) + + Float clamped[3]; + for (Int i = 0; i < 3; ++i) { + const Float v = rgb[i]; + clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax); + } + const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2])); + + Int sharedExponent = 0; // all-zero input keeps the all-zero word + if (maxComponent > 0.0f) { + sharedExponent = std::max(-kExponentBias - 1, static_cast(std::floor(std::log2(maxComponent)))) + + 1 + kExponentBias; + const Float maxScaled = std::floor( + maxComponent / std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)) + + 0.5f); + if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead + ++sharedExponent; + } + } + + const Float scale = std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)); + Uint32 word = static_cast(sharedExponent) << 27; + for (Int i = 0; i < 3; ++i) { + const auto field = static_cast(std::floor(clamped[i] / scale + 0.5f)); + word |= std::min(field, 511u) << (i * kMantissaBits); + } + return word; + } + + // RGB9E5 shared-exponent decode. + inline void DecodeSharedExponentRGB9E5(Uint32 word, Float outRgb[3]) { + constexpr Int kMantissaBits = 9; + constexpr Int kExponentBias = 15; + const Int exponent = static_cast(word >> 27) - kExponentBias - kMantissaBits; + const Float scale = std::exp2(static_cast(exponent)); + for (Int i = 0; i < 3; ++i) { + outRgb[i] = static_cast((word >> (i * kMantissaBits)) & 0x1FFu) * scale; + } + } +} // namespace MobileGL::MG_Util diff --git a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp index c345ec3d..cbeb8b92 100644 --- a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp +++ b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp @@ -15,10 +15,10 @@ namespace MobileGL { SizeT GetSizedInternalFormatSizeInBytes(TextureInternalFormat internal) { switch (internal) { case TextureInternalFormat::R8: + case TextureInternalFormat::Red: // UNorm8 shadow layout case TextureInternalFormat::R8Snorm: case TextureInternalFormat::R8I: case TextureInternalFormat::R8UI: - case TextureInternalFormat::R3G3B2: return 1; case TextureInternalFormat::R16: @@ -27,14 +27,17 @@ namespace MobileGL { case TextureInternalFormat::R16UI: case TextureInternalFormat::R16F: case TextureInternalFormat::RG8: + case TextureInternalFormat::RG: // UNorm8x2 shadow layout case TextureInternalFormat::RG8Snorm: case TextureInternalFormat::RG8I: case TextureInternalFormat::RG8UI: case TextureInternalFormat::DepthComponent16: return 2; + case TextureInternalFormat::R3G3B2: // UNorm8x3 shadow layout case TextureInternalFormat::RGB4: case TextureInternalFormat::RGB5: + case TextureInternalFormat::RGB: // UNorm8x3 shadow layout case TextureInternalFormat::RGB8: case TextureInternalFormat::RGB8Snorm: case TextureInternalFormat::SRGB8: @@ -43,11 +46,10 @@ namespace MobileGL { case TextureInternalFormat::DepthComponent24: return 3; - case TextureInternalFormat::RGB10: - case TextureInternalFormat::RGB12: case TextureInternalFormat::RGBA2: case TextureInternalFormat::RGBA4: case TextureInternalFormat::RGB5A1: + case TextureInternalFormat::RGBA: // UNorm8x4 shadow layout case TextureInternalFormat::RGBA8: case TextureInternalFormat::RGBA8Snorm: case TextureInternalFormat::RGBA8I: @@ -72,6 +74,8 @@ namespace MobileGL { return 4; case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGB10: // UNorm16x3 shadow layout + case TextureInternalFormat::RGB12: // UNorm16x3 shadow layout case TextureInternalFormat::RGB16Snorm: case TextureInternalFormat::RGB16F: case TextureInternalFormat::RGB16I: diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp index a667765c..0da4b2c6 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp @@ -8,6 +8,7 @@ #include "PixelStoreProcessor.h" #include "MG_Util/Math/HalfFloat.h" +#include "MG_Util/Math/SmallFloat.h" #include namespace MobileGL::MG_Util::PixelStoreProcessor { @@ -122,13 +123,30 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) { switch (internal) { - case TextureInternalFormat::R8: out = {1, ShadowComponent::UNorm8, false}; return true; - case TextureInternalFormat::RG8: out = {2, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::R8: + case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::RG8: + case TextureInternalFormat::RG: out = {2, ShadowComponent::UNorm8, false}; return true; case TextureInternalFormat::RGB8: + case TextureInternalFormat::RGB: case TextureInternalFormat::SRGB8: out = {3, ShadowComponent::UNorm8, false}; return true; case TextureInternalFormat::RGBA8: + case TextureInternalFormat::RGBA: case TextureInternalFormat::SRGB8Alpha8: out = {4, ShadowComponent::UNorm8, false}; return true; + // Legacy desktop-GL sized normalized formats are stored in the closest ES-legal layout + // (see TextureFormatProcessor::NormalizePixelFormat): 8-bit unorm for <=8-bit channels, + // 16-bit unorm for 10/12-bit channels. + case TextureInternalFormat::R3G3B2: + case TextureInternalFormat::RGB4: + case TextureInternalFormat::RGB5: out = {3, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::RGBA2: + case TextureInternalFormat::RGBA4: + case TextureInternalFormat::RGB5A1: out = {4, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::RGB10: + case TextureInternalFormat::RGB12: out = {3, ShadowComponent::UNorm16, false}; return true; + case TextureInternalFormat::RGBA12: out = {4, ShadowComponent::UNorm16, false}; return true; + case TextureInternalFormat::R8Snorm: out = {1, ShadowComponent::SNorm8, false}; return true; case TextureInternalFormat::RG8Snorm: out = {2, ShadowComponent::SNorm8, false}; return true; case TextureInternalFormat::RGB8Snorm: out = {3, ShadowComponent::SNorm8, false}; return true; @@ -185,12 +203,76 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { case TextureInternalFormat::RGBA32I: out = {4, ShadowComponent::Int32, true}; return true; default: - // Packed internal layouts (RGB5A1, RGB10A2, RGB9E5, ...), depth/stencil and unsized formats - // keep the legacy copy path. + // Packed internal layouts (RGB10A2, RGB9E5, ...), depth/stencil and unsized formats + // have no component-array shadow layout (packed ones are handled below). return false; } } + // Packed internal formats whose shadow bytes hold the ES upload word directly + // (GL_UNSIGNED_INT_2_10_10_10_REV / 5_9_9_9_REV / 10F_11F_11F_REV encoding, 4 bytes/texel). + enum class PackedInternalKind { + UNorm2101010Rev, // GL_RGB10_A2 + UInt2101010Rev, // GL_RGB10_A2UI + FloatR11G11B10, // GL_R11F_G11F_B10F + FloatRGB9E5, // GL_RGB9_E5 + }; + + struct InternalPackedLayout { + PackedInternalKind kind; + Int channelCount; + Bool isInteger; + }; + + Bool GetInternalPackedLayout(TextureInternalFormat internal, InternalPackedLayout& out) { + switch (internal) { + case TextureInternalFormat::RGB10A2: + out = {PackedInternalKind::UNorm2101010Rev, 4, false}; + return true; + case TextureInternalFormat::RGB10A2UI: + out = {PackedInternalKind::UInt2101010Rev, 4, true}; + return true; + case TextureInternalFormat::R11FG11FB10F: + out = {PackedInternalKind::FloatR11G11B10, 3, false}; + return true; + case TextureInternalFormat::RGB9E5: + out = {PackedInternalKind::FloatRGB9E5, 3, false}; + return true; + default: + return false; + } + } + + Uint32 EncodePackedInternalWordFloat(PackedInternalKind kind, const Float rgba[4]) { + switch (kind) { + case PackedInternalKind::UNorm2101010Rev: { + const auto field = [](Float v, Uint32 maxValue) { + return static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * static_cast(maxValue))); + }; + return field(rgba[0], 1023u) | (field(rgba[1], 1023u) << 10) | (field(rgba[2], 1023u) << 20) | + (field(rgba[3], 3u) << 30); + } + case PackedInternalKind::FloatR11G11B10: + return EncodeFloatToUnsignedF11(rgba[0]) | (EncodeFloatToUnsignedF11(rgba[1]) << 11) | + (EncodeFloatToUnsignedF10(rgba[2]) << 22); + case PackedInternalKind::FloatRGB9E5: + return EncodeSharedExponentRGB9E5(rgba); + default: + return 0; + } + } + + Uint32 EncodePackedInternalWordInt(PackedInternalKind kind, const Int64 rgba[4]) { + if (kind != PackedInternalKind::UInt2101010Rev) { + return 0; + } + const auto field = [](Int64 v, Int64 maxValue) { + return static_cast(std::clamp(v, 0, maxValue)); + }; + return field(rgba[0], 1023) | (field(rgba[1], 1023) << 10) | (field(rgba[2], 1023) << 20) | + (field(rgba[3], 3) << 30); + } + struct UnpackChannelMapping { Int formatPosition[4]; // position of R,G,B,A within the input format's component list; -1 = missing Int channelCount; @@ -301,6 +383,8 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { SizeT inputPixelSize; SizeT swapGroupSize; // UNPACK_SWAP_BYTES group: packed word size, or the component size SizeT internalPixelSize; + Bool internalIsPacked; + InternalPackedLayout internalPacked; }; // Returns true when the (format, type) -> internal-format upload needs a per-texel conversion; @@ -309,10 +393,16 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { Bool GetUnpackConversionSpec(TextureInternalFormat internal, TextureInputFormat format, TexturePixelDataType type, UnpackConversionSpec& out) { InternalShadowLayout layout{}; - if (!GetInternalShadowLayout(internal, layout)) return false; + InternalPackedLayout packedInternal{}; + const Bool hasComponentLayout = GetInternalShadowLayout(internal, layout); + const Bool hasPackedInternal = !hasComponentLayout && GetInternalPackedLayout(internal, packedInternal); + if (!hasComponentLayout && !hasPackedInternal) return false; + const Bool internalIsInteger = hasComponentLayout ? layout.isInteger : packedInternal.isInteger; + const Int internalChannelCount = hasComponentLayout ? layout.channelCount : packedInternal.channelCount; + UnpackChannelMapping mapping{}; if (!GetUnpackChannelMapping(format, mapping)) return false; - if (mapping.isInteger != layout.isInteger) return false; // rejected upstream; stay safe + if (mapping.isInteger != internalIsInteger) return false; // rejected upstream; stay safe PackedTypeLayout packed{}; const Bool isPacked = GetPackedTypeLayout(type, packed); @@ -323,6 +413,13 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { type == TexturePixelDataType::UnsignedInt8888Rev) { return false; } + // The client word already equals the packed internal word (memcpy fast path). + if (hasPackedInternal && type == TexturePixelDataType::UnsignedInt2101010Rev && + (format == TextureInputFormat::RGBA || format == TextureInputFormat::RGBAInteger) && + (packedInternal.kind == PackedInternalKind::UNorm2101010Rev || + packedInternal.kind == PackedInternalKind::UInt2101010Rev)) { + return false; + } } else { ShadowComponent direct{}; const Bool hasDirect = GetDirectShadowComponentForType(type, mapping.isInteger, direct); @@ -338,25 +435,46 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { case TexturePixelDataType::HalfFloat: if (mapping.isInteger) return false; // rejected upstream break; + case TexturePixelDataType::UnsignedInt5999Rev: + case TexturePixelDataType::UnsignedInt101111Rev: + // Packed-float RGB source words (decoded in ConvertUnpackRow); only pair with + // GL_RGB, which the state layer already enforces. + if (mapping.isInteger || mapping.channelCount != 3) return false; + // The client word already equals the packed internal word. + if (hasPackedInternal && + ((packedInternal.kind == PackedInternalKind::FloatRGB9E5 && + type == TexturePixelDataType::UnsignedInt5999Rev) || + (packedInternal.kind == PackedInternalKind::FloatR11G11B10 && + type == TexturePixelDataType::UnsignedInt101111Rev))) { + return false; + } + break; default: return false; } - if (hasDirect && direct == layout.component && mapping.channelCount == layout.channelCount && - IsIdentityChannelOrder(mapping)) { + if (hasComponentLayout && hasDirect && direct == layout.component && + mapping.channelCount == layout.channelCount && IsIdentityChannelOrder(mapping)) { return false; // input already matches the shadow layout } } out.mapping = mapping; - out.internal = layout; + out.internal = hasComponentLayout + ? layout + : InternalShadowLayout{internalChannelCount, ShadowComponent::UNorm8, internalIsInteger}; out.packed = packed; out.isPacked = isPacked; out.type = type; out.inputPixelSize = GetInputBytesPerPixel(format, type); + const Bool isPackedFloatWord = type == TexturePixelDataType::UnsignedInt5999Rev || + type == TexturePixelDataType::UnsignedInt101111Rev; out.swapGroupSize = isPacked ? static_cast(packed.totalBits / 8) - : GetBaseTexturePixelDataTypeSize(type); + : (isPackedFloatWord ? 4 : GetBaseTexturePixelDataTypeSize(type)); + out.internalIsPacked = hasPackedInternal; + out.internalPacked = packedInternal; out.internalPixelSize = - static_cast(layout.channelCount) * GetShadowComponentSize(layout.component); + hasPackedInternal ? 4 + : static_cast(layout.channelCount) * GetShadowComponentSize(layout.component); return true; } @@ -564,13 +682,36 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { rgba[ch] = DecodeComponentToInt(s + static_cast(pos) * srcComponentSize, conv.type); } } + if (conv.internalIsPacked) { + const Uint32 word = EncodePackedInternalWordInt(conv.internalPacked.kind, rgba); + Memcpy(d, &word, sizeof(word)); + continue; + } for (Int ch = 0; ch < conv.internal.channelCount; ++ch) { EncodeShadowComponentInt(d + static_cast(ch) * dstComponentSize, conv.internal.component, rgba[ch]); } } else { Float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f}; - if (conv.isPacked) { + if (conv.type == TexturePixelDataType::UnsignedInt5999Rev || + conv.type == TexturePixelDataType::UnsignedInt101111Rev) { + // Packed-float RGB source word: decode the shared-exponent / small-float fields. + Uint32 word; + Memcpy(&word, s, sizeof(word)); + Float comps[3]; + if (conv.type == TexturePixelDataType::UnsignedInt5999Rev) { + DecodeSharedExponentRGB9E5(word, comps); + } else { + comps[0] = DecodeUnsignedF11ToFloat(word & 0x7FFu); + comps[1] = DecodeUnsignedF11ToFloat((word >> 11) & 0x7FFu); + comps[2] = DecodeUnsignedF10ToFloat((word >> 22) & 0x3FFu); + } + for (Int ch = 0; ch < 4; ++ch) { + const Int pos = conv.mapping.formatPosition[ch]; + if (pos < 0 || pos >= 3) continue; + rgba[ch] = comps[pos]; + } + } else if (conv.isPacked) { const Uint32 word = ReadPackedWord(s, conv.packed.totalBits); for (Int ch = 0; ch < 4; ++ch) { const Int pos = conv.mapping.formatPosition[ch]; @@ -587,6 +728,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { DecodeComponentToFloat(s + static_cast(pos) * srcComponentSize, conv.type); } } + if (conv.internalIsPacked) { + const Uint32 word = EncodePackedInternalWordFloat(conv.internalPacked.kind, rgba); + Memcpy(d, &word, sizeof(word)); + continue; + } for (Int ch = 0; ch < conv.internal.channelCount; ++ch) { EncodeShadowComponentFloat(d + static_cast(ch) * dstComponentSize, conv.internal.component, rgba[ch]); @@ -687,9 +833,16 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { } else { Memcpy(layerDst, layerSrc, static_cast(copyWidth) * pixelSize); - if (params.SwapBytes && pixelSize > 1 && !isByteType) { - MGLOG_D("%s: SwapBytes", __func__); - SwapBytes(layerDst, pixelSize, static_cast(copyWidth)); + if (params.SwapBytes && !isByteType) { + // GL_UNPACK_SWAP_BYTES swaps within each element (component or packed + // word), never across a whole multi-component pixel. + SizeT swapGroup = GetSizedTexturePixelDataTypeSize(inputDataType); + if (swapGroup == 0) swapGroup = GetBaseTexturePixelDataTypeSize(inputDataType); + if (swapGroup > 1) { + MGLOG_D("%s: SwapBytes (group %d)", __func__, static_cast(swapGroup)); + SwapBytes(layerDst, swapGroup, + static_cast(copyWidth) * pixelSize / swapGroup); + } } if (params.LSBFirst && isBitmap) { @@ -788,4 +941,177 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { return outputPixels; } + + namespace { + Float DecodeShadowComponentToFloat(const Uint8* p, ShadowComponent component) { + switch (component) { + case ShadowComponent::UNorm8: + return static_cast(*p) / 255.0f; + case ShadowComponent::SNorm8: { + Int8 v; + Memcpy(&v, p, sizeof(v)); + return std::max(static_cast(v) / 127.0f, -1.0f); + } + case ShadowComponent::UNorm16: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return static_cast(v) / 65535.0f; + } + case ShadowComponent::SNorm16: { + Int16 v; + Memcpy(&v, p, sizeof(v)); + return std::max(static_cast(v) / 32767.0f, -1.0f); + } + case ShadowComponent::Half: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return DecodeHalfBitsToFloat(v); + } + case ShadowComponent::Float32: { + Float v; + Memcpy(&v, p, sizeof(v)); + return v; + } + default: + return 0.0f; + } + } + + Int64 DecodeShadowComponentToInt(const Uint8* p, ShadowComponent component) { + switch (component) { + case ShadowComponent::UInt8: + return *p; + case ShadowComponent::Int8: { + Int8 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case ShadowComponent::UInt16: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case ShadowComponent::Int16: { + Int16 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case ShadowComponent::UInt32: { + Uint32 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case ShadowComponent::Int32: { + Int32 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + default: + return 0; + } + } + } // namespace + + Bool DecodeShadowDataToWideRGBA(TextureInternalFormat internalFormat, const void* src, SizeT pixelCount, + Vector& outWide, Bool& outIsInteger, Bool& outIsSigned) { + if (!src) return false; + const Uint8* srcBytes = static_cast(src); + + InternalShadowLayout layout{}; + if (GetInternalShadowLayout(internalFormat, layout)) { + const SizeT componentSize = GetShadowComponentSize(layout.component); + const SizeT srcPixelSize = static_cast(layout.channelCount) * componentSize; + outIsInteger = layout.isInteger; + outIsSigned = layout.component == ShadowComponent::Int8 || layout.component == ShadowComponent::Int16 || + layout.component == ShadowComponent::Int32; + outWide.resize(pixelCount * 16); + if (layout.isInteger) { + auto* dst = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* s = srcBytes + i * srcPixelSize; + for (Int ch = 0; ch < 4; ++ch) { + Int64 v = ch == 3 ? 1 : 0; + if (ch < layout.channelCount) { + v = DecodeShadowComponentToInt(s + static_cast(ch) * componentSize, + layout.component); + } + if (outIsSigned) { + const auto out = static_cast(v); + Memcpy(&dst[i * 4 + ch], &out, sizeof(out)); + } else { + dst[i * 4 + ch] = static_cast(v); + } + } + } + } else { + auto* dst = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* s = srcBytes + i * srcPixelSize; + for (Int ch = 0; ch < 4; ++ch) { + Float v = ch == 3 ? 1.0f : 0.0f; + if (ch < layout.channelCount) { + v = DecodeShadowComponentToFloat(s + static_cast(ch) * componentSize, + layout.component); + } + dst[i * 4 + ch] = v; + } + } + } + return true; + } + + InternalPackedLayout packedInternal{}; + if (GetInternalPackedLayout(internalFormat, packedInternal)) { + outIsInteger = packedInternal.isInteger; + outIsSigned = false; + outWide.resize(pixelCount * 16); + if (packedInternal.isInteger) { + auto* dst = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + Uint32 word; + Memcpy(&word, srcBytes + i * 4, sizeof(word)); + dst[i * 4 + 0] = word & 0x3FFu; + dst[i * 4 + 1] = (word >> 10) & 0x3FFu; + dst[i * 4 + 2] = (word >> 20) & 0x3FFu; + dst[i * 4 + 3] = (word >> 30) & 0x3u; + } + } else { + auto* dst = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + Uint32 word; + Memcpy(&word, srcBytes + i * 4, sizeof(word)); + switch (packedInternal.kind) { + case PackedInternalKind::UNorm2101010Rev: + dst[i * 4 + 0] = static_cast(word & 0x3FFu) / 1023.0f; + dst[i * 4 + 1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; + dst[i * 4 + 2] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; + dst[i * 4 + 3] = static_cast((word >> 30) & 0x3u) / 3.0f; + break; + case PackedInternalKind::FloatR11G11B10: + dst[i * 4 + 0] = DecodeUnsignedF11ToFloat(word & 0x7FFu); + dst[i * 4 + 1] = DecodeUnsignedF11ToFloat((word >> 11) & 0x7FFu); + dst[i * 4 + 2] = DecodeUnsignedF10ToFloat((word >> 22) & 0x3FFu); + dst[i * 4 + 3] = 1.0f; + break; + case PackedInternalKind::FloatRGB9E5: { + Float rgb[3]; + DecodeSharedExponentRGB9E5(word, rgb); + dst[i * 4 + 0] = rgb[0]; + dst[i * 4 + 1] = rgb[1]; + dst[i * 4 + 2] = rgb[2]; + dst[i * 4 + 3] = 1.0f; + break; + } + default: + dst[i * 4 + 0] = dst[i * 4 + 1] = dst[i * 4 + 2] = 0.0f; + dst[i * 4 + 3] = 1.0f; + break; + } + } + } + return true; + } + + return false; + } } // namespace MobileGL::MG_Util::PixelStoreProcessor diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.h b/MobileGL/MG_Util/Texture/PixelStoreProcessor.h index 32f4ba0d..04dcf437 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.h +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.h @@ -22,4 +22,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { TextureInputFormat dstInputFormat, TexturePixelDataType dstDataType, IntVec3 dimension, Bool isBitmap, SizeT& outSize); void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector& swizzle); + + // 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 + // outIsInteger (outIsSigned tells signed from unsigned). Missing channels read 0 (G/B) and + // 1 / 1.0f (A). Returns false when the format has no canonical shadow layout. + Bool DecodeShadowDataToWideRGBA(TextureInternalFormat internalFormat, const void* src, SizeT pixelCount, + Vector& outWide, Bool& outIsInteger, Bool& outIsSigned); } // namespace MobileGL::MG_Util::PixelStoreProcessor diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index aa5fad0f..2b4e6857 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -19,11 +19,14 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { applicableOptions |= options & PixelFormatNormalizeOptionBit::NoDepthComponent32; break; case GL_RGBA16: + case GL_RGBA12: // stored as RGBA16 (see NormalizePixelFormat) case GL_RG16: case GL_R16: applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16; break; case GL_RGB16: + case GL_RGB10: // stored as RGB16 (see NormalizePixelFormat) + case GL_RGB12: // stored as RGB16 (see NormalizePixelFormat) applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16; applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16; break; @@ -159,6 +162,30 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { } *outInternalFormat = internalFormat; break; + // Legacy desktop-GL sized normalized formats (GL CTS packed_pixels): ES drivers reject them + // as internal formats, so store them in the closest ES-legal format with at least the same + // per-channel precision (extra precision stays inside the CTS comparison epsilon, which is + // derived from the requested format's bit widths). The upload (format, type) below matches + // the canonical shadow layout in PixelStoreProcessor (UNorm8 / UNorm16 component arrays). + case GL_R3_G3_B2: + case GL_RGB4: + case GL_RGB5: + *outInternalFormat = GL_RGB565; + break; + case GL_RGB10: + case GL_RGB12: + *outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoNorm16) || + (options & PixelFormatNormalizeOptionBit::NoRgb16) + ? GL_RGB32F + : GL_RGB16; + break; + case GL_RGBA2: + *outInternalFormat = GL_RGBA4; + break; + case GL_RGBA12: + *outInternalFormat = + (options & PixelFormatNormalizeOptionBit::NoNorm16) ? GL_RGBA32F : GL_RGBA16; + break; default: *outInternalFormat = internalFormat; break; @@ -276,6 +303,7 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { *outFormat = GL_RGB; break; case GL_SRGB8_ALPHA8: + case GL_SRGB_ALPHA: *outFormat = GL_RGBA; break; @@ -288,6 +316,24 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { case GL_RGB5_A1: *outFormat = GL_RGBA; break; + case GL_RGB10_A2UI: + *outFormat = GL_RGBA_INTEGER; + break; + + // Legacy desktop-GL sized normalized formats + case GL_R3_G3_B2: + case GL_RGB4: + case GL_RGB5: + case GL_RGB565: + case GL_RGB10: + case GL_RGB12: + *outFormat = GL_RGB; + break; + case GL_RGBA2: + case GL_RGBA4: + case GL_RGBA12: + *outFormat = GL_RGBA; + break; // Depth case GL_DEPTH_COMPONENT16: @@ -459,10 +505,44 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { *outType = GL_UNSIGNED_INT_10F_11F_11F_REV; break; case GL_RGB10_A2: + case GL_RGB10_A2UI: *outType = GL_UNSIGNED_INT_2_10_10_10_REV; break; case GL_RGB5_A1: - *outType = GL_UNSIGNED_SHORT_5_5_5_1; + // The shadow stores RGB5_A1 as UNorm8x4 (see PixelStoreProcessor); ES accepts + // GL_RGBA/GL_UNSIGNED_BYTE uploads for this internal format. + *outType = GL_UNSIGNED_BYTE; + break; + + // Legacy desktop-GL sized normalized formats: the upload type matches the canonical + // shadow layout (UNorm8 for <=8-bit channels, UNorm16 for 10/12-bit channels). + case GL_R3_G3_B2: + case GL_RGB4: + case GL_RGB5: + case GL_RGB565: + case GL_RGBA2: + case GL_RGBA4: + *outType = GL_UNSIGNED_BYTE; + break; + case GL_RGB10: + case GL_RGB12: + *outType = (options & PixelFormatNormalizeOptionBit::NoNorm16) || + (options & PixelFormatNormalizeOptionBit::NoRgb16) + ? GL_FLOAT + : GL_UNSIGNED_SHORT; + break; + case GL_RGBA12: + *outType = (options & PixelFormatNormalizeOptionBit::NoNorm16) ? GL_FLOAT : GL_UNSIGNED_SHORT; + break; + + // Unsized color formats keep their byte-per-channel client layout. + case GL_RGBA: + case GL_RGB: + case GL_RG: + case GL_RED: + case GL_SRGB: + case GL_SRGB_ALPHA: + *outType = GL_UNSIGNED_BYTE; break; // Depth