From 8c89b1618a64a143c381f4889d4150e53398b8ae Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:09:31 -0400 Subject: [PATCH 01/44] [Feat] (MG_Backend, android-plugin): show format capability tables in POST --- MobileGL/MG_Backend/BackendObject.cpp | 108 ++++----- MobileGL/MG_Backend/BackendObject.h | 19 ++ .../DirectGLES/BackendObject_DirectGLES.cpp | 73 +++--- .../DirectGLES/BackendObject_DirectGLES.h | 6 + .../BackendObject_DirectVulkan.cpp | 62 ++--- .../DirectVulkan/BackendObject_DirectVulkan.h | 8 + MobileGL/MG_Util/SelfTest/DriverPost.cpp | 14 ++ MobileGL/MG_Util/SelfTest/DriverPost.h | 2 + MobileGL/MG_Util/SelfTest/DriverPostJni.cpp | 59 ++++- .../top/mobilegl/plugin/PostActivity.java | 217 ++++++++++++++++-- 10 files changed, 405 insertions(+), 163 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.cpp b/MobileGL/MG_Backend/BackendObject.cpp index 2ceb4611..f238dd3a 100644 --- a/MobileGL/MG_Backend/BackendObject.cpp +++ b/MobileGL/MG_Backend/BackendObject.cpp @@ -15,23 +15,6 @@ namespace MobileGL::MG_Backend { namespace { - constexpr FormatCapability kPrintedFormatCapabilities[] = { - FormatCapability::Creatable, - FormatCapability::Sampled, - FormatCapability::LinearFilter, - FormatCapability::GenerateMipmap, - FormatCapability::TextureGather, - FormatCapability::TextureShadow, - FormatCapability::FramebufferRenderable, - FormatCapability::FramebufferLayered, - FormatCapability::MultisampleTexture, - FormatCapability::MultisampleRenderbuffer, - FormatCapability::ColorAttachment, - FormatCapability::DepthAttachment, - FormatCapability::StencilAttachment, - FormatCapability::TextureBuffer, - }; - Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { (void)dpy; return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT; @@ -41,40 +24,6 @@ namespace MobileGL::MG_Backend { return std::this_thread::get_id(); } - const char* ConvertFormatCapabilityToString(FormatCapability capability) { - switch (capability) { - case FormatCapability::Creatable: - return "Creatable"; - case FormatCapability::Sampled: - return "Sampled"; - case FormatCapability::LinearFilter: - return "LinearFilter"; - case FormatCapability::GenerateMipmap: - return "GenerateMipmap"; - case FormatCapability::TextureGather: - return "TextureGather"; - case FormatCapability::TextureShadow: - return "TextureShadow"; - case FormatCapability::FramebufferRenderable: - return "FramebufferRenderable"; - case FormatCapability::FramebufferLayered: - return "FramebufferLayered"; - case FormatCapability::MultisampleTexture: - return "MultisampleTexture"; - case FormatCapability::MultisampleRenderbuffer: - return "MultisampleRenderbuffer"; - case FormatCapability::ColorAttachment: - return "ColorAttachment"; - case FormatCapability::DepthAttachment: - return "DepthAttachment"; - case FormatCapability::StencilAttachment: - return "StencilAttachment"; - case FormatCapability::TextureBuffer: - return "TextureBuffer"; - } - return "Unknown"; - } - const char* GetFormatCapabilitySupportString(const FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex, @@ -94,22 +43,17 @@ namespace MobileGL::MG_Backend { } SizeT GetCapabilityColumnWidth(FormatCapability capability) { - SizeT width = std::strlen(ConvertFormatCapabilityToString(capability)); + SizeT width = std::strlen(GetFormatCapabilityName(capability)); width = std::max(width, std::strlen("Caveat")); return width; } - String GetFormatCapabilityTargetName(SizeT targetIndex) { - if (targetIndex == kFormatCapabilityRenderbufferTargetIndex) return "Renderbuffer"; - return MG_Util::ConvertTextureTargetToString(static_cast(targetIndex)); - } - String BuildFormatCapabilityHeader(SizeT formatNameWidth) { std::ostringstream line; line << std::left << std::setw(static_cast(formatNameWidth)) << ""; - for (FormatCapability capability : kPrintedFormatCapabilities) { + for (FormatCapability capability : kReportedFormatCapabilities) { line << " | " << std::left << std::setw(static_cast(GetCapabilityColumnWidth(capability))) - << ConvertFormatCapabilityToString(capability); + << GetFormatCapabilityName(capability); } return line.str(); } @@ -122,7 +66,7 @@ namespace MobileGL::MG_Backend { std::ostringstream line; line << std::left << std::setw(static_cast(formatNameWidth)) << MG_Util::ConvertTextureInternalFormatToString(format); - for (FormatCapability capability : kPrintedFormatCapabilities) { + for (FormatCapability capability : kReportedFormatCapabilities) { line << " | " << std::left << std::setw(static_cast(GetCapabilityColumnWidth(capability))) << GetFormatCapabilitySupportString(cache, targetIndex, formatIndex, capability); } @@ -160,6 +104,50 @@ namespace MobileGL::MG_Backend { return kFormatCapabilityRenderbufferTargetIndex; } + const char* GetFormatCapabilityName(FormatCapability capability) { + switch (capability) { + case FormatCapability::Creatable: + return "Creatable"; + case FormatCapability::Sampled: + return "Sampled"; + case FormatCapability::LinearFilter: + return "LinearFilter"; + case FormatCapability::GenerateMipmap: + return "GenerateMipmap"; + case FormatCapability::TextureGather: + return "TextureGather"; + case FormatCapability::TextureShadow: + return "TextureShadow"; + case FormatCapability::FramebufferRenderable: + return "FramebufferRenderable"; + case FormatCapability::FramebufferLayered: + return "FramebufferLayered"; + case FormatCapability::MultisampleTexture: + return "MultisampleTexture"; + case FormatCapability::MultisampleRenderbuffer: + return "MultisampleRenderbuffer"; + case FormatCapability::ColorAttachment: + return "ColorAttachment"; + case FormatCapability::DepthAttachment: + return "DepthAttachment"; + case FormatCapability::StencilAttachment: + return "StencilAttachment"; + case FormatCapability::TextureBuffer: + return "TextureBuffer"; + } + return "Unknown"; + } + + String GetFormatCapabilityTargetName(SizeT targetIndex) { + if (targetIndex == kFormatCapabilityRenderbufferTargetIndex) { + return "Renderbuffer"; + } + if (targetIndex >= kFormatCapabilityTextureTargetCount) { + return "Unknown"; + } + return MG_Util::ConvertTextureTargetToString(static_cast(targetIndex)); + } + void PrintFormatCapabilities(const FormatCapabilityCache& cache) { const SizeT formatNameWidth = GetPrintedFormatNameWidth(); diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index ffc889a5..5032c5dc 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -47,6 +47,23 @@ namespace MobileGL { using FormatCapabilityFlags = Flags; + inline constexpr Array kReportedFormatCapabilities = { + FormatCapability::Creatable, + FormatCapability::Sampled, + FormatCapability::LinearFilter, + FormatCapability::GenerateMipmap, + FormatCapability::TextureGather, + FormatCapability::TextureShadow, + FormatCapability::FramebufferRenderable, + FormatCapability::FramebufferLayered, + FormatCapability::MultisampleTexture, + FormatCapability::MultisampleRenderbuffer, + FormatCapability::ColorAttachment, + FormatCapability::DepthAttachment, + FormatCapability::StencilAttachment, + FormatCapability::TextureBuffer, + }; + inline constexpr SizeT kFormatCapabilityTextureTargetCount = static_cast(TextureTarget::TextureTargetCount); inline constexpr SizeT kFormatCapabilityRenderbufferTargetIndex = kFormatCapabilityTextureTargetCount; @@ -70,6 +87,8 @@ namespace MobileGL { Bool HasFormatCapability(FormatCapabilityFlags caps, FormatCapability capability); SizeT GetFormatCapabilityTargetIndex(TextureTarget target); SizeT GetRenderbufferFormatCapabilityTargetIndex(); + const char* GetFormatCapabilityName(FormatCapability capability); + String GetFormatCapabilityTargetName(SizeT targetIndex); void PrintFormatCapabilities(const FormatCapabilityCache& cache); // Opaque backend fence-sync handle, created by GLFunctionsTable::FenceSync diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index df2ae21b..de7dd358 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -150,23 +150,6 @@ namespace MobileGL::MG_Backend::DirectGLES { String Reason; }; - constexpr FormatCapability kGLESProbeCapabilities[] = { - FormatCapability::Creatable, - FormatCapability::Sampled, - FormatCapability::LinearFilter, - FormatCapability::GenerateMipmap, - FormatCapability::TextureGather, - FormatCapability::TextureShadow, - FormatCapability::FramebufferRenderable, - FormatCapability::FramebufferLayered, - FormatCapability::MultisampleTexture, - FormatCapability::MultisampleRenderbuffer, - FormatCapability::ColorAttachment, - FormatCapability::DepthAttachment, - FormatCapability::StencilAttachment, - FormatCapability::TextureBuffer, - }; - GLESProbeFormatInfo BuildNativeProbeFormatInfo(GLenum requestedInternalFormat) { GLESProbeFormatInfo info; info.InternalFormat = requestedInternalFormat; @@ -176,9 +159,10 @@ namespace MobileGL::MG_Backend::DirectGLES { return info; } - Flags GetForcedPixelFormatNormalizeOptions() { + Flags GetForcedPixelFormatNormalizeOptions( + const MG_External::GLESCapabilities& capabilities) { Flags options; - if (g_GLESCapabilities.IsAngleRenderer) { + if (capabilities.IsAngleRenderer) { options |= PixelFormatNormalizeOptionBit::NoRgb16; options |= PixelFormatNormalizeOptionBit::NoSnorm16; options |= PixelFormatNormalizeOptionBit::NoSnorm8; @@ -186,11 +170,12 @@ namespace MobileGL::MG_Backend::DirectGLES { return options; } - Flags GetDriverPixelFormatNormalizeOptions() { + Flags GetDriverPixelFormatNormalizeOptions( + const MG_External::GLESCapabilities& capabilities) { Flags options = PixelFormatNormalizeOptionBit::NoDepthComponent32; options |= PixelFormatNormalizeOptionBit::NoRGBA8Snorm; options |= PixelFormatNormalizeOptionBit::NoRGB16Snorm; - if (!g_GLESCapabilities.SupportsNorm16Texture) { + if (!capabilities.SupportsNorm16Texture) { options |= PixelFormatNormalizeOptionBit::NoNorm16; } return options; @@ -241,21 +226,11 @@ namespace MobileGL::MG_Backend::DirectGLES { return MG_Util::ConvertGLEnumToString(internalFormat); } - String GetFormatCapabilityTargetNameForLog(SizeT targetIndex) { - if (targetIndex == GetRenderbufferFormatCapabilityTargetIndex()) { - return "Renderbuffer"; - } - if (targetIndex < kFormatCapabilityTextureTargetCount) { - return MG_Util::ConvertTextureTargetToString(static_cast(targetIndex)); - } - return "Unknown"; - } - void LogGLESFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex, const GLESProbeFormatInfo& fallbackInfo) { MGLOG_D("Caveat: %s %s not fully supported. Reason: %s. Fallback: %s", - GetFormatCapabilityTargetNameForLog(targetIndex).c_str(), + GetFormatCapabilityTargetName(targetIndex).c_str(), MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(), fallbackInfo.Reason.c_str(), ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str()); @@ -306,7 +281,7 @@ namespace MobileGL::MG_Backend::DirectGLES { SizeT formatIndex, FormatCapabilityFlags caps) { Bool added = false; - for (FormatCapability capability : kGLESProbeCapabilities) { + for (FormatCapability capability : kReportedFormatCapabilities) { if (HasFormatCapability(caps, capability) && !HasFormatCapability(cache.FullCaps[targetIndex][formatIndex], capability)) { cache.CaveatCaps[targetIndex][formatIndex] |= capability; @@ -316,7 +291,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return added; } - Int GetGLESFormatMaxSamples(const DynamicBackendParameters& dynamicParameters, + Int GetGLESFormatMaxSamples(const MG_External::GLESCapabilities& capabilities, TextureInternalFormat logicalFormat, GLenum imageFormat) { const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat); @@ -324,12 +299,12 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER; if (isDepth || isStencil) { - return dynamicParameters.MaxDepthTextureSamples; + return capabilities.MaxDepthTextureSamples; } if (isInteger) { - return dynamicParameters.MaxIntegerSamples; + return capabilities.MaxIntegerSamples; } - return dynamicParameters.MaxColorTextureSamples; + return capabilities.MaxColorTextureSamples; } Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, @@ -527,12 +502,14 @@ namespace MobileGL::MG_Backend::DirectGLES { return sampleCounts; } - void ProbeGLESFormatCapabilities(const MG_External::GLESFunctionsTable& gl, - FormatCapabilityCache& cache, - const DynamicBackendParameters& dynamicParameters) { + void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl, + const MG_External::GLESCapabilities& capabilities, + FormatCapabilityCache& cache) { cache.Clear(); - const Flags forcedOptions = GetForcedPixelFormatNormalizeOptions(); - const Flags driverOptions = GetDriverPixelFormatNormalizeOptions(); + const Flags forcedOptions = + GetForcedPixelFormatNormalizeOptions(capabilities); + const Flags driverOptions = + GetDriverPixelFormatNormalizeOptions(capabilities); for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) { const auto logicalFormat = static_cast(formatIndex); @@ -594,7 +571,7 @@ namespace MobileGL::MG_Backend::DirectGLES { AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex, GetRenderbufferFeatureCaps(logicalFormat)); const Int maxSamples = - GetGLESFormatMaxSamples(dynamicParameters, logicalFormat, nativeInfo.ImageFormat); + GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat); cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples); } else { @@ -608,7 +585,7 @@ namespace MobileGL::MG_Backend::DirectGLES { LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo); } const Int maxSamples = - GetGLESFormatMaxSamples(dynamicParameters, logicalFormat, fallbackInfo.ImageFormat); + GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat); cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(gl, fallbackInfo.InternalFormat, logicalFormat, maxSamples); } @@ -655,6 +632,12 @@ namespace MobileGL::MG_Backend::DirectGLES { } } // namespace + void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl, + const MG_External::GLESCapabilities& capabilities, + FormatCapabilityCache& cache) { + PopulateFormatCapabilitiesImpl(gl, capabilities, cache); + } + BackendObject_DirectGLES::~BackendObject_DirectGLES() { DestroyEGLContext(); } @@ -695,7 +678,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // the extension list is first built). UpdateAdvertisedTimerQueryExtension(); UpdateDynamicBackendParameters(); - ProbeGLESFormatCapabilities(m_GLESFunctions, MutableFormatCapabilities(), m_dynamicParameters); + PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities()); PrintFormatCapabilities(GetFormatCapabilities()); return true; } diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index 6d9d6935..b13f2344 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -12,6 +12,12 @@ #include namespace MobileGL::MG_Backend::DirectGLES { + // Populates the same format-capability cache used by backend startup. The caller + // must keep the supplied GLES context current for the duration of this call. + void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl, + const MG_External::GLESCapabilities& capabilities, + FormatCapabilityCache& cache); + class BackendObject_DirectGLES : public BackendObject { public: ~BackendObject_DirectGLES() override; diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index c359cb59..44502133 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -182,25 +182,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { return MG_Util::ConvertTextureInternalFormatToVkEnum(*fallbackLogicalFormat); } - constexpr FormatCapability kVulkanProbeCapabilities[] = { - FormatCapability::Creatable, - FormatCapability::Sampled, - FormatCapability::LinearFilter, - FormatCapability::GenerateMipmap, - FormatCapability::TextureGather, - FormatCapability::TextureShadow, - FormatCapability::FramebufferRenderable, - FormatCapability::FramebufferLayered, - FormatCapability::MultisampleTexture, - FormatCapability::MultisampleRenderbuffer, - FormatCapability::ColorAttachment, - FormatCapability::DepthAttachment, - FormatCapability::StencilAttachment, - FormatCapability::TextureBuffer, - }; - Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) { - for (FormatCapability capability : kVulkanProbeCapabilities) { + for (FormatCapability capability : kReportedFormatCapabilities) { if (HasFormatCapability(fallbackCaps, capability) && !HasFormatCapability(nativeCaps, capability)) { return true; @@ -209,21 +192,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } - String GetFormatCapabilityTargetNameForLog(SizeT targetIndex) { - if (targetIndex == GetRenderbufferFormatCapabilityTargetIndex()) { - return "Renderbuffer"; - } - if (targetIndex < kFormatCapabilityTextureTargetCount) { - return MG_Util::ConvertTextureTargetToString(static_cast(targetIndex)); - } - return "Unknown"; - } - void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex, TextureInternalFormat fallbackFormat) { MGLOG_D("Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s", - GetFormatCapabilityTargetNameForLog(targetIndex).c_str(), + GetFormatCapabilityTargetName(targetIndex).c_str(), MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(), MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str()); } @@ -237,11 +210,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return counts; } - void FillVulkanFormatCapabilities(VkPhysicalDevice physicalDevice, - const DynamicBackendParameters& dynamicParameters, - FormatCapabilityCache& cache) { + void PopulateFormatCapabilitiesImpl(VkPhysicalDevice physicalDevice, + PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties, + const MG_External::VulkanCapabilities& capabilities, + FormatCapabilityCache& cache) { cache.Clear(); - if (physicalDevice == VK_NULL_HANDLE) { + if (physicalDevice == VK_NULL_HANDLE || getFormatProperties == nullptr) { return; } @@ -258,12 +232,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkFormatProperties nativeProperties{}; if (nativeFormat != VK_FORMAT_UNDEFINED) { - vkGetPhysicalDeviceFormatProperties(physicalDevice, nativeFormat, &nativeProperties); + getFormatProperties(physicalDevice, nativeFormat, &nativeProperties); } VkFormatProperties fallbackProperties{}; if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) { - vkGetPhysicalDeviceFormatProperties(physicalDevice, fallbackFormat, &fallbackProperties); + getFormatProperties(physicalDevice, fallbackFormat, &fallbackProperties); } for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) { @@ -289,11 +263,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat); const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat); const Bool isInteger = IsIntegerInternalFormat(logicalFormat); - Int maxSamples = dynamicParameters.MaxColorTextureSamples; + Int maxSamples = capabilities.MaxColorTextureSamples; if (isDepth || isStencil) { - maxSamples = dynamicParameters.MaxDepthTextureSamples; + maxSamples = capabilities.MaxDepthTextureSamples; } else if (isInteger) { - maxSamples = dynamicParameters.MaxIntegerSamples; + maxSamples = capabilities.MaxIntegerSamples; } cache.SampleCounts[targetIndex][formatIndex] = BuildSampleCounts(maxSamples); } @@ -332,12 +306,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { cache.CaveatCaps[renderbufferTargetIndex][formatIndex]; if (HasFormatCapability(rbCaps, FormatCapability::MultisampleRenderbuffer)) { cache.SampleCounts[renderbufferTargetIndex][formatIndex] = - BuildSampleCounts(dynamicParameters.MaxFramebufferSamples); + BuildSampleCounts(capabilities.MaxFramebufferSamples); } } } } // namespace + void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice, + PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties, + const MG_External::VulkanCapabilities& capabilities, + FormatCapabilityCache& cache) { + PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache); + } + BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default; BackendObject_DirectVulkan::BackendObject_DirectVulkan(): m_rendererInfo{GetRendererIdentity()} {} @@ -394,7 +375,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { } UpdateDynamicBackendParameters(); UpdateAdvertisedExtensions(); - FillVulkanFormatCapabilities(physicalDevice.handle, m_dynamicParameters, MutableFormatCapabilities()); + PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps, + MutableFormatCapabilities()); PrintFormatCapabilities(GetFormatCapabilities()); return true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index e0c70203..f3a19816 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -12,6 +12,14 @@ #include namespace MobileGL::MG_Backend::DirectVulkan { + // Populates the same format-capability cache used by backend startup. Passing the + // instance-resolved function keeps standalone callers independent of global loader + // initialization; the physical device must remain valid for the duration of the call. + void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice, + PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties, + const MG_External::VulkanCapabilities& capabilities, + FormatCapabilityCache& cache); + class BackendObject_DirectVulkan : public BackendObject { public: BackendObject_DirectVulkan(); diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index fa84600c..6cb8c4ec 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -525,6 +525,9 @@ namespace MobileGL::MG_Util::SelfTest { summary.capsValid = true; const MG_External::GLESCapabilities& caps = summary.caps; builder.report.rendererInfo = format("{} ({})", caps.GLESRendererString, caps.GLESVersionString); + builder.report.formatCapabilities.emplace(); + MG_Backend::DirectGLES::PopulateFormatCapabilities( + glesFuncs, caps, builder.report.formatCapabilities.value()); EvaluateGlesChecklist(builder, caps, glesFuncs); ProbeGlesTimerQuery(builder, caps, glesFuncs); } while (false); @@ -980,6 +983,9 @@ namespace MobileGL::MG_Util::SelfTest { getInstanceProcAddr(instance, "vkGetPhysicalDeviceFeatures2")); const auto vkGetPhysicalDeviceProperties2Fn = reinterpret_cast( getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties2")); + const auto vkGetPhysicalDeviceFormatPropertiesFn = + reinterpret_cast( + getInstanceProcAddr(instance, "vkGetPhysicalDeviceFormatProperties")); // The instance is destroyed from a scope guard so it is released on every early-return // path and even if a String/format allocation throws while report rows are being built. @@ -1059,6 +1065,14 @@ namespace MobileGL::MG_Util::SelfTest { summary.deviceName = String(properties.deviceName); summary.apiVersionString = VkApiVersionToString(properties.apiVersion); summary.driverVersionString = driverVersionString; + if (vkGetPhysicalDeviceFormatPropertiesFn != nullptr) { + MG_External::VulkanCapabilities formatProbeCapabilities{}; + BackendLoader::FillInVulkanCapabilities(formatProbeCapabilities, properties); + builder.report.formatCapabilities.emplace(); + MG_Backend::DirectVulkan::PopulateFormatCapabilities( + physicalDevice, vkGetPhysicalDeviceFormatPropertiesFn, formatProbeCapabilities, + builder.report.formatCapabilities.value()); + } // The chosen-device facts (name, enumeration count, graphics queue) ride along // on both outcomes so the device API verdict never hides them. diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.h b/MobileGL/MG_Util/SelfTest/DriverPost.h index d81ca193..e698acf9 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.h +++ b/MobileGL/MG_Util/SelfTest/DriverPost.h @@ -8,6 +8,7 @@ #pragma once #include +#include namespace MobileGL::MG_Util::SelfTest { // One row of a backend power-on self-test (POST) report. @@ -33,6 +34,7 @@ namespace MobileGL::MG_Util::SelfTest { String verdict = "UNSUPPORTED"; // "OK" | "DEGRADED" | "UNSUPPORTED" String rendererInfo; Vector checks; + Optional formatCapabilities; }; // Probes the DEVICE GLES driver with self-contained EGL boilerplate (1x1 pbuffer diff --git a/MobileGL/MG_Util/SelfTest/DriverPostJni.cpp b/MobileGL/MG_Util/SelfTest/DriverPostJni.cpp index 01719159..c42f57b0 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPostJni.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPostJni.cpp @@ -11,6 +11,7 @@ #ifdef __ANDROID__ #include "DriverPost.h" +#include #include @@ -19,6 +20,9 @@ namespace { using MobileGL::SizeT; using MobileGL::String; using MobileGL::StringStream; + using MobileGL::Uint64; + using MobileGL::MG_Backend::FormatCapabilityCache; + using MobileGL::MG_Backend::FormatCapabilityFlags; using MobileGL::MG_Util::SelfTest::BackendPostReport; using MobileGL::MG_Util::SelfTest::PostCheck; @@ -70,6 +74,55 @@ namespace { out << '"' << EscapeJsonString(value) << '"'; } + Uint64 BuildFormatCapabilityMask(FormatCapabilityFlags capabilities) { + Uint64 mask = 0; + for (SizeT capabilityIndex = 0; + capabilityIndex < MobileGL::MG_Backend::kReportedFormatCapabilities.size(); ++capabilityIndex) { + if (MobileGL::MG_Backend::HasFormatCapability( + capabilities, MobileGL::MG_Backend::kReportedFormatCapabilities[capabilityIndex])) { + mask |= 1ull << capabilityIndex; + } + } + return mask; + } + + void AppendFormatCapabilitiesJson(StringStream& out, const FormatCapabilityCache& cache) { + out << ",\"formatCapabilities\":{\"capabilities\":["; + for (SizeT capabilityIndex = 0; + capabilityIndex < MobileGL::MG_Backend::kReportedFormatCapabilities.size(); ++capabilityIndex) { + if (capabilityIndex != 0) { + out << ','; + } + AppendJsonString( + out, MobileGL::MG_Backend::GetFormatCapabilityName( + MobileGL::MG_Backend::kReportedFormatCapabilities[capabilityIndex])); + } + out << "],\"targets\":["; + for (SizeT targetIndex = 0; targetIndex < MobileGL::MG_Backend::kFormatCapabilityTargetCount; + ++targetIndex) { + if (targetIndex != 0) { + out << ','; + } + out << "{\"name\":"; + AppendJsonString(out, MobileGL::MG_Backend::GetFormatCapabilityTargetName(targetIndex)); + out << ",\"rows\":["; + for (SizeT formatIndex = 0; formatIndex < MobileGL::MG_Backend::kFormatCapabilityFormatCount; + ++formatIndex) { + if (formatIndex != 0) { + out << ','; + } + out << '['; + AppendJsonString( + out, MobileGL::MG_Util::ConvertTextureInternalFormatToString( + static_cast(formatIndex))); + out << ',' << BuildFormatCapabilityMask(cache.FullCaps[targetIndex][formatIndex]); + out << ',' << BuildFormatCapabilityMask(cache.CaveatCaps[targetIndex][formatIndex]) << ']'; + } + out << "]}"; + } + out << "]}"; + } + void AppendBackendReportJson(StringStream& out, const BackendPostReport& report) { out << "{\"available\":" << (report.available ? "true" : "false"); out << ",\"verdict\":"; @@ -90,7 +143,11 @@ namespace { AppendJsonString(out, check.detail); out << '}'; } - out << "]}"; + out << ']'; + if (report.formatCapabilities.has_value()) { + AppendFormatCapabilitiesJson(out, report.formatCapabilities.value()); + } + out << '}'; } } // namespace diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java index 881b668c..24e95e6d 100644 --- a/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java @@ -7,6 +7,7 @@ import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.View; +import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; @@ -30,6 +31,15 @@ public final class PostActivity extends Activity { private static final int COLOR_DETAIL = 0xFFBBBBBB; private static final int COLOR_ROW_EVEN = 0xFF1A1A1A; private static final int COLOR_ROW_ODD = 0xFF121212; + private static final int COLOR_TABLE_HEADER = 0xFF303030; + private static final int COLOR_FORMAT_CELL = 0xFF242424; + private static final int COLOR_CAPABILITY_FULL = 0xFF2E7D32; + private static final int COLOR_CAPABILITY_CAVEAT = 0xFFFBC02D; + private static final int COLOR_CAPABILITY_NONE = 0xFFC62828; + + private static final int FORMAT_COLUMN_WIDTH_DP = 152; + private static final int CAPABILITY_COLUMN_WIDTH_DP = 152; + private static final int CAPABILITY_ROW_HEIGHT_DP = 36; private static final String INDICATOR_COLLAPSED = "▸"; private static final String INDICATOR_EXPANDED = "▾"; @@ -244,25 +254,26 @@ public final class PostActivity extends Activity { } JSONArray checks = backend.optJSONArray("checks"); - if (checks == null) { - return; - } - LinearLayout table = new LinearLayout(this); - table.setOrientation(LinearLayout.VERTICAL); - LinearLayout.LayoutParams tableParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ); - tableParams.topMargin = dp(6); - contentLayout.addView(table, tableParams); - int rowIndex = 0; - for (int i = 0; i < checks.length(); ++i) { - JSONObject check = checks.optJSONObject(i); - if (check == null) { - continue; + if (checks != null) { + LinearLayout table = new LinearLayout(this); + table.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams tableParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + tableParams.topMargin = dp(6); + contentLayout.addView(table, tableParams); + int rowIndex = 0; + for (int i = 0; i < checks.length(); ++i) { + JSONObject check = checks.optJSONObject(i); + if (check == null) { + continue; + } + addCheckRow(table, check, rowIndex++); } - addCheckRow(table, check, rowIndex++); } + + renderFormatCapabilities(backend.optJSONObject("formatCapabilities")); } /** @@ -336,6 +347,178 @@ public final class PostActivity extends Activity { }); } + /** Adds one initially-collapsed capability matrix for every reported target. */ + private void renderFormatCapabilities(JSONObject formatCapabilities) { + addText("Format capabilities", 14, COLOR_TEXT, true, dp(16)); + if (formatCapabilities == null) { + addText("Format capability table unavailable.", 12, COLOR_INFO, false, dp(4)); + return; + } + + JSONArray capabilities = formatCapabilities.optJSONArray("capabilities"); + JSONArray targets = formatCapabilities.optJSONArray("targets"); + if (capabilities == null || capabilities.length() == 0 || targets == null || targets.length() == 0) { + addText("Format capability table is empty.", 12, COLOR_INFO, false, dp(4)); + return; + } + + for (int targetIndex = 0; targetIndex < targets.length(); ++targetIndex) { + JSONObject target = targets.optJSONObject(targetIndex); + if (target != null) { + addFormatTargetTable(target, capabilities); + } + } + } + + /** + * Adds a target header whose table is created only while expanded. Removing the + * table again on collapse avoids retaining all status cells for both backends. + */ + private void addFormatTargetTable(JSONObject target, JSONArray capabilities) { + String targetName = target.optString("name", "Unknown target"); + JSONArray rows = target.optJSONArray("rows"); + int formatCount = rows == null ? 0 : rows.length(); + + LinearLayout section = new LinearLayout(this); + section.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams sectionParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + sectionParams.topMargin = dp(6); + contentLayout.addView(section, sectionParams); + + TextView toggleView = makeText( + INDICATOR_COLLAPSED + " " + targetName + " (" + formatCount + " formats)", + 12, + COLOR_TEXT, + true + ); + toggleView.setBackgroundColor(COLOR_ROW_EVEN); + toggleView.setGravity(Gravity.CENTER_VERTICAL); + toggleView.setMinimumHeight(dp(44)); + toggleView.setPadding(dp(10), dp(6), dp(10), dp(6)); + section.addView(toggleView, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + )); + + toggleView.setOnClickListener(view -> { + boolean expanded = section.getChildCount() > 1; + if (expanded) { + section.removeViews(1, section.getChildCount() - 1); + toggleView.setText( + INDICATOR_COLLAPSED + " " + targetName + " (" + formatCount + " formats)" + ); + return; + } + + section.addView(buildFormatCapabilityTable(capabilities, rows), new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + )); + toggleView.setText( + INDICATOR_EXPANDED + " " + targetName + " (" + formatCount + " formats)" + ); + }); + } + + /** Builds the horizontally-scrollable table for one target. */ + private HorizontalScrollView buildFormatCapabilityTable(JSONArray capabilities, JSONArray rows) { + HorizontalScrollView scrollView = new HorizontalScrollView(this); + scrollView.setFillViewport(false); + scrollView.setHorizontalScrollBarEnabled(true); + scrollView.setPadding(0, dp(2), 0, dp(4)); + + LinearLayout table = new LinearLayout(this); + table.setOrientation(LinearLayout.VERTICAL); + table.addView(buildFormatCapabilityHeader(capabilities)); + + if (rows != null) { + for (int rowIndex = 0; rowIndex < rows.length(); ++rowIndex) { + JSONArray row = rows.optJSONArray(rowIndex); + if (row != null) { + table.addView(buildFormatCapabilityRow(capabilities.length(), row)); + } + } + } + + scrollView.addView(table, new HorizontalScrollView.LayoutParams( + HorizontalScrollView.LayoutParams.WRAP_CONTENT, + HorizontalScrollView.LayoutParams.WRAP_CONTENT + )); + return scrollView; + } + + private LinearLayout buildFormatCapabilityHeader(JSONArray capabilities) { + LinearLayout header = new LinearLayout(this); + header.setOrientation(LinearLayout.HORIZONTAL); + addFormatTableCell(header, "Format", FORMAT_COLUMN_WIDTH_DP, COLOR_TABLE_HEADER, COLOR_TEXT, true); + for (int capabilityIndex = 0; capabilityIndex < capabilities.length(); ++capabilityIndex) { + addFormatTableCell( + header, + capabilities.optString(capabilityIndex, "Capability " + capabilityIndex), + CAPABILITY_COLUMN_WIDTH_DP, + COLOR_TABLE_HEADER, + COLOR_TEXT, + true + ); + } + return header; + } + + private LinearLayout buildFormatCapabilityRow(int capabilityCount, JSONArray row) { + LinearLayout line = new LinearLayout(this); + line.setOrientation(LinearLayout.HORIZONTAL); + + addFormatTableCell( + line, + row.optString(0, "Unknown"), + FORMAT_COLUMN_WIDTH_DP, + COLOR_FORMAT_CELL, + COLOR_TEXT, + false + ); + + long fullMask = row.optLong(1, 0L); + long caveatMask = row.optLong(2, 0L); + for (int capabilityIndex = 0; capabilityIndex < capabilityCount; ++capabilityIndex) { + long capabilityBit = capabilityIndex < Long.SIZE ? 1L << capabilityIndex : 0L; + boolean full = capabilityBit != 0L && (fullMask & capabilityBit) != 0L; + boolean caveat = !full && capabilityBit != 0L && (caveatMask & capabilityBit) != 0L; + addFormatTableCell( + line, + full ? "Full" : caveat ? "Caveat" : "None", + CAPABILITY_COLUMN_WIDTH_DP, + full ? COLOR_CAPABILITY_FULL : caveat ? COLOR_CAPABILITY_CAVEAT : COLOR_CAPABILITY_NONE, + caveat ? 0xFF000000 : 0xFFFFFFFF, + true + ); + } + return line; + } + + private void addFormatTableCell(LinearLayout row, + String text, + int widthDp, + int backgroundColor, + int textColor, + boolean bold) { + TextView cell = makeText(text, 10, textColor, bold); + cell.setBackgroundColor(backgroundColor); + cell.setGravity(Gravity.CENTER); + cell.setSingleLine(true); + cell.setPadding(dp(6), 0, dp(6), 0); + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + dp(widthDp), + dp(CAPABILITY_ROW_HEIGHT_DP) + ); + params.rightMargin = dp(1); + params.bottomMargin = dp(1); + row.addView(cell, params); + } + /** Adds the collapsed "Raw report" toggle plus the (initially hidden) raw JSON dump. */ private void addRawJsonSection(String json) { TextView toggleView = addText(INDICATOR_COLLAPSED + " Raw report (tap to expand)", 12, COLOR_INFO, true, dp(24)); From 305701326cef95b120f562fe57141dc9758c67e6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:18:46 -0400 Subject: [PATCH 02/44] [Fix] (MG_Util/SelfTest): preserve POST checks before format probing --- MobileGL/MG_Util/SelfTest/DriverPost.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 6cb8c4ec..82d93ee3 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -525,11 +525,11 @@ namespace MobileGL::MG_Util::SelfTest { summary.capsValid = true; const MG_External::GLESCapabilities& caps = summary.caps; builder.report.rendererInfo = format("{} ({})", caps.GLESRendererString, caps.GLESVersionString); + EvaluateGlesChecklist(builder, caps, glesFuncs); + ProbeGlesTimerQuery(builder, caps, glesFuncs); builder.report.formatCapabilities.emplace(); MG_Backend::DirectGLES::PopulateFormatCapabilities( glesFuncs, caps, builder.report.formatCapabilities.value()); - EvaluateGlesChecklist(builder, caps, glesFuncs); - ProbeGlesTimerQuery(builder, caps, glesFuncs); } while (false); } @@ -1065,14 +1065,6 @@ namespace MobileGL::MG_Util::SelfTest { summary.deviceName = String(properties.deviceName); summary.apiVersionString = VkApiVersionToString(properties.apiVersion); summary.driverVersionString = driverVersionString; - if (vkGetPhysicalDeviceFormatPropertiesFn != nullptr) { - MG_External::VulkanCapabilities formatProbeCapabilities{}; - BackendLoader::FillInVulkanCapabilities(formatProbeCapabilities, properties); - builder.report.formatCapabilities.emplace(); - MG_Backend::DirectVulkan::PopulateFormatCapabilities( - physicalDevice, vkGetPhysicalDeviceFormatPropertiesFn, formatProbeCapabilities, - builder.report.formatCapabilities.value()); - } // The chosen-device facts (name, enumeration count, graphics queue) ride along // on both outcomes so the device API verdict never hides them. @@ -1260,6 +1252,14 @@ namespace MobileGL::MG_Util::SelfTest { timestampPeriod) + TimerQueryDisabledNote()); } + if (vkGetPhysicalDeviceFormatPropertiesFn != nullptr) { + MG_External::VulkanCapabilities formatProbeCapabilities{}; + BackendLoader::FillInVulkanCapabilities(formatProbeCapabilities, properties); + builder.report.formatCapabilities.emplace(); + MG_Backend::DirectVulkan::PopulateFormatCapabilities( + physicalDevice, vkGetPhysicalDeviceFormatPropertiesFn, formatProbeCapabilities, + builder.report.formatCapabilities.value()); + } } BackendPostReport RunVulkanDriverPost() { From 2e7073a89010772f81fb12419f0d50e30c7ab03f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 22:56:31 -0400 Subject: [PATCH 03/44] [Perf] (MG_Backend/DirectGLES): replace per-draw global-UBO glBufferSubData with a persistent-mapped ring allocator (fence-watermark reclaimed, MOBILEGL_DISABLE_UBO_RING opt-out); scrub stale indexed-binding shadow on glDeleteBuffers --- MobileGL/Config.h | 4 + MobileGL/ConfigLoader.cpp | 1 + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 55 +++- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 253 ++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/Managers.h | 44 +++ 5 files changed, 350 insertions(+), 7 deletions(-) diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 51ba0ddd..0c2ec66d 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -57,6 +57,10 @@ namespace MobileGL::MG_Config { Bool CoherentAsFlush = false; // MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp). Bool TraceSkipAutodestroy = false; + // MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the + // per-draw glBufferSubData path instead of the persistent-mapped ring allocator + // (negative control / driver-bug escape hatch). + Bool DisableUboRing = false; }; extern FeaturesTable Features; } // namespace MobileGL::MG_Config diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 1639361f..8a110027 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -121,6 +121,7 @@ namespace MobileGL::MG_ConfigLoader { QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER"); features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); + features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); } inline void InitBackendType() { diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index c9783edd..e53a2ea6 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1076,14 +1076,52 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND); #endif const Uint32 uboContentVersion = currentProgram->GetUBOContentVersion(); - if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) { - g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId()); - g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(), - currentProgram->MapUBO()); - g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); - backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion); + const SizeT uboSize = static_cast(currentProgram->GetUBOSize()); + // Preferred path: write changed contents into a fresh slot of the + // shared persistent-mapped ring and bind it as a range. The GPU + // never reads bytes the CPU is writing, so the driver has no + // write-after-read hazard to resolve — the in-place glBufferSubData + // below forced Adreno into a ghost/stall on every uniform-dirtying + // draw (MC dirties uniforms every draw), which dominated frame time. + Bool ringBound = false; + if (BufferImpl::UboRingAvailable()) { + const SizeT bindSize = + std::max(uboSize, static_cast(backendProgram.GetGlobalUboBackendBlockSize())); + const Uint64 frameSerial = CurrentFrameSerial(); + auto& ringSlot = backendProgram.GetGlobalUboRingAllocation(); + Bool slotValid = ringSlot.ringGeneration == BufferImpl::UboRingGeneration() && + ringSlot.frameSerial == frameSerial && + ringSlot.contentVersion == uboContentVersion; + if (!slotValid) { + SizeT offset = 0; + if (BufferImpl::UboRingAllocate(bindSize, offset)) { + std::memcpy(static_cast(BufferImpl::UboRingMappedPtr()) + offset, + currentProgram->MapUBO(), uboSize); + ringSlot = {uboContentVersion, BufferImpl::UboRingGeneration(), frameSerial, + offset}; + slotValid = true; + } + } + if (slotValid) { + BufferImpl::BindBufferRangeCached(GL_UNIFORM_BUFFER, 0, BufferImpl::UboRingBufferId(), + static_cast(ringSlot.offset), + static_cast(bindSize)); + ringBound = true; + } + } + if (!ringBound) { + // Fallback (no EXT_buffer_storage / fences, or ring creation + // failed): the original in-place upload. + if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) { + g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId()); + g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(), + currentProgram->MapUBO()); + g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); + backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion); + } + BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, 0, + backendProgram.GetBackendGlobalUBOId()); } - BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, 0, backendProgram.GetBackendGlobalUBOId()); } { @@ -4129,6 +4167,9 @@ namespace MobileGL::MG_Backend::DirectGLES { g_completedFrameSerial.store(completed, std::memory_order_relaxed); } + // After the watermark advanced: retire grown-away UBO-ring stores and record + // the frame's ring high-water mark for slot reclamation. + BufferImpl::UboRingOnPresent(); BufferImpl::TrimBufferPool(); } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index a0823d1f..c09e015b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -9,6 +9,7 @@ #include "Managers.h" #include "Utils.h" #include "DirectGLES.h" +#include #include #include @@ -270,6 +271,13 @@ namespace MobileGL::MG_Backend::DirectGLES { // an older generation hold ids from a dead context. Uint g_bufferContextGeneration = 1; + // Defined next to the indexed-binding shadow below; forward-declared so + // every glDeleteBuffers site in this namespace can scrub stale shadow + // entries (GL resets a deleted buffer's indexed bindings to 0, and a + // recycled name matching a stale shadow entry would otherwise + // false-skip the rebind). + void ScrubIndexedBufferBindingShadowForId(Uint id); + // Resources whose owning BufferObject died; ids deleted at the next // sync point with a current ES context. Vector> g_deferredBufferReleases; @@ -313,6 +321,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const std::lock_guard lock(g_poolMutex); auto& bucket = g_bufferPool[r.storageSize]; if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) { + ScrubIndexedBufferBindingShadowForId(r.id); g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool r.id = 0; return; @@ -353,6 +362,56 @@ namespace MobileGL::MG_Backend::DirectGLES { return 0; } + // --- Global-UBO ring (see Managers.h) ------------------------------------ + constexpr SizeT kUboRingInitialBytes = 4u * 1024u * 1024u; + constexpr SizeT kUboRingMaxBytes = 64u * 1024u * 1024u; + + struct UboRingState { + Uint id = 0; + Uint8* mappedPtr = nullptr; + SizeT size = 0; + // Monotonic linear cursors: `head` counts every byte ever allocated + // (incl. wrap padding); everything below `tail` is GPU-complete. Ring + // offset of a linear position is pos % size, so in-flight bytes are + // head - tail and must stay <= size. + Uint64 head = 0; + Uint64 tail = 0; + Uint32 generation = 0; // bumped on every (re)create/grow; 0 = never valid + Uint contextGeneration = 0; + SizeT alignment = 256; + // A hard storage-creation failure under this context; stop retrying + // per draw (cleared when the context generation moves on). + Bool creationFailed = false; + }; + UboRingState g_uboRing; + + // Grown-away ring stores: deletable only once the GPU finished the last + // frame that could reference them (same watermark as the buffer pool). + struct RetiredUboRing { + Uint id = 0; + Uint contextGeneration = 0; + Uint64 retireSerial = 0; + }; + Vector g_retiredUboRings; + + // Present()-time high-water marks: every byte below headAtPresent was + // written during frames <= frameSerial, so once frameSerial completes, + // tail may advance to headAtPresent. FIFO by construction. + struct UboRingFrameMark { + Uint64 frameSerial = 0; + Uint64 headAtPresent = 0; + }; + Vector g_uboRingFrameMarks; + + // The ES context the ring's id/map belonged to is gone (or was never + // seen): drop every handle without GL calls and re-arm creation. + void ResetUboRingForNewContext() { + g_uboRing = {}; + g_uboRing.contextGeneration = g_bufferContextGeneration; + g_retiredUboRings.clear(); + g_uboRingFrameMarks.clear(); + } + GLESBufferResource* ResourceOf(BufferObject& bufferObject) { return static_cast(bufferObject.GetBackendResource().get()); } @@ -438,6 +497,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // Need a fresh id: glBufferStorage fails on a buffer that already has // immutable storage, and any prior mutable store is replaced anyway. if (resource->id != 0) { + ScrubIndexedBufferBindingShadowForId(resource->id); g_GLESFuncs.glDeleteBuffers(1, &resource->id); resource->id = 0; } @@ -565,6 +625,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { InvalidateArrayBufferBindingCache(); } + ScrubIndexedBufferBindingShadowForId(glesResource->id); g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); glesResource->id = 0; } @@ -602,6 +663,9 @@ namespace MobileGL::MG_Backend::DirectGLES { void OnBackendContextDestroyed() { UnregisterBufferBackendOps(); ++g_bufferContextGeneration; + // The global-UBO ring's id and persistent map died with the context; + // drop the handles (no GL) and let the next draw recreate the ring. + ResetUboRingForNewContext(); } void ProcessDeferredBufferReleases() { @@ -625,6 +689,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { InvalidateArrayBufferBindingCache(); } + ScrubIndexedBufferBindingShadowForId(glesResource->id); g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); glesResource->id = 0; } @@ -769,6 +834,21 @@ namespace MobileGL::MG_Backend::DirectGLES { if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index]; return nullptr; } + + // glDeleteBuffers resets the deleted buffer's bindings (indexed ones + // included) to 0 in the current context; mirror that in the shadow, or a + // later buffer recycling the same name with a matching range would + // false-skip its rebind. Default IndexedBufferBinding{} == base(0) == + // the post-delete GL state. + void ScrubIndexedBufferBindingShadowForId(Uint id) { + if (id == 0) return; + for (auto& binding : g_indexedUBOBindings) { + if (binding.id == id) binding = {}; + } + for (auto& binding : g_indexedSSBOBindings) { + if (binding.id == id) binding = {}; + } + } } // namespace void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) { @@ -812,6 +892,7 @@ namespace MobileGL::MG_Backend::DirectGLES { auto& bucket = g_bufferPool[oldestKey]; PooledBuffer& e = bucket[oldestIdx]; if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) { + ScrubIndexedBufferBindingShadowForId(e.id); g_GLESFuncs.glDeleteBuffers(1, &e.id); } g_pooledBytes -= e.size; @@ -827,6 +908,167 @@ namespace MobileGL::MG_Backend::DirectGLES { g_bufferPool.clear(); g_pooledBytes = 0; } + + // --- Global-UBO ring (see Managers.h) ------------------------------------ + namespace { + // (Re)create the ring store with room for at least minBytes. Any live + // store is retired (deleted once the GPU finished the last frame that + // could reference its slots), never deleted in place. Returns false and + // leaves the current store untouched when minBytes cannot fit under the + // size cap; a GL failure loses the store and latches creationFailed so + // draws stop retrying under this context. + Bool CreateUboRingStorage(SizeT minBytes) { + SizeT newSize = kUboRingInitialBytes; + while (newSize < minBytes) newSize *= 2; + if (newSize > kUboRingMaxBytes) return false; + + if (g_uboRing.id != 0) { + g_retiredUboRings.push_back( + {g_uboRing.id, g_uboRing.contextGeneration, DirectGLES::CurrentFrameSerial() + 1}); + } + const Uint32 nextGeneration = g_uboRing.generation + 1; + g_uboRing.id = 0; + g_uboRing.mappedPtr = nullptr; + + Uint id = 0; + g_GLESFuncs.glGenBuffers(1, &id); + if (id != 0) { + BindBufferId(TempBufferTarget, id); + g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast(newSize), nullptr, + GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); + void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(newSize), + GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); + if (!ptr) { + g_GLESFuncs.glDeleteBuffers(1, &id); + id = 0; + } else { + g_uboRing.mappedPtr = static_cast(ptr); + } + } + if (id == 0) { + MGLOG_E("Global-UBO ring: persistent storage creation failed (%zu bytes); " + "falling back to glBufferSubData uploads.", + newSize); + g_uboRing.creationFailed = true; + return false; + } + + const GLint capsAlignment = g_GLESCapabilities.UniformBufferOffsetAlignment; + SizeT alignment = capsAlignment > 0 ? static_cast(capsAlignment) : 256; + // The cursor math masks with (alignment - 1); spec doesn't promise a + // power of two, so round up to one. + SizeT pow2 = 1; + while (pow2 < alignment) pow2 <<= 1; + g_uboRing.id = id; + g_uboRing.size = newSize; + g_uboRing.head = 0; + g_uboRing.tail = 0; + g_uboRing.generation = nextGeneration; + g_uboRing.alignment = pow2; + g_uboRingFrameMarks.clear(); + return true; + } + } // namespace + + Bool UboRingAvailable() { + if (MG_Config::Features.DisableUboRing) return false; + // Reclamation rides the Present fence watermark; without working fences + // slots would never be provably GPU-idle (same rule as IsPoolable). + if (!g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers || + !g_GLESFuncs.glFenceSync || !g_GLESFuncs.glGetSynciv) { + return false; + } + if (!CanTouchGLNow()) return false; + if (g_uboRing.contextGeneration != g_bufferContextGeneration) { + ResetUboRingForNewContext(); + } + return !g_uboRing.creationFailed; + } + + Bool UboRingAllocate(SizeT size, SizeT& outOffset) { + if (size == 0 || !UboRingAvailable()) return false; + const SizeT alignedSize = (size + g_uboRing.alignment - 1) & ~(g_uboRing.alignment - 1); + if (g_uboRing.id == 0 && !CreateUboRingStorage(alignedSize)) { + return false; + } + + // Advance tail past every frame the GPU provably finished. + const Uint64 completed = DirectGLES::CompletedFrameSerial(); + SizeT retiredMarks = 0; + for (const auto& mark : g_uboRingFrameMarks) { + if (mark.frameSerial > completed) break; + if (mark.headAtPresent > g_uboRing.tail) g_uboRing.tail = mark.headAtPresent; + ++retiredMarks; + } + if (retiredMarks > 0) { + g_uboRingFrameMarks.erase(g_uboRingFrameMarks.begin(), + g_uboRingFrameMarks.begin() + static_cast(retiredMarks)); + } + + // A slot may not straddle the ring end; pad the cursor to the boundary. + SizeT offset = static_cast(g_uboRing.head % g_uboRing.size); + if (offset + alignedSize > g_uboRing.size) { + g_uboRing.head += g_uboRing.size - offset; + offset = 0; + } + + if (g_uboRing.head + alignedSize - g_uboRing.tail > g_uboRing.size) { + // In-flight span would overrun live slots: grow instead of overwrite. + if (CreateUboRingStorage(std::max(g_uboRing.size * 2, alignedSize))) { + offset = 0; + } else if (g_uboRing.creationFailed) { + return false; // store lost; callers fall back to glBufferSubData + } else { + // At the size cap (>kUboRingMaxBytes of uniforms in flight — not a + // real workload): drain the GPU once rather than corrupt live slots. + if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); + g_uboRing.tail = g_uboRing.head; + g_uboRingFrameMarks.clear(); + offset = static_cast(g_uboRing.head % g_uboRing.size); + if (offset + alignedSize > g_uboRing.size) { + g_uboRing.head += g_uboRing.size - offset; + offset = 0; + } + } + } + + g_uboRing.head += alignedSize; + outOffset = offset; + return true; + } + + void* UboRingMappedPtr() { return g_uboRing.mappedPtr; } + Uint UboRingBufferId() { return g_uboRing.id; } + Uint32 UboRingGeneration() { return g_uboRing.generation; } + + void UboRingOnPresent() { + if (!CanTouchGLNow()) return; + + // Delete grown-away stores the GPU is provably done with. + const Uint64 completed = DirectGLES::CompletedFrameSerial(); + for (SizeT i = g_retiredUboRings.size(); i-- > 0;) { + RetiredUboRing& entry = g_retiredUboRings[i]; + const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration; + if (!staleContext && entry.retireSerial > completed) continue; + if (!staleContext && entry.id != 0) { + ScrubIndexedBufferBindingShadowForId(entry.id); + g_GLESFuncs.glDeleteBuffers(1, &entry.id); + } + g_retiredUboRings[i] = g_retiredUboRings.back(); + g_retiredUboRings.pop_back(); + } + + if (g_uboRing.id == 0 || g_uboRing.contextGeneration != g_bufferContextGeneration) return; + // Record this frame's high-water mark (Present just fenced the serial now + // reported by CurrentFrameSerial()). A fence-less Present repeats the + // serial; fold into the existing mark. + const Uint64 serial = DirectGLES::CurrentFrameSerial(); + if (!g_uboRingFrameMarks.empty() && g_uboRingFrameMarks.back().frameSerial == serial) { + g_uboRingFrameMarks.back().headAtPresent = g_uboRing.head; + } else { + g_uboRingFrameMarks.push_back({serial, g_uboRing.head}); + } + } } // namespace BufferImpl namespace VertexArrayImpl { @@ -2576,13 +2818,24 @@ namespace MobileGL::MG_Backend::DirectGLES { void BackendProgramObjectImpl::CacheResourceLocations( const SharedPtr& stateProgramObject) { m_globalUboBackendBlockIndex = -1; + m_globalUboBackendBlockSize = 0; m_lastUploadedGlobalUboVersion = ~0u; + m_globalUboRingAllocation = {}; if (stateProgramObject->GetUBOSize() > 0) { const Uint blockIndex = g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); if (blockIndex != GL_INVALID_INDEX) { m_globalUboBackendBlockIndex = static_cast(blockIndex); g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, blockIndex, 0); + // Ring bindings are ranges and must span the block as the backend + // compiled it (its std140 padding may exceed the frontend's + // SPIR-V-reflected size). + if (g_GLESFuncs.glGetActiveUniformBlockiv) { + GLint blockDataSize = 0; + g_GLESFuncs.glGetActiveUniformBlockiv(m_backendProgramId, blockIndex, + GL_UNIFORM_BLOCK_DATA_SIZE, &blockDataSize); + m_globalUboBackendBlockSize = static_cast(blockDataSize); + } } else { MGLOG_W("Program %u has frontend global UBO storage, but backend has no %s block.", stateProgramObject->GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index c6c7e2e6..c1a82b6e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -189,6 +189,43 @@ namespace MobileGL::MG_Backend::DirectGLES { // without glDeleteBuffers (called when the ES context is going away). void TrimBufferPool(); void ClearBufferPool(); + + // --- Global-UBO ring ------------------------------------------------------ + // One persistently+coherently mapped buffer (EXT_buffer_storage) shared by + // every program's lowered default-uniform block. Each content change is + // bump-allocated into a fresh slot and bound with glBindBufferRange, so the + // CPU never rewrites bytes the GPU may still be reading — the per-draw + // glBufferSubData into one static UBO forced Adreno to resolve that + // write-after-read hazard on every uniform-dirtying draw (MC dirties + // uniforms every draw). Reclamation rides the Present() frame-fence + // watermark; no ring bytes are recycled before their frame's GPU work + // completed. + // + // A program's cached slot, reusable within one frame while the frontend UBO + // content version is unchanged. Cross-frame reuse is intentionally not + // attempted: later same-frame allocations may recycle bytes of completed + // frames, so re-referencing them would need per-bind pinning — rewriting + // GetUBOSize() bytes once per program per frame is far cheaper. + struct UboRingAllocation { + Uint32 contentVersion = ~0u; // frontend UBO content version held at `offset` + Uint32 ringGeneration = 0; // ring identity the slot lives in (0 = never valid) + Uint64 frameSerial = ~Uint64{0}; // frame the slot was written in + SizeT offset = 0; + }; + // False when the feature is disabled, EXT_buffer_storage / fences are + // missing, the ES context is not current, or ring creation already failed + // under this context (callers then take the legacy glBufferSubData path). + Bool UboRingAvailable(); + // Bump-allocate `size` bytes aligned to GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT. + // Grows the ring (new GL store, generation bump) when the in-flight span + // would be overrun. Returns false when storage (re)creation fails. + Bool UboRingAllocate(SizeT size, SizeT& outOffset); + void* UboRingMappedPtr(); + Uint UboRingBufferId(); + Uint32 UboRingGeneration(); + // Present()-time upkeep: records the frame's high-water mark for reclamation + // and deletes grown-away ring stores once the GPU is done with them. + void UboRingOnPresent(); } // namespace BufferImpl namespace VertexArrayImpl { @@ -392,6 +429,11 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector& GetSamplerUniformBindings() { return m_samplerUniformBindings; } Uint32 GetLastUploadedGlobalUboVersion() const { return m_lastUploadedGlobalUboVersion; } void SetLastUploadedGlobalUboVersion(Uint32 version) { m_lastUploadedGlobalUboVersion = version; } + // Backend-reported GL_UNIFORM_BLOCK_DATA_SIZE of the global block; ring + // bindings must span at least this much (may exceed the frontend's + // reflected size when the transpiled block pads differently). + Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; } + BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; } // Frontend link version this backend program (and its resource caches) was // built from; a mismatch means every link-derived cache here is stale. Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; } @@ -410,9 +452,11 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool m_isInitialized = false; Int m_globalUboBackendBlockIndex = -1; + Int m_globalUboBackendBlockSize = 0; Vector m_uniformBlockBackendIndices; // frontend block index -> backend index (-1 = absent) Vector m_samplerUniformBindings; Uint32 m_lastUploadedGlobalUboVersion = ~0u; + BufferImpl::UboRingAllocation m_globalUboRingAllocation; Uint32 m_syncedLinkVersion = ~0u; }; From c7ac5de28e9772ed4e7c1d9cc8887b4ff1236ce8 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 22:57:27 -0400 Subject: [PATCH 04/44] [Chore] (MG_Backend/DirectGLES): log global-UBO ring creation --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index c09e015b..57c39e17 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -966,6 +966,8 @@ namespace MobileGL::MG_Backend::DirectGLES { g_uboRing.generation = nextGeneration; g_uboRing.alignment = pow2; g_uboRingFrameMarks.clear(); + MGLOG_I("Global-UBO ring: %zu MiB persistent store ready (id %u, gen %u, align %zu).", + newSize / (1024u * 1024u), id, nextGeneration, pow2); return true; } } // namespace From f509b19b1d0aad9434dd5f48a8fa05561a1fe70b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:14:19 -0400 Subject: [PATCH 05/44] [Fix] (MG_Backend/DirectGLES): UBO ring review fixes - invalidate array-buffer bind cache on failed ring creation, bump generation on emergency drain, division-based alignment rounding --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 57c39e17..95892282 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -939,6 +939,9 @@ namespace MobileGL::MG_Backend::DirectGLES { void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(newSize), GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); if (!ptr) { + // The dying id is what the array-buffer cache has recorded as + // bound; a later buffer recycling the name would false-skip. + InvalidateArrayBufferBindingCache(); g_GLESFuncs.glDeleteBuffers(1, &id); id = 0; } else { @@ -954,20 +957,15 @@ namespace MobileGL::MG_Backend::DirectGLES { } const GLint capsAlignment = g_GLESCapabilities.UniformBufferOffsetAlignment; - SizeT alignment = capsAlignment > 0 ? static_cast(capsAlignment) : 256; - // The cursor math masks with (alignment - 1); spec doesn't promise a - // power of two, so round up to one. - SizeT pow2 = 1; - while (pow2 < alignment) pow2 <<= 1; g_uboRing.id = id; g_uboRing.size = newSize; g_uboRing.head = 0; g_uboRing.tail = 0; g_uboRing.generation = nextGeneration; - g_uboRing.alignment = pow2; + g_uboRing.alignment = capsAlignment > 0 ? static_cast(capsAlignment) : 256; g_uboRingFrameMarks.clear(); MGLOG_I("Global-UBO ring: %zu MiB persistent store ready (id %u, gen %u, align %zu).", - newSize / (1024u * 1024u), id, nextGeneration, pow2); + newSize / (1024u * 1024u), id, nextGeneration, g_uboRing.alignment); return true; } } // namespace @@ -989,7 +987,11 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool UboRingAllocate(SizeT size, SizeT& outOffset) { if (size == 0 || !UboRingAvailable()) return false; - const SizeT alignedSize = (size + g_uboRing.alignment - 1) & ~(g_uboRing.alignment - 1); + // Division-based rounding: the spec doesn't promise a power-of-two + // alignment. Slot offsets stay multiples of the alignment because every + // slot size is, and wrap padding restarts at ring offset 0. + const SizeT alignedSize = + (size + g_uboRing.alignment - 1) / g_uboRing.alignment * g_uboRing.alignment; if (g_uboRing.id == 0 && !CreateUboRingStorage(alignedSize)) { return false; } @@ -1026,6 +1028,10 @@ namespace MobileGL::MG_Backend::DirectGLES { if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); g_uboRing.tail = g_uboRing.head; g_uboRingFrameMarks.clear(); + // Same-frame slots written before the drain may now be recycled by + // the very next allocations; a generation bump keeps later draws + // from rebinding those cached offsets. + ++g_uboRing.generation; offset = static_cast(g_uboRing.head % g_uboRing.size); if (offset + alignedSize > g_uboRing.size) { g_uboRing.head += g_uboRing.size - offset; From 21753b0e4cf66e8679ae3536de512763950d989f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:19:47 -0400 Subject: [PATCH 06/44] [Fix] (MG_Backend/DirectGLES): UBO ring - preserve generation across ES context recreation, retire frame marks at Present to bound growth --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 95892282..b25e3e12 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -404,9 +404,14 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector g_uboRingFrameMarks; // The ES context the ring's id/map belonged to is gone (or was never - // seen): drop every handle without GL calls and re-arm creation. + // seen): drop every handle without GL calls and re-arm creation. The + // generation counter must survive the reset — frame serials also survive + // context recreation, so a restarted counter could revalidate a stale + // per-program slot cache against the new ring. void ResetUboRingForNewContext() { + const Uint32 keptGeneration = g_uboRing.generation; g_uboRing = {}; + g_uboRing.generation = keptGeneration; g_uboRing.contextGeneration = g_bufferContextGeneration; g_retiredUboRings.clear(); g_uboRingFrameMarks.clear(); @@ -1067,6 +1072,19 @@ namespace MobileGL::MG_Backend::DirectGLES { } if (g_uboRing.id == 0 || g_uboRing.contextGeneration != g_bufferContextGeneration) return; + // Retire completed marks here too — UboRingAllocate is the main consumer, + // but frames with no global-UBO draws would otherwise let the list grow + // one entry per Present, unboundedly. + SizeT retiredMarks = 0; + for (const auto& mark : g_uboRingFrameMarks) { + if (mark.frameSerial > completed) break; + if (mark.headAtPresent > g_uboRing.tail) g_uboRing.tail = mark.headAtPresent; + ++retiredMarks; + } + if (retiredMarks > 0) { + g_uboRingFrameMarks.erase(g_uboRingFrameMarks.begin(), + g_uboRingFrameMarks.begin() + static_cast(retiredMarks)); + } // Record this frame's high-water mark (Present just fenced the serial now // reported by CurrentFrameSerial()). A fence-less Present repeats the // serial; fold into the existing mark. From dc2f3a477b05a2cca9dca4f214ff516878758d4f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:22:13 -0400 Subject: [PATCH 07/44] [Chore] (MG_Backend/DirectGLES): demote UBO ring creation log to debug level --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index b25e3e12..340627b7 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -969,7 +969,7 @@ namespace MobileGL::MG_Backend::DirectGLES { g_uboRing.generation = nextGeneration; g_uboRing.alignment = capsAlignment > 0 ? static_cast(capsAlignment) : 256; g_uboRingFrameMarks.clear(); - MGLOG_I("Global-UBO ring: %zu MiB persistent store ready (id %u, gen %u, align %zu).", + MGLOG_D("Global-UBO ring: %zu MiB persistent store ready (id %u, gen %u, align %zu).", newSize / (1024u * 1024u), id, nextGeneration, g_uboRing.alignment); return true; } From 5331150cb96a581ae52733fefed39f8b9d0d91e8 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 01:50:33 -0400 Subject: [PATCH 08/44] [Fix] (MG_Util/ShaderTranspiler): stop stripping precision qualifiers - the strip corrupted "precision highp float;" into invalid syntax; glslang accepts and ignores them natively in 460 core (unblocks ~2000 GL CTS cases per backend) --- MobileGL/MG_Test/Program/ProgramUtilTest.cpp | 80 +++++++++++++++++-- .../ShaderSourceProcessor.cpp | 11 +-- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index ef645cac..652826f6 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -112,8 +112,14 @@ void main() { EXPECT_NE(source.find("out vec2 uv;"), String::npos); EXPECT_EQ(source.find("attribute"), String::npos); EXPECT_EQ(source.find("varying"), String::npos); - EXPECT_EQ(source.find("HIGHP_OR_DEFAULT"), String::npos); - EXPECT_EQ(source.find("#define"), String::npos); + // Precision-qualifier macros are left for glslang's own preprocessor to expand. + EXPECT_NE(source.find("#define HIGHP_OR_DEFAULT highp"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } } TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderModernizesGlmarkStyleSource) { @@ -137,9 +143,14 @@ void main() { EXPECT_NE(source.find("mg_FragColor = color;"), String::npos); EXPECT_EQ(source.find("gl_FragColor"), String::npos); EXPECT_EQ(source.find("texture2D"), String::npos); - EXPECT_EQ(source.find("MEDIUMP_OR_DEFAULT"), String::npos); - EXPECT_EQ(source.find("mediump"), String::npos); - EXPECT_EQ(source.find("#define"), String::npos); + // Precision-qualifier macros are left for glslang's own preprocessor to expand. + EXPECT_NE(source.find("#define MEDIUMP_OR_DEFAULT mediump"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } } TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderModernizesFragData) { @@ -166,6 +177,65 @@ void main() { } } +TEST_F(ProgramUtilTest, PreprocessKeepsDefaultPrecisionStatements) { + using namespace MG_Util::ShaderTranspiler; + + // Mirrors the GL CTS helper shaders (e.g. glcPixelStorageModesTests): the old qualifier strip + // turned "precision highp float;" into invalid "precision float;". Precision qualifiers are + // legal (and ignored) in the forced 460 core profile, so they now pass through untouched. + String source = R"(#version 330 +precision highp float; +precision mediump int; +out vec4 fragColor; +uniform highp sampler2D tex; + +void main() { + highp vec2 uv = vec2(0.5); + fragColor = texture(tex, uv); +})"; + + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_NE(source.find("precision highp float;"), String::npos); + EXPECT_NE(source.find("precision mediump int;"), String::npos); + EXPECT_NE(source.find("uniform highp sampler2D tex;"), String::npos); + EXPECT_NE(source.find("fragColor = texture(tex, uv);"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + +TEST_F(ProgramUtilTest, PreprocessKeepsPrecisionInLegacyShaderForGlslang) { + using namespace MG_Util::ShaderTranspiler; + + // Legacy ES-style shader: precision statements and qualifier macros are left for glslang + // (its preprocessor expands the #define; the 460 core parse ignores the qualifiers). + String source = R"(#define HIGHP_OR_DEFAULT highp +precision HIGHP_OR_DEFAULT float; +precision mediump int; +varying vec2 uv; + +void main() { + mediump float shade = uv.x; + gl_FragColor = vec4(uv, shade, 1.0); +})"; + + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_NE(source.find("precision HIGHP_OR_DEFAULT float;"), String::npos); + EXPECT_NE(source.find("precision mediump int;"), String::npos); + EXPECT_NE(source.find("in vec2 uv;"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + TEST_F(ProgramUtilTest, PreprocessFragmentShaderInjectsDepthRangeShim) { using namespace MG_Util::ShaderTranspiler; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index 2cf8dcb2..43c2d5aa 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -262,15 +262,8 @@ namespace { } void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) { - RemoveDefineForIdentifier(source, "HIGHP_OR_DEFAULT"); - RemoveDefineForIdentifier(source, "MEDIUMP_OR_DEFAULT"); - RemoveDefineForIdentifier(source, "LOWP_OR_DEFAULT"); - ReplaceIdentifier(source, "HIGHP_OR_DEFAULT", ""); - ReplaceIdentifier(source, "MEDIUMP_OR_DEFAULT", ""); - ReplaceIdentifier(source, "LOWP_OR_DEFAULT", ""); - ReplaceIdentifier(source, "highp", ""); - ReplaceIdentifier(source, "mediump", ""); - ReplaceIdentifier(source, "lowp", ""); + // Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and + // ignored in the forced "#version 460 core" profile, so glslang handles them natively. ReplaceIdentifier(source, "texture2D", "texture"); ReplaceIdentifier(source, "texture2DProj", "textureProj"); From fe7a5ee1b2b2905d91b995d1d70b37ce1b58bf54 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 02:23:18 -0400 Subject: [PATCH 09/44] [Fix] (MG_Impl/GLImpl, MG_State, MG_Backend/DirectGLES): eliminate packed_pixels SIGTRAPs - complete TexImage format/type/internalformat validation matrix (depth-stencil family, integer-ness, packed-type pairing, 3D depth rejection), fix inverted UpdateSubData assert with clamped copy, demote unimplemented readback asserts to logged no-ops --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 49 +++--- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 42 +++-- .../MG_Impl/GLImpl/Texture/Validators.cpp | 145 +++++++++++++----- .../GLState/TextureState/MipmapStorage.cpp | 5 +- MobileGL/MG_Test/Texture/TextureTest.cpp | 88 +++++++++++ 5 files changed, 258 insertions(+), 71 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index e53a2ea6..7b926059 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3266,17 +3266,20 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ReadPixels: x=%d y=%d w=%d h=%d format=%s type=%s pixels=%p", x, y, width, height, MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), pixels); - MOBILEGL_ASSERT(format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || - format == GL_RED_INTEGER || format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX, - "Only GL_RGBA, GL_RGBA_INTEGER, GL_RED, GL_RED_INTEGER, GL_DEPTH_COMPONENT and " - "GL_STENCIL_INDEX are supported currently, " - "while requested %s.", - MG_Util::ConvertGLEnumToString(format).c_str()); - MOBILEGL_ASSERT(type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV || - type == GL_INT || type == GL_FLOAT, - "Only GL_UNSIGNED_BYTE, GL_UNSIGNED_INT, GL_UNSIGNED_INT_2_10_10_10_REV, " - "GL_INT and GL_FLOAT are supported currently, while requested %s.", - MG_Util::ConvertGLEnumToString(type).c_str()); + // Unimplemented readback formats degrade to a logged no-op instead of killing the process; + // spec-invalid combinations are already rejected with GL errors at the state layer. + if (format != GL_RGBA && format != GL_RGBA_INTEGER && format != GL_RED && format != GL_RED_INTEGER && + format != GL_DEPTH_COMPONENT && format != GL_STENCIL_INDEX) { + MGLOG_E("ReadPixels: format %s is not implemented yet, skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str()); + return; + } + if (type != GL_UNSIGNED_BYTE && type != GL_UNSIGNED_INT && type != GL_UNSIGNED_INT_2_10_10_10_REV && + type != GL_INT && type != GL_FLOAT) { + MGLOG_E("ReadPixels: type %s is not implemented yet, skipping readback", + MG_Util::ConvertGLEnumToString(type).c_str()); + return; + } MGLOG_D("ReadPixels: SyncNeccessaryTextures()"); TextureImpl::SyncNeccessaryTextures(); @@ -3362,16 +3365,20 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_Util::ConvertGLEnumToString(target).c_str(), level, MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), pixels); - MOBILEGL_ASSERT(format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_BGRA, - "Only GL_RGBA, GL_RGBA_INTEGER and GL_BGRA are supported currently, while requested %s.", - MG_Util::ConvertGLEnumToString(format).c_str()); - MOBILEGL_ASSERT(type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV || - type == GL_INT || type == GL_FLOAT || type == GL_UNSIGNED_INT_8_8_8_8 || - type == GL_UNSIGNED_INT_8_8_8_8_REV || type == GL_HALF_FLOAT, - "Only GL_UNSIGNED_BYTE, GL_UNSIGNED_INT, GL_UNSIGNED_INT_2_10_10_10_REV, " - "GL_INT, GL_FLOAT, GL_HALF_FLOAT, GL_UNSIGNED_INT_8_8_8_8 and GL_UNSIGNED_INT_8_8_8_8_REV " - "are supported currently, while requested %s.", - MG_Util::ConvertGLEnumToString(type).c_str()); + // Unimplemented readback formats degrade to a logged no-op instead of killing the process; + // spec-invalid combinations are already rejected with GL errors at the state layer. + if (format != GL_RGBA && format != GL_RGBA_INTEGER && format != GL_BGRA) { + MGLOG_E("GetTexImage: format %s is not implemented yet, skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str()); + return; + } + if (type != GL_UNSIGNED_BYTE && type != GL_UNSIGNED_INT && type != GL_UNSIGNED_INT_2_10_10_10_REV && + type != GL_INT && type != GL_FLOAT && type != GL_UNSIGNED_INT_8_8_8_8 && + type != GL_UNSIGNED_INT_8_8_8_8_REV && type != GL_HALF_FLOAT) { + MGLOG_E("GetTexImage: type %s is not implemented yet, skipping readback", + MG_Util::ConvertGLEnumToString(type).c_str()); + return; + } GLenum esFormat = format, esType = type; if (esFormat == GL_BGRA) esFormat = GL_RGBA; diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 0e790ad4..88201752 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -1357,6 +1357,18 @@ namespace MobileGL::MG_Impl::GLImpl { return; if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return; + // Depth and depth-stencil formats are not three-dimensional in core GL (2D-array targets are fine). + if ((textureUploadTarget == TextureUploadTarget::Texture3D || + textureUploadTarget == TextureUploadTarget::ProxyTexture3D) && + (textureInputFormat == TextureInputFormat::DepthComponent || + textureInputFormat == TextureInputFormat::DepthStencil)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "Depth formats are invalid for 3D texture targets")); + return; + } + // TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the // GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped. // GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER @@ -2648,17 +2660,25 @@ namespace MobileGL::MG_Impl::GLImpl { } } - // Special case for depth/stencil - if (textureInputFormat == TextureInputFormat::StencilIndex) { - if (textureObject->GetFormat() != TextureInternalFormat::DepthStencil && - textureObject->GetFormat() != TextureInternalFormat::Depth24Stencil8 && - textureObject->GetFormat() != TextureInternalFormat::Depth32FStencil8) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "GetTexImage_State", - "No stencil buffer for stencil index format")); - return false; - } + // Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch, + // integer-ness). Also rejects STENCIL_INDEX readback, which needs GL_ARB_texture_stencil8 + // (not advertised by MobileGL). + if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput( + textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) { + return false; + } + + // GetTexImage-specific: DEPTH_STENCIL readback needs a depth-stencil texture (a depth-only + // texture has no stencil data to return). + if (textureInputFormat == TextureInputFormat::DepthStencil && + textureObject->GetFormat() != TextureInternalFormat::DepthStencil && + textureObject->GetFormat() != TextureInternalFormat::Depth24Stencil8 && + textureObject->GetFormat() != TextureInternalFormat::Depth32FStencil8) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "GetTexImage_State", + "DEPTH_STENCIL readback requires a depth-stencil texture")); + return false; } return true; diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index 96b7dfaa..bb060efe 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -175,61 +175,132 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } + static Bool IsIntegerColorInputFormat(TextureInputFormat format) { + return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger || + format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger || + format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger; + } + + static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) { + switch (internalFormat) { + case TextureInternalFormat::R8I: + case TextureInternalFormat::R8UI: + case TextureInternalFormat::R16I: + case TextureInternalFormat::R16UI: + case TextureInternalFormat::R32I: + case TextureInternalFormat::R32UI: + case TextureInternalFormat::RG8I: + case TextureInternalFormat::RG8UI: + case TextureInternalFormat::RG16I: + case TextureInternalFormat::RG16UI: + case TextureInternalFormat::RG32I: + case TextureInternalFormat::RG32UI: + case TextureInternalFormat::RGB8I: + case TextureInternalFormat::RGB8UI: + case TextureInternalFormat::RGB16I: + case TextureInternalFormat::RGB16UI: + case TextureInternalFormat::RGB32I: + case TextureInternalFormat::RGB32UI: + case TextureInternalFormat::RGBA8I: + case TextureInternalFormat::RGBA8UI: + case TextureInternalFormat::RGBA16I: + case TextureInternalFormat::RGBA16UI: + case TextureInternalFormat::RGBA32I: + case TextureInternalFormat::RGBA32UI: + case TextureInternalFormat::RGB10A2UI: + return true; + default: + return false; + } + } + + static Bool IsDepthLikeInternalFormat(TextureInternalFormat internalFormat) { + switch (internalFormat) { + case TextureInternalFormat::DepthComponent: + case TextureInternalFormat::DepthComponent16: + case TextureInternalFormat::DepthComponent24: + case TextureInternalFormat::DepthComponent32: // not core, kept for Minecraft 1.21.5+ + case TextureInternalFormat::DepthComponent32F: + case TextureInternalFormat::Depth24Stencil8: + case TextureInternalFormat::Depth32FStencil8: + case TextureInternalFormat::DepthStencil: + return true; + default: + return false; + } + } + + static Bool IsDepthLikeInputFormat(TextureInputFormat format) { + return format == TextureInputFormat::DepthComponent || format == TextureInputFormat::DepthStencil || + format == TextureInputFormat::StencilIndex; + } + + // Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests + // isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and + // integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path. Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format, TextureInternalFormat internalFormat, TexturePixelDataType type) { + const auto recordInvalidOperation = [](const char* message) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput", + message)); + return false; + }; + if (type == TexturePixelDataType::UnsignedByte332 || type == TexturePixelDataType::UnsignedByte233Rev || - type == TexturePixelDataType::UnsignedShort565 || type == TexturePixelDataType::UnsignedShort565Rev || - type == TexturePixelDataType::UnsignedInt101111Rev) { + type == TexturePixelDataType::UnsignedShort565 || type == TexturePixelDataType::UnsignedShort565Rev) { + if (format != TextureInputFormat::RGB && format != TextureInputFormat::RGBInteger) { + return recordInvalidOperation("Packed RGB type requires RGB or RGB_INTEGER format"); + } + } + + if (type == TexturePixelDataType::UnsignedInt101111Rev || type == TexturePixelDataType::UnsignedInt5999Rev) { if (format != TextureInputFormat::RGB) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput", - "Invalid format for the given type")); - return false; + return recordInvalidOperation("Packed float RGB type requires RGB format"); } } if (type == TexturePixelDataType::UnsignedShort4444 || type == TexturePixelDataType::UnsignedShort4444Rev || type == TexturePixelDataType::UnsignedShort5551 || type == TexturePixelDataType::UnsignedShort1555Rev || type == TexturePixelDataType::UnsignedInt8888 || type == TexturePixelDataType::UnsignedInt8888Rev || - type == TexturePixelDataType::UnsignedInt1010102 || type == TexturePixelDataType::UnsignedInt2101010Rev || - type == TexturePixelDataType::UnsignedInt5999Rev) { - if (format != TextureInputFormat::RGBA && format != TextureInputFormat::BGRA) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput", - "Invalid format for the given type")); - return false; + type == TexturePixelDataType::UnsignedInt1010102 || type == TexturePixelDataType::UnsignedInt2101010Rev) { + if (format != TextureInputFormat::RGBA && format != TextureInputFormat::BGRA && + format != TextureInputFormat::RGBAInteger && format != TextureInputFormat::BGRAInteger) { + return recordInvalidOperation("Packed RGBA type requires RGBA/BGRA (integer) format"); } } - if (internalFormat == TextureInternalFormat::DepthComponent || - internalFormat == TextureInternalFormat::DepthComponent16 || - internalFormat == TextureInternalFormat::DepthComponent24 || - internalFormat == TextureInternalFormat::DepthComponent32F) { - if (format != TextureInputFormat::DepthComponent) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput", - "Invalid format for depth component internal format")); - return false; + if (type == TexturePixelDataType::UnsignedInt248 || type == TexturePixelDataType::Float32UnsignedInt248Rev) { + if (format != TextureInputFormat::DepthStencil) { + return recordInvalidOperation("Packed depth-stencil type requires DEPTH_STENCIL format"); } } - if (format == TextureInputFormat::DepthComponent && - (internalFormat != TextureInternalFormat::DepthComponent && - internalFormat != TextureInternalFormat::DepthComponent16 && - internalFormat != TextureInternalFormat::DepthComponent24 && - internalFormat != TextureInternalFormat::DepthComponent32F && - internalFormat != TextureInternalFormat::DepthComponent32 // workaround for Minecraft 1.21.5+ - )) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput", - "Invalid internal format for depth component format")); - return false; + if (format == TextureInputFormat::DepthStencil && type != TexturePixelDataType::UnsignedInt248 && + type != TexturePixelDataType::Float32UnsignedInt248Rev) { + return recordInvalidOperation("DEPTH_STENCIL format requires a packed depth-stencil type"); } + + if (IsIntegerColorInputFormat(format) && + (type == TexturePixelDataType::Float || type == TexturePixelDataType::HalfFloat)) { + return recordInvalidOperation("Integer format cannot be used with a floating-point type"); + } + + // TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4). + if (format == TextureInputFormat::StencilIndex) { + return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format"); + } + + if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) { + return recordInvalidOperation("Depth/stencil-ness of format and internal format must match"); + } + + if (IsIntegerColorInputFormat(format) != IsIntegerColorInternalFormat(internalFormat)) { + return recordInvalidOperation("Integer-ness of format and internal format must match"); + } + return true; } diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index 3c227c31..e4127398 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -31,11 +31,12 @@ namespace MobileGL { auto& targetData = m_data; MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range"); auto& levelData = targetData[level]; - MOBILEGL_ASSERT(levelData.size() <= input.size, "UpdateSubData: input data larger than allocated"); + MOBILEGL_ASSERT(input.size <= levelData.size(), "UpdateSubData: input data larger than allocated"); if (input.data && input.size > 0) { const Uint8* src = static_cast(input.data); - Memcpy(levelData.data(), src, input.size); + // Clamp so a size mismatch can never write past the allocation. + Memcpy(levelData.data(), src, std::min(input.size, levelData.size())); m_isDirty[level] = true; } } diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index a2fc634c..462e30c9 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -257,6 +257,94 @@ TEST_F(TextureTest, BoundTexImage2DUnpacksPackedBgra8888ToRgba8WithPixelStoreSki EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// GL CTS packed_pixels feeds every format/type/internalformat combination to TexImage and expects +// GL_INVALID_OPERATION for the invalid ones; these used to slip through validation and SIGTRAP in +// the shadow-storage upload path. +TEST_F(TextureTest, TexImage2DRejectsMismatchedFormatCombinations) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + // Depth-stencil internal format with a color format. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 2, 2, 0, GL_BGR, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Color internal format with a depth format. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Stencil-only uploads do not exist in core 3.3. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 2, 2, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, + nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Packed depth-stencil type with a color format. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_INT_24_8, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // DEPTH_STENCIL format requires one of the two packed depth-stencil types. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 2, 2, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_BYTE, + nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Integer-ness of format and internal format must match (both directions). + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8UI, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Integer formats cannot be paired with floating-point types. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32I, 2, 2, 0, GL_RGBA_INTEGER, GL_FLOAT, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // UNSIGNED_INT_5_9_9_9_REV pairs with RGB only. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 2, 2, 0, GL_RGBA, GL_UNSIGNED_INT_5_9_9_9_REV, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); +} + +TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 2, 2, 0, GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Depth-component internal format accepts DEPTH_STENCIL input (stencil bits are dropped). + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 2, 2, 0, GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8UI, 2, 2, 0, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Packed RGB types allow the integer variant of the RGB format. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB8UI, 2, 2, 0, GL_RGB_INTEGER, GL_UNSIGNED_BYTE_3_3_2, + nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 2, 2, 0, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, TexImage3DRejectsDepthFormatsForThreeDimensionalTarget) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_DEPTH24_STENCIL8, 2, 2, 2, 0, GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // 2D-array targets remain valid for depth formats. + GLuint arrayTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &arrayTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, arrayTexture); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_DEPTH24_STENCIL8, 2, 2, 2, 0, GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, BoundTexSubImage2DUnpacksPackedBgra8888RevToRgba8) { GLuint texture = 0; MG_Impl::GLImpl::GenTextures(1, &texture); From 0b94e02de59da3aa44d610e1bb568889297bfa17 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 02:36:07 -0400 Subject: [PATCH 10/44] [Fix] (MG_Backend/DirectVulkan): skip combined depth-stencil texture data uploads instead of recording invalid single-copy with multi-bit aspect mask (VK_INCOMPLETE at vkEndCommandBuffer killed the process); proper per-aspect de-interleave tracked separately --- .../DirectVulkan/Renderer/VkTextureManager.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 50e6562e..da38a978 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1424,6 +1424,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + // Combined depth-stencil images need per-aspect de-interleaved copies (VkBufferImageCopy + // aspectMask must have exactly one bit set). Until that is implemented, skip the upload + // instead of recording an invalid command buffer that kills the process. + const VkImageAspectFlags uploadAspectMask = GetAspectMaskForFormat(outResource.format); + if ((uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT)) { + MGLOG_E("UploadDirtyMipLevels: skipping unimplemented depth-stencil data upload for textureId=%d", + mipmapTexture.GetExternalIndex()); + for (const auto& item : uploadItems) { + mipmapTexture.MarkStorageDirty(item.target, item.level, false); + } + return true; + } + VkBuffer stagingBuffer = VK_NULL_HANDLE; VmaAllocation stagingAllocation = nullptr; From 176d130f097b8635f1771c111f3a4c915ff066ed Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:06:03 -0400 Subject: [PATCH 11/44] [Fix] (MG_Backend): GetTexImage level-range check was off-by-one (max level is inclusive; single-level textures asserted on level 0), demoted to logged skip; advertise GL_ARB_texture_storage_multisample (entry points already implemented - unadvertised extension left null glw pointers and CTS framebuffer_blit jumped to address 0) --- .../DirectGLES/BackendObject_DirectGLES.cpp | 3 ++- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 13 ++++++------- .../DirectVulkan/BackendObject_DirectVulkan.cpp | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index de7dd358..0c5b62be 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -824,7 +824,8 @@ namespace MobileGL::MG_Backend::DirectGLES { E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, - E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access, + E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, + E_GL_ARB_direct_state_access, E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 7b926059..4246f071 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3450,14 +3450,13 @@ namespace MobileGL::MG_Backend::DirectGLES { auto* textureMipmapObject = static_cast(textureObject.get()); auto& levelRange = textureMipmapObject->GetLevelRange(); - MGLOG_D("GetTexImage: mipmap level range = [%d, %d)", levelRange.x(), levelRange.y()); + MGLOG_D("GetTexImage: mipmap level range = [%d, %d]", levelRange.x(), levelRange.y()); - if (level < levelRange.x() || level >= levelRange.y()) { - MGLOG_E("GetTexImage: Requested level %d out of range", level); - MOBILEGL_ASSERT(false, - "GetTexImage: Requested level %d is out of range " - "(base level %d, max level %d).", - level, levelRange.x(), levelRange.y()); + // levelRange.y() is GL_TEXTURE_MAX_LEVEL, an inclusive level index — a single-level + // texture has range [0, 0] and level 0 must be readable. + if (static_cast(level) < levelRange.x() || static_cast(level) > levelRange.y()) { + MGLOG_E("GetTexImage: Requested level %d is out of range (base level %u, max level %u), skipping readback", + level, levelRange.x(), levelRange.y()); return; } diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 44502133..4f9f4788 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -502,7 +502,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, - E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access, + E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, + E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size}; From cf8f928db8a83457db614e2c0bbdf1e3b1f8aa64 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:31:32 -0400 Subject: [PATCH 12/44] [Fix] (MG_Impl/GLImpl, MG_State): fallback UBO backing for optimizer-eliminated uniforms (null-MapUBO SIGSEGV in KHR-GL33 do_while loops) + per-element locations/offsets for array uniforms incl. nested struct arrays (size assert in KHR-GL33 struct.uniform); demote uniform write assert to log-and-clamp --- .../MG_Impl/GLImpl/Program/GL_Program.cpp | 74 ++++++- .../GLState/ProgramState/ProgramObject.cpp | 148 ++++++++++--- .../GLState/ProgramState/ProgramObject.h | 39 +++- MobileGL/MG_Test/Program/ProgramTest.cpp | 208 ++++++++++++++++++ .../MG_Util/ShaderTranspiler/SpvcSession.cpp | 81 +++++-- .../MG_Util/ShaderTranspiler/SpvcSession.h | 5 + 6 files changed, 504 insertions(+), 51 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index d9a87961..ea8a1201 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -735,6 +735,12 @@ namespace MobileGL::MG_Impl::GLImpl { auto size = programObject->GetUniformSizesInBytes(location); char* pUBO = (char*)programObject->MapUBO(); auto* ttype = programObject->GetUniformTType(location); + if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || + offset + size > programObject->GetUBOSize()) { + MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__, + program, location); + return; + } if (!ttype->isMatrix() || ttype->getMatrixCols() != 3) Memcpy(params, pUBO + offset, size); @@ -784,6 +790,12 @@ namespace MobileGL::MG_Impl::GLImpl { auto size = programObject->GetUniformSizesInBytes(location); char* pUBO = static_cast(programObject->MapUBO()); auto* ttype = programObject->GetUniformTType(location); + if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || + offset + size > programObject->GetUBOSize()) { + MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__, + program, location); + return; + } if constexpr (std::is_same_v) { if (ttype->isMatrix() && ttype->getMatrixCols() == 3) { @@ -900,14 +912,31 @@ namespace MobileGL::MG_Impl::GLImpl { if (!programObject.IsUniformOpaqueAtLocation(location)) { MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(), location, programObject.GetMaxUniformLocation()); - auto size = programObject.GetUniformSizesInBytes(location); - auto offset = programObject.GetUniformOffset(location); - MOBILEGL_ASSERT(size >= ItemCount * sizeof(T), - "Uniform size mismatch, expected at least %zu bytes, got %zu bytes.", ItemCount * sizeof(T), - size); + const SizeT size = programObject.GetUniformSizesInBytes(location); + const Uint offset = programObject.GetUniformOffset(location); + char* pUBO = static_cast(programObject.MapUBO()); + const SizeT uboSize = programObject.GetUBOSize(); + SizeT writeSize = ItemCount * sizeof(T); + if (size < writeSize) { + // Metadata bug: degrade to a clamped copy instead of killing the process. + MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu " + "bytes; clamping", + __func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size); + writeSize = size; + } + if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || + offset + byteOffsetInsideUniform + writeSize > uboSize) { + // Should not happen: linking gives every settable uniform backing + // storage. Log and drop the write instead of faulting. + MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu " + "uboSize=%zu); dropping write", + __func__, programObject.GetExternalIndex(), location, static_cast(pUBO), offset, + writeSize, uboSize); + return; + } MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(), location, offset + byteOffsetInsideUniform); - Memcpy((char*)programObject.MapUBO() + offset + byteOffsetInsideUniform, value, ItemCount * sizeof(T)); + Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize); programObject.MarkUBOContentDirty(); } else { auto* ttype = programObject.GetUniformTType(location); @@ -940,6 +969,11 @@ namespace MobileGL::MG_Impl::GLImpl { } for (GLint offset = 0; offset < count; offset++) { + if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) { + // GL 3.3 §2.11.4: values for elements beyond the end of the uniform + // array are ignored. Never step onto a neighboring uniform's location. + break; + } if (!programObject->IsValidUniformLocation(location + offset)) { RecordInvalidUniformLocationError(__func__, location + offset, "the current program object"); return; @@ -964,6 +998,10 @@ namespace MobileGL::MG_Impl::GLImpl { } for (GLint offset = 0; offset < count; offset++) { + if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) { + // Values for elements beyond the end of the uniform array are ignored. + break; + } if (!programObject->IsValidUniformLocation(location + offset)) { RecordInvalidUniformLocationError(__func__, location + offset, "program " + std::to_string(program)); @@ -1092,6 +1130,10 @@ namespace MobileGL::MG_Impl::GLImpl { // For matrix uniforms, we handle each matrix individually for (GLint i = 0; i < count; i++) { + if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { + // Values for elements beyond the end of the uniform array are ignored. + break; + } if (!programObject->IsValidUniformLocation(location + i)) { RecordInvalidUniformLocationError(__func__, location + i, "the current program object"); return; @@ -1124,6 +1166,10 @@ namespace MobileGL::MG_Impl::GLImpl { // For matrix uniforms, we handle each matrix individually // Handle padding in mat3 correctly!! for (GLint i = 0; i < count; i++) { + if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { + // Values for elements beyond the end of the uniform array are ignored. + break; + } if (!programObject->IsValidUniformLocation(location + i)) { RecordInvalidUniformLocationError(__func__, location + i, "the current program object"); return; @@ -1159,6 +1205,10 @@ namespace MobileGL::MG_Impl::GLImpl { // For matrix uniforms, we handle each matrix individually for (GLint i = 0; i < count; i++) { + if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { + // Values for elements beyond the end of the uniform array are ignored. + break; + } if (!programObject->IsValidUniformLocation(location + i)) { RecordInvalidUniformLocationError(__func__, location + i, "the current program object"); return; @@ -1219,6 +1269,10 @@ namespace MobileGL::MG_Impl::GLImpl { } for (GLint i = 0; i < count; i++) { + if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { + // Values for elements beyond the end of the uniform array are ignored. + break; + } if (!programObject->IsValidUniformLocation(location + i)) { RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program)); return; @@ -1249,6 +1303,10 @@ namespace MobileGL::MG_Impl::GLImpl { } for (GLint i = 0; i < count; i++) { + if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { + // Values for elements beyond the end of the uniform array are ignored. + break; + } if (!programObject->IsValidUniformLocation(location + i)) { RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program)); return; @@ -1283,6 +1341,10 @@ namespace MobileGL::MG_Impl::GLImpl { } for (GLint i = 0; i < count; i++) { + if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { + // Values for elements beyond the end of the uniform array are ignored. + break; + } if (!programObject->IsValidUniformLocation(location + i)) { RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program)); return; diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 8abe3b5d..5acd0bd6 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -8,6 +8,7 @@ #include "ProgramObject.h" #include +#include #include #include #include @@ -87,6 +88,19 @@ namespace { } } + // How many consecutive uniform locations a uniform occupies. Array uniforms (opaque + // or not) span one location per element so glUniform*v(count > 1) and + // glGetUniformLocation("arr[k]") can address elements individually; everything else + // spans a single location. TObjectReflection.size only carries the element count for + // non-block arrays, so prefer the TType, which is authoritative for both. + static MobileGL::Int GetUniformLocationSpan(const glslang::TObjectReflection& uniform) { + const glslang::TType* type = uniform.getType(); + if (type != nullptr && type->isSizedArray()) { + return std::max(1, type->getOuterArraySize()); + } + return std::max(1, uniform.size); + } + static bool ComputeShaderDeclaresLocalSize(const MobileGL::String& source) { bool inLineComment = false; bool inBlockComment = false; @@ -361,8 +375,7 @@ namespace MobileGL::MG_State::GLState { for (int i = 0; i < m_activeUniformCount; i++) { auto& uniform = m_program->getUniform(i); auto location = uniform.layoutLocation(); - const Int locationSpan = - (uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1; + const Int locationSpan = GetUniformLocationSpan(uniform); requiredUniformLocations += locationSpan; if (location != glslang::TQualifier::layoutLocationEnd) { m_maxUniformLocation = std::max(m_maxUniformLocation, location + locationSpan - 1); @@ -401,8 +414,7 @@ namespace MobileGL::MG_State::GLState { m_externalIndex, uniform.name.c_str()); continue; // will allocate unallocated uniforms later } - const Int locationSpan = - (uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1; + const Int locationSpan = GetUniformLocationSpan(uniform); for (Int element = 0; element < locationSpan; ++element) { m_uniformIndexInTProgram[location + element] = i; } @@ -419,8 +431,8 @@ namespace MobileGL::MG_State::GLState { }); for (auto index : unallocatedUniformIndex) { auto& uniform = m_program->getUniform(index); - const Int locationSpan = - (uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1; + const Int locationSpan = GetUniformLocationSpan(uniform); + Bool placed = false; for (; locNeedle <= m_maxUniformLocation; locNeedle++) { bool hasRoom = locNeedle + locationSpan - 1 <= m_maxUniformLocation; for (Int element = 0; hasRoom && element < locationSpan; ++element) { @@ -437,8 +449,25 @@ namespace MobileGL::MG_State::GLState { "(index %d)", m_externalIndex, uniform.name.c_str(), locNeedle, locNeedle + locationSpan - 1, index); locNeedle += locationSpan; + placed = true; break; } + if (!placed) { + // Explicit-location uniforms can fragment the space so no contiguous + // span is left; grow the table instead of leaving the uniform without + // a location (which would make it unsettable via glUniform*). + const SizeT base = m_uniformIndexInTProgram.size(); + m_uniformIndexInTProgram.resize(base + locationSpan, glslang::TQualifier::layoutLocationEnd); + m_uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1); + m_maxUniformLocation = static_cast(base + locationSpan - 1); + for (Int element = 0; element < locationSpan; ++element) { + m_uniformIndexInTProgram[base + element] = index; + } + m_uniformLocations[uniform.name] = static_cast(base); + MGLOG_D("ProgramObject %u: Reflection - grew location table to place uniform '%s' at %zu..%zu", + m_externalIndex, uniform.name.c_str(), base, base + locationSpan - 1); + locNeedle = base + locationSpan; + } } for (int i = 0; i < m_activeUniformCount; i++) { @@ -457,7 +486,7 @@ namespace MobileGL::MG_State::GLState { const auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name); const int initialUnit = explicitBinding != m_explicitOpaqueUniformBindings.end() ? static_cast(explicitBinding->second) : 0; - const Int locationSpan = std::max(1, uniform.size); + const Int locationSpan = GetUniformLocationSpan(uniform); for (Int element = 0; element < locationSpan && location + element < m_uniformSamplerOrImageUnitIndex.size(); ++element) { m_uniformSamplerOrImageUnitIndex[location + element] = @@ -622,8 +651,11 @@ namespace MobileGL::MG_State::GLState { m_uniformSizesInBytes.clear(); m_uniformOffsets.clear(); m_globalUboScratch.clear(); - m_uniformOffsets.resize(m_maxUniformLocation + 1); - m_uniformSizesInBytes.resize(m_maxUniformLocation + 1); + // kInvalidUniformOffset marks locations that end up without global-UBO backing + // (e.g. the optimizer eliminated every use of the uniform); the fallback pass + // below gives those locations tail storage so glUniform* always has a target. + m_uniformOffsets.resize(m_maxUniformLocation + 1, kInvalidUniformOffset); + m_uniformSizesInBytes.resize(m_maxUniformLocation + 1, 0); for (SizeT i = 0; i < m_generatedSpirv.size(); i++) { auto& spv = m_generatedSpirv[i]; @@ -653,31 +685,89 @@ namespace MobileGL::MG_State::GLState { m_globalUboScratch.resize(size); } for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) { - if (m_uniformLocations.find(name) != m_uniformLocations.end()) { - m_uniformOffsets[m_uniformLocations[name]] = offset; - MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u assigned to location %u", - m_externalIndex, name.c_str(), offset, m_uniformLocations[name]); - } else { - MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in " - "m_uniformLocations", - m_externalIndex, name.c_str(), offset); - } - } - for (const auto& [name, size] : meta.plainUniformMemberSizesInBytes) { - if (m_uniformLocations.find(name) != m_uniformLocations.end()) { - m_uniformSizesInBytes[m_uniformLocations[name]] = size; - MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u assigned to location %u", - m_externalIndex, name.c_str(), size, m_uniformLocations[name]); - } else { - MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u but not found in " - "m_uniformLocations", - m_externalIndex, name.c_str(), size); - } + const auto locationIt = m_uniformLocations.find(name); + if (locationIt == m_uniformLocations.end()) { + MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in " + "m_uniformLocations", + m_externalIndex, name.c_str(), offset); + continue; + } + const Uint baseLocation = locationIt->second; + if (!IsValidUniformLocation(static_cast(baseLocation))) { + continue; + } + + const Int uniformIndex = m_uniformIndexInTProgram[baseLocation]; + const GLint arraySize = GetActiveUniformArraySize(uniformIndex); + SizeT memberSize = 0; + const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name); + if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) { + memberSize = sizeIt->second; + } + Uint arrayStride = 0; + const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name); + if (strideIt != meta.plainUniformArrayStridesInUBO.end()) { + arrayStride = strideIt->second; + } + + // Array uniforms span one location per element (see DoReflection); + // give each element its real byte offset inside the UBO. + const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1); + for (GLint element = 0; element < elementCount; ++element) { + const Uint location = baseLocation + static_cast(element); + if (location > m_maxUniformLocation || m_uniformIndexInTProgram[location] != uniformIndex) { + break; + } + m_uniformOffsets[location] = offset + static_cast(element) * arrayStride; + const SizeT consumed = static_cast(element) * arrayStride; + m_uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0; + } + MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u stride=%u size=%zu assigned " + "to locations %u..%u", + m_externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation, + baseLocation + static_cast(elementCount) - 1); } MGLOG_D("ProgramObject %u: GenerateBinary - finished parsing module %zu metadata", m_externalIndex, i); } } + + // Fallback pass: a linked program's active non-opaque uniforms must accept + // glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains + // them (AggressiveDCE can remove a dead loop together with the only loads of a + // uniform -- or the entire global UBO, leaving the scratch unallocated). Hand + // such locations CPU-side storage at the (16-byte aligned) tail of the shadow + // buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU + // never reads these bytes, so this only keeps the GL-visible state coherent. + for (Uint location = 0; location <= m_maxUniformLocation; ++location) { + if (m_uniformOffsets[location] != kInvalidUniformOffset) continue; + if (!IsValidUniformLocation(static_cast(location))) continue; + const auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]); + const glslang::TType* type = uniform.getType(); + if (type != nullptr && type->isOpaque()) continue; + if (uniform.index >= 0 && uniform.index < m_program->getNumUniformBlocks() && + std::strstr(m_program->getUniformBlock(uniform.index).name.c_str(), + MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) { + // Member of a named uniform block: not settable through glUniform*, so it + // needs no global-UBO shadow storage. + continue; + } + + // std140-style slot: the matrix upload paths write column vectors at + // 16-byte strides, so a matrix slot must cover cols * 16 bytes. + SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType); + if (type != nullptr && type->isMatrix()) { + slotSize = static_cast(type->getMatrixCols()) * 16u; + } + slotSize = (slotSize + 15u) & ~static_cast(15u); + const SizeT slotOffset = (m_globalUboScratch.size() + 15u) & ~static_cast(15u); + m_globalUboScratch.resize(slotOffset + slotSize, 0); + m_uniformOffsets[location] = static_cast(slotOffset); + m_uniformSizesInBytes[location] = slotSize; + MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' location %u has no UBO backing in the " + "generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu", + m_externalIndex, uniform.name.c_str(), location, slotSize, slotOffset); + } } void ProgramObject::WaitUntilGenerationCompleted() const { diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 3687e05d..715f56a4 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -43,8 +43,40 @@ namespace MobileGL::MG_State::GLState { Uint GetMaxUniformLocation() const { return m_maxUniformLocation; } Int GetUniformLocation(const String& name) const { const auto it = m_uniformLocations.find(name); - if (it == m_uniformLocations.end()) return -1; - return (Int)it->second; + if (it != m_uniformLocations.end()) return (Int)it->second; + + // "arr[k]" resolves to the location of element k: glslang reflection stores + // arrays under their base name (no "[0]" suffix), and DoReflection reserves + // one location per array element, so element k lives at base + k. + if (name.length() < 4 || name.back() != ']') return -1; + const SizeT bracket = name.rfind('['); + // Require at least one digit between the brackets. + if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1; + Uint element = 0; + for (SizeT i = bracket + 1; i < name.length() - 1; ++i) { + if (name[i] < '0' || name[i] > '9') return -1; + element = element * 10 + static_cast(name[i] - '0'); + if (element > 0x0FFFFFFFu) return -1; + } + const auto baseIt = m_uniformLocations.find(name.substr(0, bracket)); + if (baseIt == m_uniformLocations.end()) return -1; + const Int base = (Int)baseIt->second; + if (!IsValidUniformLocation(base)) return -1; + const Int index = m_uniformIndexInTProgram[base]; + // "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only + // in-range elements. + const glslang::TType* type = m_program->getUniform(index).getType(); + if (type == nullptr || !type->isArray()) return -1; + if (static_cast(element) >= GetActiveUniformArraySize(index)) return -1; + const Int location = base + (Int)element; + if (!UniformLocationsAliasSameUniform(base, location)) return -1; + return location; + } + + // True when both locations are element slots of the same uniform variable. + Bool UniformLocationsAliasSameUniform(Int a, Int b) const { + if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false; + return m_uniformIndexInTProgram[a] == m_uniformIndexInTProgram[b]; } Int GetActiveUniformIndex(const String& name) const { @@ -170,6 +202,9 @@ namespace MobileGL::MG_State::GLState { auto& uniform = m_program->getUniform(static_cast(index)); return uniform.name; } + // Sentinel for a uniform location without global-UBO backing storage (should not + // survive linking: GenerateBinary falls back to tail-allocated scratch storage). + static constexpr Uint kInvalidUniformOffset = ~0u; Uint GetUniformOffset(Uint location) const { return m_uniformOffsets[location]; } Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); } diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index acad1095..015445a1 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -1845,3 +1845,211 @@ TEST_F(ProgramTest, GetActiveUniformsivErrors) { EXPECT_EQ(GetError(), GL_NO_ERROR); EXPECT_EQ(params[0], -999); } + +namespace { + GLuint LinkVsFsProgram(const char* vsSource, const char* fsSource) { + char infoLog[4096] = ""; + GLuint vs = CreateShader(GL_VERTEX_SHADER); + ShaderSource(vs, 1, &vsSource, nullptr); + CompileShader(vs); + GLint vsStatus = GL_FALSE; + GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus); + GetShaderInfoLog(vs, sizeof(infoLog), nullptr, infoLog); + EXPECT_EQ(vsStatus, GL_TRUE) << infoLog; + + GLuint fs = CreateShader(GL_FRAGMENT_SHADER); + ShaderSource(fs, 1, &fsSource, nullptr); + CompileShader(fs); + GLint fsStatus = GL_FALSE; + GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus); + GetShaderInfoLog(fs, sizeof(infoLog), nullptr, infoLog); + EXPECT_EQ(fsStatus, GL_TRUE) << infoLog; + + GLuint program = CreateProgram(); + AttachShader(program, vs); + AttachShader(program, fs); + LinkProgram(program); + GLint linkStatus = GL_FALSE; + GetProgramiv(program, GL_LINK_STATUS, &linkStatus); + GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog); + EXPECT_EQ(linkStatus, GL_TRUE) << infoLog; + return program; + } + + const char* kPassthroughCoordsVs = R"(#version 330 +in vec4 a_position; +in vec4 a_coords; +out vec4 coords_in; +void main() { + gl_Position = a_position; + coords_in = a_coords; +})"; +} // namespace + +// Repro for KHR-GL33.shaders.loops.do_while_dynamic_iterations.empty_body_* (and the +// only_continue / unconditional_break variants): the loop is dead code, so the SPIR-V +// optimizer eliminates it together with the only loads of `one` / `ui_one` -- and with +// them the entire global UBO. The uniforms stay active in link reflection, so +// glUniform1i on them must still have backing storage instead of memcpy-ing to null. +TEST_F(ProgramTest, DoWhileDeadLoopUniformsKeepBackingStorage) { + const char* loopBodies[] = {"", "continue;", "break;"}; + for (const char* body : loopBodies) { + const String fsSource = String(R"(#version 330 +uniform int ui_one; +uniform mediump int one; +in vec4 coords_in; +out vec4 o_color; +void main() { + vec4 res = coords_in; + mediump int i = 0; + do {)") + body + R"(} while (i++ < one*ui_one); + o_color = res; +})"; + GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource.c_str()); + + const GLint locOne = GetUniformLocation(program, "one"); + const GLint locUiOne = GetUniformLocation(program, "ui_one"); + ASSERT_GE(locOne, 0) << "body: '" << body << "'"; + ASSERT_GE(locUiOne, 0) << "body: '" << body << "'"; + + UseProgram(program); + Uniform1i(locOne, 1); // crashed with a null MapUBO() before the fallback storage + Uniform1i(locUiOne, 2); + EXPECT_EQ(GetError(), GL_NO_ERROR) << "body: '" << body << "'"; + + GLint readback = -1; + GetUniformiv(program, locOne, &readback); + EXPECT_EQ(readback, 1) << "body: '" << body << "'"; + readback = -1; + GetUniformiv(program, locUiOne, &readback); + EXPECT_EQ(readback, 2) << "body: '" << body << "'"; + EXPECT_EQ(GetError(), GL_NO_ERROR) << "body: '" << body << "'"; + } +} + +// Repro for KHR-GL33.shaders.struct.uniform.*nested_struct_array_*: leaf uniforms of +// nested struct arrays need (a) one location per array element and (b) real byte +// offsets inside the global UBO. Before the fix every leaf had a single location and +// offset 0, so glUniform2fv(loc, 2, ...) tripped the size assert on the neighboring +// float uniform (and corrupted it in release builds). +TEST_F(ProgramTest, NestedStructArrayUniformElementWrites) { + // Struct shape from CTS glcShaderStructTests nested_struct_array (uniform case). + const char* fsSource = R"(#version 330 +struct T { + mediump float a; + mediump vec2 b[2]; +}; +struct S { + mediump float a; + T b[3]; + int c; +}; +uniform S s[2]; +in vec4 coords_in; +out vec4 o_color; +void main() { + mediump float r = (s[0].b[1].b[0].x + s[1].b[2].b[1].y) * s[0].b[0].a; + mediump float g = s[1].b[0].b[0].y * s[0].b[2].a * s[1].b[2].a; + mediump float b = (s[0].b[2].b[1].y + s[0].b[1].b[0].y + s[1].a) * s[0].b[1].a; + mediump float a = float(s[0].c) + s[1].b[2].a - s[1].b[1].a; + o_color = vec4(r, g, b, a); +})"; + GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource); + UseProgram(program); + + const GLint locVecArray = GetUniformLocation(program, "s[0].b[1].b"); + ASSERT_GE(locVecArray, 0); + // Element locations are consecutive and reachable via the "[k]" suffix. + EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[0]"), locVecArray); + EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[1]"), locVecArray + 1); + EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[2]"), -1); + + // Distinct scalar leaves must land at distinct UBO offsets (they all aliased + // offset 0 before the fix). + const char* scalarLeaves[] = {"s[0].b[0].a", "s[0].b[1].a", "s[0].b[2].a", "s[1].a", "s[1].b[1].a", + "s[1].b[2].a"}; + const GLfloat scalarValues[] = {0.5f, 0.25f, 0.125f, 7.0f, 3.0f, 4.0f}; + for (SizeT i = 0; i < std::size(scalarLeaves); ++i) { + const GLint loc = GetUniformLocation(program, scalarLeaves[i]); + ASSERT_GE(loc, 0) << scalarLeaves[i]; + Uniform1f(loc, scalarValues[i]); + } + + // CTS-style whole-array write: glUniform2fv with count = 2 on a vec2[2] leaf. + // Before the fix this asserted/corrupted the next uniform ("s[0].b[2].a"). + const GLfloat vecData[4] = {1.0f, 2.0f, 3.0f, 4.0f}; + Uniform2fv(locVecArray, 2, vecData); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + GLfloat vecReadback[2] = {}; + GetUniformfv(program, locVecArray, vecReadback); + EXPECT_EQ(vecReadback[0], 1.0f); + EXPECT_EQ(vecReadback[1], 2.0f); + GetUniformfv(program, locVecArray + 1, vecReadback); + EXPECT_EQ(vecReadback[0], 3.0f); + EXPECT_EQ(vecReadback[1], 4.0f); + + // All scalar leaves survived the array write intact. + for (SizeT i = 0; i < std::size(scalarLeaves); ++i) { + GLfloat readback = -1.0f; + GetUniformfv(program, GetUniformLocation(program, scalarLeaves[i]), &readback); + EXPECT_EQ(readback, scalarValues[i]) << scalarLeaves[i]; + } + + // std140: vec2 array elements inside the struct are 16 bytes apart, and the + // per-element offsets differ. + auto programObject = MG_State::pGLContext->GetProgramObject(program); + ASSERT_NE(programObject, nullptr); + const Uint offsetElement0 = programObject->GetUniformOffset(static_cast(locVecArray)); + const Uint offsetElement1 = programObject->GetUniformOffset(static_cast(locVecArray + 1)); + EXPECT_EQ(offsetElement1, offsetElement0 + 16u); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +// Plain top-level uniform arrays share the same per-element location machinery. +TEST_F(ProgramTest, PlainArrayUniformElementLocationsAndWrites) { + const char* fsSource = R"(#version 330 +uniform float arr[4]; +uniform float guard; +in vec4 coords_in; +out vec4 o_color; +void main() { + o_color = vec4(arr[0] + arr[1], arr[2] + arr[3], guard, 1.0); +})"; + GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource); + UseProgram(program); + + const GLint locArr = GetUniformLocation(program, "arr"); + ASSERT_GE(locArr, 0); + EXPECT_EQ(GetUniformLocation(program, "arr[0]"), locArr); + EXPECT_EQ(GetUniformLocation(program, "arr[2]"), locArr + 2); + EXPECT_EQ(GetUniformLocation(program, "arr[4]"), -1); + + const GLint locGuard = GetUniformLocation(program, "guard"); + ASSERT_GE(locGuard, 0); + EXPECT_EQ(GetUniformLocation(program, "guard[0]"), -1); // not an array + + Uniform1f(locGuard, 9.0f); + + const GLfloat values[4] = {1.0f, 2.0f, 3.0f, 4.0f}; + Uniform1fv(locArr, 4, values); + for (int i = 0; i < 4; ++i) { + GLfloat readback = -1.0f; + GetUniformfv(program, locArr + i, &readback); + EXPECT_EQ(readback, values[i]) << "arr[" << i << "]"; + } + + // Overlong writes stop at the end of the array (GL 3.3 §2.11.4) instead of + // spilling into the next uniform. + const GLfloat tail[3] = {30.0f, 40.0f, 50.0f}; + Uniform1fv(GetUniformLocation(program, "arr[2]"), 3, tail); + EXPECT_EQ(GetError(), GL_NO_ERROR); + GLfloat readback = -1.0f; + GetUniformfv(program, locArr + 2, &readback); + EXPECT_EQ(readback, 30.0f); + GetUniformfv(program, locArr + 3, &readback); + EXPECT_EQ(readback, 40.0f); + GetUniformfv(program, locGuard, &readback); + EXPECT_EQ(readback, 9.0f); // untouched by the overlong write + EXPECT_EQ(GetError(), GL_NO_ERROR); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp index 7c31ecb0..32e3d831 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp @@ -48,6 +48,69 @@ namespace MobileGL { return SPVC_BASETYPE_UNKNOWN; } + // Record one flattened leaf uniform of the global UBO into the metadata maps. + static void RecordGlobalUboLeaf(const SpvReflectBlockVariable& member, const String& name, + Uint32 offsetInUBO, SpvcMetadata& metadata) { + metadata.plainUniformOffsetsInUBO[name] = offsetInUBO; + metadata.plainUniformMemberSizesInBytes[name] = member.size; + metadata.plainUniformArrayStridesInUBO[name] = + member.array.dims_count > 0 ? member.array.stride : 0; + + Uint32 vectorSize = member.numeric.vector.component_count; + if (vectorSize == 0) vectorSize = 1; + Uint32 matCol = member.numeric.matrix.column_count; + if (matCol == 0) matCol = 1; + metadata.plainUniformMemberTypes[name] = { + .basetype = MapReflectToSpvcBasetype(member), + .vectorSize = vectorSize, + .matCol = matCol, + }; + } + + // Flatten a (possibly nested struct / struct array) member of the global UBO + // into leaf entries named the way glslang reflection names plain uniforms: + // "s[0].b[1].b" for `uniform S s[2]` with `struct T { vec2 b[2]; }` members. + // glUniform* writes are routed per leaf location, so the state layer needs a + // byte offset for every leaf, not just for the top-level block members. + // `baseOffset` accumulates parent offsets; member.offset is relative to the + // enclosing struct (top-level members: relative to the block start). + static void FlattenGlobalUboMember(const SpvReflectBlockVariable& member, const String& prefix, + Uint32 baseOffset, SpvcMetadata& metadata) { + const String name = prefix + (member.name != nullptr ? member.name : ""); + const Uint32 selfOffset = baseOffset + member.offset; + + if (member.member_count == 0 || member.members == nullptr) { + RecordGlobalUboLeaf(member, name, selfOffset, metadata); + return; + } + + if (member.array.dims_count == 0) { + // Plain nested struct. + for (Uint32 j = 0; j < member.member_count; ++j) { + FlattenGlobalUboMember(member.members[j], name + ".", selfOffset, metadata); + } + return; + } + + if (member.array.dims_count > 1) { + // Arrays of arrays of structs cannot be declared in the GL 3.3-era GLSL + // MobileGL ingests; record the base so at least element 0 resolves. + MGLOG_W("FlattenGlobalUboMember: multi-dimensional struct array '%s' is not supported, " + "flattening element 0 only", + name.c_str()); + } + + const Uint32 elementCount = member.array.dims[0] > 0 ? member.array.dims[0] : 1; + const Uint32 elementStride = member.array.stride; + for (Uint32 element = 0; element < elementCount; ++element) { + const String elementPrefix = name + "[" + std::to_string(element) + "]."; + const Uint32 elementOffset = selfOffset + element * elementStride; + for (Uint32 j = 0; j < member.member_count; ++j) { + FlattenGlobalUboMember(member.members[j], elementPrefix, elementOffset, metadata); + } + } + } + SpvcSession::SpvcSession(const Vector& spirv, Flags usage) : usage(usage) { if (usage & SessionUsageBit::Transpile) { @@ -299,20 +362,10 @@ namespace MobileGL { metadata.globalUboSize = block.size; for (uint32_t j = 0; j < block.member_count; ++j) { - auto& member = block.members[j]; - metadata.plainUniformOffsetsInUBO[member.name] = member.offset; - metadata.plainUniformMemberSizesInBytes[member.name] = member.size; - - Uint32 vectorSize = member.numeric.vector.component_count; - if (vectorSize == 0) vectorSize = 1; - Uint32 matCol = member.numeric.matrix.column_count; - if (matCol == 0) matCol = 1; - - metadata.plainUniformMemberTypes[member.name] = { - .basetype = MapReflectToSpvcBasetype(member), - .vectorSize = vectorSize, - .matCol = matCol, - }; + // Recurse into nested structs / struct arrays so every leaf + // uniform ("s[0].b[1].b") gets its real byte offset; top-level + // scalars/vectors/matrices flatten to themselves. + FlattenGlobalUboMember(block.members[j], "", 0, metadata); } return SPVC_SUCCESS; } diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h index bd7de090..f5296b2e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h @@ -65,6 +65,11 @@ namespace MobileGL { UnorderedMap plainUniformOffsetsInUBO; UnorderedMap plainUniformMemberSizesInBytes; UnorderedMap plainUniformMemberTypes; + // Byte stride between consecutive array elements of an arrayed plain + // uniform (0 for non-arrays). Keyed like the offset map: names are the + // flattened leaf names glslang reflection uses ("s[0].b[1].b"), without + // a trailing "[0]". + UnorderedMap plainUniformArrayStridesInUBO; SizeT globalUboSize = 0; }; From f0ed5c1b8e03030d5156a14c45c65116fc1f49f4 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:40:54 -0400 Subject: [PATCH 13/44] [Fix] (MG_Backend/DirectGLES): convert narrow client formats (RED/RG/RGB/BGR/BGRA + integer variants, byte/short/half/8888 types) in GetTexImage/ReadPixels via wide RGBA readback --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 549 +++++++++++++++++- 1 file changed, 520 insertions(+), 29 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 4246f071..0fb5b3bd 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -3262,22 +3263,487 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } + // ---- Client-format readback conversion --------------------------------------------------------------------- + // ES 3.x glReadPixels only guarantees GL_RGBA/GL_UNSIGNED_BYTE, GL_RGBA_INTEGER/GL_(UNSIGNED_)INT, + // GL_RGBA/GL_FLOAT for float buffers plus one implementation-defined pair, while desktop GL clients read + // back narrower layouts (RED, RG, RGB, BGR, byte-order packed types, ...). For those we read a guaranteed + // wide RGBA format into scratch memory and repack into the caller's (format, type) layout on the CPU, + // honoring the client-side PACK pixel-store parameters. + + static Float DecodeHalfBitsToFloat(Uint16 half) { + const Uint32 sign = static_cast(half & 0x8000u) << 16; + const Uint32 exponent = (half >> 10) & 0x1Fu; + const Uint32 mantissa = half & 0x3FFu; + Uint32 bits; + if (exponent == 0) { + if (mantissa == 0) { + bits = sign; // signed zero + } else { + // Subnormal half: renormalize into a float exponent. + Uint32 e = 127 - 15 + 1; + Uint32 m = mantissa; + while ((m & 0x400u) == 0) { + m <<= 1; + --e; + } + bits = sign | (e << 23) | ((m & 0x3FFu) << 13); + } + } else if (exponent == 31) { + bits = sign | 0x7F800000u | (mantissa << 13); // Inf / NaN + } else { + bits = sign | ((exponent + 112) << 23) | (mantissa << 13); + } + return std::bit_cast(bits); + } + + static Uint16 EncodeFloatToHalfBits(Float value) { + const Uint32 bits = std::bit_cast(value); + const auto sign = static_cast((bits >> 16) & 0x8000u); + const Uint32 exponent = (bits >> 23) & 0xFFu; + const Uint32 mantissa = bits & 0x7FFFFFu; + if (exponent == 0xFF) { // Inf / NaN + return static_cast(sign | 0x7C00u | (mantissa != 0 ? 0x200u : 0u)); + } + const Int32 halfExponent = static_cast(exponent) - 127 + 15; + if (halfExponent >= 31) { + return static_cast(sign | 0x7C00u); // overflow -> Inf + } + if (halfExponent <= 0) { + if (halfExponent < -10) { + return sign; // underflow -> signed zero + } + const Uint32 m = mantissa | 0x800000u; + const Uint32 shift = static_cast(14 - halfExponent); + Uint32 half = m >> shift; + if ((m >> (shift - 1)) & 1u) { + ++half; // round to nearest + } + return static_cast(sign | half); + } + Uint32 half = (static_cast(halfExponent) << 10) | (mantissa >> 13); + if (mantissa & 0x1000u) { + ++half; // round to nearest; a carry into the exponent is the correct result + } + return static_cast(sign | half); + } + + struct ReadbackChannelMapping { + Int sourceChannel[4]; // RGBA source channel feeding each destination channel + Int channelCount; // destination channel count + Bool isInteger; + }; + + static Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) { + switch (format) { + case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true; + case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true; + case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true; + case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true; + case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true; + case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true; + case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true; + case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true; + case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true; + case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true; + case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true; + case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true; + default: + return false; + } + } + + static Bool IsPackedReadback8888Type(GLenum type) { + return type == GL_UNSIGNED_INT_8_8_8_8 || type == GL_UNSIGNED_INT_8_8_8_8_REV; + } + + static SizeT GetReadbackComponentSize(GLenum type) { + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: + return 1; + case GL_UNSIGNED_SHORT: + case GL_SHORT: + case GL_HALF_FLOAT: + return 2; + case GL_UNSIGNED_INT: + case GL_INT: + case GL_FLOAT: + case GL_UNSIGNED_INT_8_8_8_8: + case GL_UNSIGNED_INT_8_8_8_8_REV: + return 4; + default: + return 0; + } + } + + static Bool CanDecodeWideSourceType(GLenum type) { + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: + case GL_UNSIGNED_SHORT: + case GL_SHORT: + case GL_HALF_FLOAT: + case GL_FLOAT: + return true; + default: + return false; + } + } + + static void DrainESErrors() { + for (Int i = 0; i < 32 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { + } + } + + static GLenum QueryReadAttachmentComponentType() { + GLint framebufferId = 0; + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &framebufferId); + if (framebufferId == 0) { + return GL_UNSIGNED_NORMALIZED; // default framebuffers are normalized fixed-point + } + GLint readBuffer = GL_COLOR_ATTACHMENT0; + g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); + if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) { + readBuffer = GL_COLOR_ATTACHMENT0; + } + GLint componentType = 0; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast(readBuffer), + GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType); + DrainESErrors(); + 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; + } + const Bool packed8888 = IsPackedReadback8888Type(type); + if (packed8888 && mapping.channelCount != 4) { + return false; + } + if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) { + return false; + } + const SizeT dstComponentSize = GetReadbackComponentSize(type); + if (dstComponentSize == 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), 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. + const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); + const SizeT dstPixelBytes = + packed8888 ? sizeof(Uint32) : static_cast(mapping.channelCount) * dstComponentSize; + 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 + + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; + const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; + + const SizeT pboBaseOffset = reinterpret_cast(pixels); // with a PBO, `pixels` is an offset + if (pixelPackBufferObject) { + const SizeT requiredSize = + pboBaseOffset + dstSkipOffset + static_cast(height - 1) * dstRowStride + dstRowBytes; + if (requiredSize > pixelPackBufferObject->GetSize()) { + MGLOG_E("Readback conversion: pixel pack buffer is too small"); + return true; + } + } + + const SizeT srcComponentSize = GetReadbackComponentSize(wideType); + const SizeT srcPixelBytes = 4 * srcComponentSize; + Vector convertedRow(dstRowBytes); + + for (GLsizei row = 0; row < height; ++row) { + const Uint8* srcRow = wide.data() + static_cast(row) * static_cast(width) * srcPixelBytes; + for (GLsizei col = 0; col < width; ++col) { + const Uint8* srcPixel = srcRow + static_cast(col) * srcPixelBytes; + Uint8* dstPixel = convertedRow.data() + static_cast(col) * dstPixelBytes; + if (mapping.isInteger) { + Int64 src[4]; + for (Int c = 0; c < 4; ++c) { + src[c] = wideType == GL_INT + ? static_cast(reinterpret_cast(srcPixel)[c]) + : static_cast(reinterpret_cast(srcPixel)[c]); + } + if (packed8888) { + Uint32 word = 0; + for (Int ch = 0; ch < 4; ++ch) { + const auto v = + static_cast(std::clamp(src[mapping.sourceChannel[ch]], 0, 255)); + word |= type == GL_UNSIGNED_INT_8_8_8_8 ? v << (24 - ch * 8) : v << (ch * 8); + } + Memcpy(dstPixel, &word, sizeof(word)); + } else { + for (Int ch = 0; ch < mapping.channelCount; ++ch) { + const Int64 v = src[mapping.sourceChannel[ch]]; + Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; + switch (type) { + case GL_UNSIGNED_BYTE: + *dstComponent = static_cast(std::clamp(v, 0, 255)); + break; + case GL_BYTE: { + const auto out = static_cast(std::clamp(v, -128, 127)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_SHORT: { + const auto out = static_cast(std::clamp(v, 0, 65535)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_SHORT: { + const auto out = static_cast(std::clamp(v, -32768, 32767)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_INT: { + const auto out = static_cast(std::clamp(v, 0, 4294967295LL)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_INT: { + const auto out = + static_cast(std::clamp(v, -2147483648LL, 2147483647LL)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + default: + break; + } + } + } + } else { + Float src[4]; + switch (wideType) { + case GL_UNSIGNED_BYTE: + for (Int c = 0; c < 4; ++c) { + src[c] = static_cast(srcPixel[c]) / 255.0f; + } + break; + case GL_BYTE: + for (Int c = 0; c < 4; ++c) { + src[c] = std::max( + static_cast(reinterpret_cast(srcPixel)[c]) / 127.0f, -1.0f); + } + break; + case GL_UNSIGNED_SHORT: + for (Int c = 0; c < 4; ++c) { + src[c] = static_cast(reinterpret_cast(srcPixel)[c]) / 65535.0f; + } + break; + case GL_SHORT: + for (Int c = 0; c < 4; ++c) { + src[c] = std::max( + static_cast(reinterpret_cast(srcPixel)[c]) / 32767.0f, -1.0f); + } + break; + case GL_HALF_FLOAT: + for (Int c = 0; c < 4; ++c) { + src[c] = DecodeHalfBitsToFloat(reinterpret_cast(srcPixel)[c]); + } + break; + default: // GL_FLOAT + for (Int c = 0; c < 4; ++c) { + src[c] = reinterpret_cast(srcPixel)[c]; + } + break; + } + if (packed8888) { + Uint32 word = 0; + for (Int ch = 0; ch < 4; ++ch) { + const auto v = static_cast( + std::llround(std::clamp(src[mapping.sourceChannel[ch]], 0.0f, 1.0f) * 255.0)); + word |= type == GL_UNSIGNED_INT_8_8_8_8 ? v << (24 - ch * 8) : v << (ch * 8); + } + Memcpy(dstPixel, &word, sizeof(word)); + } else { + for (Int ch = 0; ch < mapping.channelCount; ++ch) { + const Float v = src[mapping.sourceChannel[ch]]; + Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; + switch (type) { + case GL_UNSIGNED_BYTE: + *dstComponent = + static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0)); + break; + case GL_BYTE: { + const auto out = + static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_SHORT: { + const auto out = + static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_SHORT: { + const auto out = + static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_INT: { + const auto out = static_cast( + std::llround(static_cast(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_INT: { + const auto out = static_cast( + std::llround(static_cast(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_FLOAT: + Memcpy(dstComponent, &v, sizeof(v)); + break; + case GL_HALF_FLOAT: { + const Uint16 out = EncodeFloatToHalfBits(v); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + default: + break; + } + } + } + } + } + + if (packParams.SwapBytes) { + const SizeT groupSize = packed8888 ? sizeof(Uint32) : dstComponentSize; + if (groupSize > 1) { + for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) { + std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize); + } + } + } + + const SizeT dstOffset = dstSkipOffset + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes}, + pboBaseOffset + dstOffset); + } else { + Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); + } + } + + 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(wideType).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; + } + + static Bool IsLegacyNativeReadPixelsType(GLenum type) { + return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV || + type == GL_INT || type == GL_FLOAT; + } + void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { MGLOG_D("ReadPixels: x=%d y=%d w=%d h=%d format=%s type=%s pixels=%p", x, y, width, height, MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), pixels); - // Unimplemented readback formats degrade to a logged no-op instead of killing the process; - // spec-invalid combinations are already rejected with GL errors at the state layer. - if (format != GL_RGBA && format != GL_RGBA_INTEGER && format != GL_RED && format != GL_RED_INTEGER && - format != GL_DEPTH_COMPONENT && format != GL_STENCIL_INDEX) { - MGLOG_E("ReadPixels: format %s is not implemented yet, skipping readback", - MG_Util::ConvertGLEnumToString(format).c_str()); - return; - } - if (type != GL_UNSIGNED_BYTE && type != GL_UNSIGNED_INT && type != GL_UNSIGNED_INT_2_10_10_10_REV && - type != GL_INT && type != GL_FLOAT) { - MGLOG_E("ReadPixels: type %s is not implemented yet, skipping readback", - MG_Util::ConvertGLEnumToString(type).c_str()); + // Combinations the ES driver has always handled directly keep the native path; other color layouts go + // through the wide-format conversion path. Anything still uncovered degrades to a logged no-op instead + // of killing the process; spec-invalid combinations are already rejected with GL errors at the state layer. + const Bool useNativeReadback = IsLegacyNativeReadPixelsFormat(format) && IsLegacyNativeReadPixelsType(type); + ReadbackChannelMapping conversionMapping{}; + const Bool convertible = + GetReadbackChannelMapping(format, conversionMapping) && GetReadbackComponentSize(type) != 0; + if (!useNativeReadback && !convertible) { + MGLOG_E("ReadPixels: format %s with type %s is not implemented yet, skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); return; } @@ -3300,6 +3766,15 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("ReadPixels: bound READ FBO is not complete"); return; } + if (!useNativeReadback) { + if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels)) { + MGLOG_D("ReadPixels: finished via client-format conversion"); + return; + } + MGLOG_E("ReadPixels: 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 (format == GL_DEPTH_COMPONENT && type == GL_FLOAT && ReadPixelsDepthFloatViaUnsignedInt(x, y, width, height, pixels)) { MGLOG_D("ReadPixels: finished via depth GL_FLOAT fallback"); @@ -3359,6 +3834,20 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ReadPixels: finished"); } + // Combinations the ES driver has always handled directly for GetTexImage; everything else that maps + // to a color channel layout is repacked via ReadPixelsViaFormatConversion. + static Bool IsNativeGetTexImagePair(GLenum format, GLenum type) { + if (format == GL_RGBA) { + return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV || + type == GL_INT || type == GL_FLOAT || type == GL_HALF_FLOAT || + type == GL_UNSIGNED_INT_8_8_8_8_REV; + } + if (format == GL_RGBA_INTEGER) { + return type == GL_INT || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV; + } + return false; + } + void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, void* pixels) { DebugImpl::ErrorLopper errorLopper; MGLOG_D("GetTexImage: target=%s level=%d format=%s type=%s pixels=%p", @@ -3367,22 +3856,19 @@ namespace MobileGL::MG_Backend::DirectGLES { // Unimplemented readback formats degrade to a logged no-op instead of killing the process; // spec-invalid combinations are already rejected with GL errors at the state layer. - if (format != GL_RGBA && format != GL_RGBA_INTEGER && format != GL_BGRA) { - MGLOG_E("GetTexImage: format %s is not implemented yet, skipping readback", - MG_Util::ConvertGLEnumToString(format).c_str()); - return; - } - if (type != GL_UNSIGNED_BYTE && type != GL_UNSIGNED_INT && type != GL_UNSIGNED_INT_2_10_10_10_REV && - type != GL_INT && type != GL_FLOAT && type != GL_UNSIGNED_INT_8_8_8_8 && - type != GL_UNSIGNED_INT_8_8_8_8_REV && type != GL_HALF_FLOAT) { - MGLOG_E("GetTexImage: type %s is not implemented yet, skipping readback", - MG_Util::ConvertGLEnumToString(type).c_str()); + const Bool useNativeReadback = IsNativeGetTexImagePair(format, type); + ReadbackChannelMapping conversionMapping{}; + const Bool convertible = + GetReadbackChannelMapping(format, conversionMapping) && GetReadbackComponentSize(type) != 0; + if (!useNativeReadback && !convertible) { + 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; } GLenum esFormat = format, esType = type; - if (esFormat == GL_BGRA) esFormat = GL_RGBA; - if (esType == GL_UNSIGNED_INT_8_8_8_8 || esType == GL_UNSIGNED_INT_8_8_8_8_REV) esType = GL_UNSIGNED_BYTE; + // On little-endian hosts UNSIGNED_INT_8_8_8_8_REV has the same memory layout as UNSIGNED_BYTE. + if (esType == GL_UNSIGNED_INT_8_8_8_8_REV) esType = GL_UNSIGNED_BYTE; MGLOG_D("GetTexImage: SyncNeccessaryTextures()"); TextureImpl::SyncNeccessaryTextures(); @@ -3464,6 +3950,16 @@ 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)) { + MGLOG_D("GetTexImage: finished via client-format conversion"); + 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; + } + // Handle PBO auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); @@ -3513,11 +4009,6 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ReadPixels: Restoring previous pixel pack buffer binding %u", prevPixelPackBuffer); g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, prevPixelPackBuffer); - } else { - if (esFormat == GL_RGBA && format == GL_BGRA && esType == GL_UNSIGNED_BYTE && - type == GL_UNSIGNED_INT_8_8_8_8_REV) { - MGLOG_D("ReadPixels: ProcessColorSwizzle BGRA (not implemented)"); - } } DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { From b9844ed7c10e1474c9785f6bfabce3396a261d1b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:35:50 -0400 Subject: [PATCH 14/44] [Fix] (MG_Util/Texture): infer RGBA8 for packed RGBA uploads (cherry picked from commit 1146188ed4852217c392639826262d7c27db5b70) --- MobileGL/MG_Test/Texture/TextureTest.cpp | 51 +++++++++++++++++++ .../MGToMG/TextureEnumConverter.cpp | 2 + 2 files changed, 53 insertions(+) diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 462e30c9..d22a09f2 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include using namespace MobileGL; @@ -368,6 +369,56 @@ TEST_F(TextureTest, BoundTexSubImage2DUnpacksPackedBgra8888RevToRgba8) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(TextureTest, UnsizedRgbaInfersRgba8ForPacked8888Types) { + EXPECT_EQ(MG_Util::ConvertInternalFormatToSized(TextureInternalFormat::RGBA, TextureInputFormat::BGRA, + TexturePixelDataType::UnsignedInt8888), + TextureInternalFormat::RGBA8); + EXPECT_EQ(MG_Util::ConvertInternalFormatToSized(TextureInternalFormat::RGBA, TextureInputFormat::BGRA, + TexturePixelDataType::UnsignedInt8888Rev), + TextureInternalFormat::RGBA8); +} + +TEST_F(TextureTest, BoundTexImageAndSubImage2DUseInferredRgba8ForPackedBgra8888Rev) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 initialPixels[] = { + 10, 20, 30, 40, + 50, 60, 70, 80, + }; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 1, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, + initialPixels); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::RGBA8); + const auto* stored = GetBoundTexture2DLevelBytes(texture); + const Uint8 expectedInitial[] = { + 30, 20, 10, 40, + 70, 60, 50, 80, + }; + for (SizeT i = 0; i < sizeof(expectedInitial); ++i) { + EXPECT_EQ(stored[i], expectedInitial[i]) << "initial byte " << i; + } + + const Uint8 updatedPixels[] = { + 90, 100, 110, 120, + 130, 140, 150, 160, + }; + MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, + updatedPixels); + + stored = GetBoundTexture2DLevelBytes(texture); + const Uint8 expectedUpdated[] = { + 110, 100, 90, 120, + 150, 140, 130, 160, + }; + for (SizeT i = 0; i < sizeof(expectedUpdated); ++i) { + EXPECT_EQ(stored[i], expectedUpdated[i]) << "updated byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, BoundTexSubImage2DUnpacksPackedRgba8888ToRgba8) { GLuint texture = 0; MG_Impl::GLImpl::GenTextures(1, &texture); diff --git a/MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp index 62b953fd..3fe452b2 100644 --- a/MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp @@ -129,6 +129,8 @@ namespace MobileGL { case TextureInternalFormat::RGBA: { switch (type) { case TexturePixelDataType::UnsignedByte: + case TexturePixelDataType::UnsignedInt8888: + case TexturePixelDataType::UnsignedInt8888Rev: return TextureInternalFormat::RGBA8; case TexturePixelDataType::UnsignedShort: return TextureInternalFormat::RGBA16; From 535e9f8b158d553e55c56f6139657528d8f62c46 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:54:33 -0400 Subject: [PATCH 15/44] [Fix] (MG_Util/Metrics): correct Depth32FStencil8 shadow texel size (16->8) and FLOAT_32_UNSIGNED_INT_24_8_REV type size (4->8) to the GL client transfer layout --- MobileGL/MG_Util/Metrics/TextureMetrics.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp index 4bf7da45..1c92191c 100644 --- a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp +++ b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp @@ -87,6 +87,9 @@ namespace MobileGL { case TextureInternalFormat::RG32F: case TextureInternalFormat::RG32I: case TextureInternalFormat::RG32UI: + // Matches the FLOAT_32_UNSIGNED_INT_24_8_REV client layout the shadow storage keeps: + // a float32 depth word followed by a word with stencil in bits 0-7. + case TextureInternalFormat::Depth32FStencil8: return 8; case TextureInternalFormat::RGB32F: @@ -97,7 +100,6 @@ namespace MobileGL { case TextureInternalFormat::RGBA32F: case TextureInternalFormat::RGBA32I: case TextureInternalFormat::RGBA32UI: - case TextureInternalFormat::Depth32FStencil8: return 16; case TextureInternalFormat::R11FG11FB10F: @@ -243,8 +245,11 @@ namespace MobileGL { case TexturePixelDataType::UnsignedInt101111Rev: case TexturePixelDataType::UnsignedInt5999Rev: case TexturePixelDataType::UnsignedInt248: - case TexturePixelDataType::Float32UnsignedInt248Rev: return 4; + // FLOAT_32_UNSIGNED_INT_24_8_REV transfers two 32-bit words per texel: + // a float32 depth word followed by a word with stencil in bits 0-7. + case TexturePixelDataType::Float32UnsignedInt248Rev: + return 8; default: return 0; } From 8b78379f6fb11b0fe88401b06e0acc5391615574 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:54:33 -0400 Subject: [PATCH 16/44] [Feat] (MG_Backend/DirectVulkan): implement combined depth-stencil texture upload via per-aspect de-interleaved staging copies --- .../Renderer/VkTextureManager.cpp | 128 +++++++++++++++--- 1 file changed, 112 insertions(+), 16 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index da38a978..bce89d7b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -383,6 +383,86 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + // Combined depth-stencil shadow storage keeps the GL client texel layout produced by the state + // layer: packed UNSIGNED_INT_24_8 words (depth in bits 8-31, stencil in bits 0-7) for + // Depth24Stencil8/DepthStencil, and FLOAT_32_UNSIGNED_INT_24_8_REV pairs (float32 depth word, + // then a word with stencil in bits 0-7) for Depth32FStencil8. VkBufferImageCopy regions must + // address exactly one aspect, so de-interleave the shadow texels into a depth region (32-bit + // words with depth in bits 0-23 for VK_FORMAT_D24_UNORM_S8_UINT, tightly packed float32 for + // VK_FORMAT_D32_SFLOAT_S8_UINT) followed by a tightly packed one-byte-per-texel stencil + // region. The blob is padded to a 4-byte multiple so subsequent staging offsets keep the + // alignment depth-stencil buffer-image copies require. + static Bool DeinterleaveDepthStencilSource(const void* source, SizeT sourceByteSize, const IntVec3& texelSize, + TextureInternalFormat internalFormat, VkFormat imageFormat, + Vector& outDeinterleavedData, SizeT& outDepthAspectByteSize) { + MOBILEGL_ASSERT(source != nullptr, "DeinterleaveDepthStencilSource: source is null"); + + const Bool packedDepth24Source = internalFormat == TextureInternalFormat::Depth24Stencil8 || + internalFormat == TextureInternalFormat::DepthStencil; + const Bool floatDepthSource = internalFormat == TextureInternalFormat::Depth32FStencil8; + if (!packedDepth24Source && !floatDepthSource) { + return false; + } + if (imageFormat != VK_FORMAT_D24_UNORM_S8_UINT && imageFormat != VK_FORMAT_D32_SFLOAT_S8_UINT) { + return false; + } + + const SizeT depth = static_cast(std::max(texelSize.z(), 1)); + const SizeT pixelCount = static_cast(texelSize.x()) * static_cast(texelSize.y()) * depth; + MOBILEGL_ASSERT(pixelCount > 0, "DeinterleaveDepthStencilSource: invalid texel size (%d, %d, %d)", + texelSize.x(), texelSize.y(), texelSize.z()); + const SizeT sourcePixelSize = packedDepth24Source ? 4 : 8; + if (sourceByteSize != pixelCount * sourcePixelSize) { + MGLOG_E("DeinterleaveDepthStencilSource: unexpected source byte size=%zu for pixelCount=%zu " + "sourcePixelSize=%zu", + sourceByteSize, pixelCount, sourcePixelSize); + return false; + } + + const SizeT depthAspectByteSize = pixelCount * sizeof(Uint32); + const SizeT paddedByteSize = (depthAspectByteSize + pixelCount + 3) / 4 * 4; + outDeinterleavedData.resize(paddedByteSize); + + const auto* src = static_cast(source); + Uint8* depthDst = outDeinterleavedData.data(); + Uint8* stencilDst = depthDst + depthAspectByteSize; + for (SizeT pixel = 0; pixel < pixelCount; ++pixel) { + Uint32 depthWord = 0; // depth in bits 0-23 for D24_UNORM, raw float32 bits for D32_SFLOAT + Uint8 stencilValue = 0; + if (packedDepth24Source) { + Uint32 packed = 0; + std::memcpy(&packed, src + pixel * 4, sizeof(packed)); + stencilValue = static_cast(packed & 0xFFu); + const Uint32 depth24 = packed >> 8; + if (imageFormat == VK_FORMAT_D24_UNORM_S8_UINT) { + depthWord = depth24; + } else { + const Float depthFloat = static_cast(depth24) / 16777215.0f; + std::memcpy(&depthWord, &depthFloat, sizeof(depthWord)); + } + } else { + Uint32 stencilWord = 0; + std::memcpy(&depthWord, src + pixel * 8, sizeof(depthWord)); + std::memcpy(&stencilWord, src + pixel * 8 + sizeof(Uint32), sizeof(stencilWord)); + stencilValue = static_cast(stencilWord & 0xFFu); + if (imageFormat == VK_FORMAT_D24_UNORM_S8_UINT) { + Float depthFloat = 0.0f; + std::memcpy(&depthFloat, &depthWord, sizeof(depthFloat)); + depthFloat = std::min(std::max(depthFloat, 0.0f), 1.0f); + depthWord = static_cast(depthFloat * 16777215.0f + 0.5f); + } + } + std::memcpy(depthDst + pixel * sizeof(Uint32), &depthWord, sizeof(depthWord)); + stencilDst[pixel] = stencilValue; + } + for (SizeT pad = depthAspectByteSize + pixelCount; pad < paddedByteSize; ++pad) { + outDeinterleavedData[pad] = 0; + } + + outDepthAspectByteSize = depthAspectByteSize; + return true; + } + static VkComponentSwizzle ToVkComponentSwizzle(TextureSwizzleParam swizzle) { switch (swizzle) { case TextureSwizzleParam::Red: @@ -1353,6 +1433,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 level = 0; Uint32 baseArrayLayer = 0; SizeT uploadByteSize = 0; + SizeT depthAspectByteSize = 0; IntVec3 texelSize = {0, 0, 0}; const void* source = nullptr; Vector expandedData; @@ -1368,6 +1449,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { } const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(mipmapTexture.GetFormat()); + // Combined depth-stencil images need per-aspect de-interleaved copies: VkBufferImageCopy's + // imageSubresource.aspectMask must have exactly one bit set. + const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format); + const Bool isCombinedDepthStencil = (aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && + (aspectMask & VK_IMAGE_ASPECT_STENCIL_BIT) != 0; + VkDeviceSize stagingSize = 0; for (const TextureUploadTarget target : targets) { const Uint32 definedMipLevels = GetUploadMipLevelCount(mipmapTexture, target); @@ -1411,6 +1498,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { mipmapTexture.GetExternalIndex(), MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level); uploadItem.uploadByteSize = uploadItem.expandedData.size(); + } else if (isCombinedDepthStencil) { + const Bool deinterleaved = DeinterleaveDepthStencilSource( + source, byteSize, texelSize, mipmapTexture.GetFormat(), outResource.format, + uploadItem.expandedData, uploadItem.depthAspectByteSize); + if (!deinterleaved) { + MGLOG_E("UploadDirtyMipLevels: failed to de-interleave depth-stencil textureId=%d target=%s " + "level=%u", + mipmapTexture.GetExternalIndex(), + MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level); + return false; + } + uploadItem.uploadByteSize = uploadItem.expandedData.size(); } uploadItems.push_back(Move(uploadItem)); if (!uploadItems.back().expandedData.empty()) { @@ -1424,19 +1523,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - // Combined depth-stencil images need per-aspect de-interleaved copies (VkBufferImageCopy - // aspectMask must have exactly one bit set). Until that is implemented, skip the upload - // instead of recording an invalid command buffer that kills the process. - const VkImageAspectFlags uploadAspectMask = GetAspectMaskForFormat(outResource.format); - if ((uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT)) { - MGLOG_E("UploadDirtyMipLevels: skipping unimplemented depth-stencil data upload for textureId=%d", - mipmapTexture.GetExternalIndex()); - for (const auto& item : uploadItems) { - mipmapTexture.MarkStorageDirty(item.target, item.level, false); - } - return true; - } - VkBuffer stagingBuffer = VK_NULL_HANDLE; VmaAllocation stagingAllocation = nullptr; @@ -1473,7 +1559,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; VK_VERIFY(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(texture)"); - const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format); VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags uploadSrcAccessMask = 0; GetImageTransitionSourceState(outResource.layout, uploadSrcStageMask, uploadSrcAccessMask); @@ -1499,8 +1584,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { copy.imageOffset = {0, 0, 0}; copy.imageExtent = {static_cast(item.texelSize.x()), static_cast(item.texelSize.y()), item.texelSize.z() > 0 ? static_cast(item.texelSize.z()) : 1u}; - vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - 1, ©); + if (isCombinedDepthStencil) { + // One copy per aspect: the staging blob holds the depth region followed by the + // tightly packed one-byte-per-texel stencil region. + VkBufferImageCopy aspectCopies[2] = {copy, copy}; + aspectCopies[0].imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + aspectCopies[1].imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; + aspectCopies[1].bufferOffset = item.offset + item.depthAspectByteSize; + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, aspectCopies); + } else { + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + } } const VkImageLayout finalLayout = ResolveSampledReadOnlyLayout(aspectMask); From 5eeba2957929289778317eac421f018ab51cbc7f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 04:11:48 -0400 Subject: [PATCH 17/44] [Fix] (CI): resume interrupted fixture downloads --- .github/scripts/fetch-trace-fixture-lfs.sh | 157 +++++++++++++++++++-- 1 file changed, 149 insertions(+), 8 deletions(-) diff --git a/.github/scripts/fetch-trace-fixture-lfs.sh b/.github/scripts/fetch-trace-fixture-lfs.sh index d8dab943..65a231ec 100644 --- a/.github/scripts/fetch-trace-fixture-lfs.sh +++ b/.github/scripts/fetch-trace-fixture-lfs.sh @@ -10,11 +10,22 @@ case_name="$1" fixture_dir="${2:-tools/trace_replay/fixtures}" python_bin="${PYTHON:-python3}" mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}" +download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}" +retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}" if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then python_bin=python fi +if ! [[ "${download_attempts}" =~ ^[1-9][0-9]*$ ]]; then + echo "MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS must be a positive integer: ${download_attempts}" >&2 + exit 2 +fi +if ! [[ "${retry_delay}" =~ ^[0-9]+$ ]]; then + echo "MOBILEGL_TRACE_FIXTURE_RETRY_DELAY must be a non-negative integer: ${retry_delay}" >&2 + exit 2 +fi + fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \ --format fixture-files \ --case "${case_name}" \ @@ -34,6 +45,140 @@ if [ "${case_name}" = "OpenRA" ]; then exit 0 fi +get_lfs_metadata() { + local file="$1" + local pointer + local expected_oid + local expected_size + + if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then + echo "failed to read tracked fixture metadata: ${file}" >&2 + return 1 + fi + if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then + echo "tracked fixture is not a Git LFS pointer: ${file}" >&2 + return 1 + fi + + expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")" + expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")" + if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then + echo "invalid Git LFS pointer metadata: ${file}" >&2 + return 1 + fi + + printf '%s %s\n' "${expected_oid}" "${expected_size}" +} + +verify_fixture_file() { + local downloaded_file="$1" + local display_name="$2" + local expected_oid="$3" + local expected_size="$4" + local actual_oid + local actual_size + + if [ ! -f "${downloaded_file}" ]; then + echo "fixture file is missing: ${display_name}" >&2 + return 1 + fi + + actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')" + if [ "${actual_size}" != "${expected_size}" ]; then + echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2 + return 1 + fi + + actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')" + if [ "${actual_oid}" != "${expected_oid}" ]; then + echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2 + return 1 + fi +} + +fetch_file_from_mirror() { + local file="$1" + local url="$2" + local metadata + local expected_oid + local expected_size + local tmp_file="${file}.tmp" + local attempt + local partial_size + local curl_status + + metadata="$(get_lfs_metadata "${file}")" || return 1 + read -r expected_oid expected_size <<< "${metadata}" + + if [ -f "${tmp_file}" ]; then + partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')" + if [ "${partial_size}" -gt "${expected_size}" ]; then + echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2 + rm -f "${tmp_file}" + elif [ "${partial_size}" = "${expected_size}" ]; then + if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then + mv "${tmp_file}" "${file}" + return 0 + fi + rm -f "${tmp_file}" + fi + fi + + for ((attempt = 1; attempt <= download_attempts; attempt++)); do + partial_size=0 + if [ -f "${tmp_file}" ]; then + partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')" + fi + + if [ "${partial_size}" -gt 0 ]; then + echo "Resuming mirror download for ${file} at byte ${partial_size} (attempt ${attempt}/${download_attempts})" + else + echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})" + fi + + if curl -L --fail --show-error --continue-at - --output "${tmp_file}" "${url}"; then + if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then + mv "${tmp_file}" "${file}" + return 0 + fi + echo "Mirror download failed integrity verification; retrying from the beginning: ${file}" >&2 + rm -f "${tmp_file}" + else + curl_status=$? + partial_size=0 + if [ -f "${tmp_file}" ]; then + partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')" + fi + + if [ "${partial_size}" = "${expected_size}" ]; then + if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then + mv "${tmp_file}" "${file}" + return 0 + fi + rm -f "${tmp_file}" + partial_size=0 + elif [ "${partial_size}" -gt "${expected_size}" ]; then + echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2 + rm -f "${tmp_file}" + partial_size=0 + elif [ "${curl_status}" -eq 33 ]; then + echo "Mirror refused the resume request; retrying from the beginning: ${file}" >&2 + rm -f "${tmp_file}" + partial_size=0 + fi + + echo "Mirror download attempt ${attempt}/${download_attempts} failed with curl exit ${curl_status}; retained ${partial_size} bytes for resume: ${file}" >&2 + fi + + if [ "${attempt}" -lt "${download_attempts}" ]; then + sleep "${retry_delay}" + fi + done + + rm -f "${tmp_file}" + return 1 +} + fetch_from_mirror() { mkdir -p "${fixture_dir}" for file in "${files[@]}"; do @@ -42,11 +187,9 @@ fetch_from_mirror() { name="$(basename "${file}")" url="${mirror_base%/}/${name}" echo "Fetching trace fixture from mirror: ${url}" - if ! curl -L --fail --retry 3 --retry-delay 2 -o "${file}.tmp" "${url}"; then - rm -f "${file}.tmp" + if ! fetch_file_from_mirror "${file}" "${url}"; then return 1 fi - mv "${file}.tmp" "${file}" done } @@ -59,9 +202,7 @@ else fi for file in "${files[@]}"; do - test -s "${file}" - if head -n 1 "${file}" | grep -q "version https://git-lfs.github.com/spec/v1"; then - echo "failed to hydrate LFS fixture: ${file}" >&2 - exit 1 - fi + metadata="$(get_lfs_metadata "${file}")" + read -r expected_oid expected_size <<< "${metadata}" + verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}" done From 20e1b417ccde8145be4e9e6755fce48f7d312093 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 04:39:57 -0400 Subject: [PATCH 18/44] [Fix] (MG_Util/Texture): expand channels and convert component types on texture unpack to the internal shadow layout --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 59 +- MobileGL/MG_Test/Texture/TextureTest.cpp | 157 +++++ MobileGL/MG_Util/Math/HalfFloat.h | 69 ++ .../MG_Util/Texture/PixelStoreProcessor.cpp | 591 ++++++++++++++++-- 4 files changed, 777 insertions(+), 99 deletions(-) create mode 100644 MobileGL/MG_Util/Math/HalfFloat.h diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 0fb5b3bd..c28e30d6 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -3270,62 +3271,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // wide RGBA format into scratch memory and repack into the caller's (format, type) layout on the CPU, // honoring the client-side PACK pixel-store parameters. - static Float DecodeHalfBitsToFloat(Uint16 half) { - const Uint32 sign = static_cast(half & 0x8000u) << 16; - const Uint32 exponent = (half >> 10) & 0x1Fu; - const Uint32 mantissa = half & 0x3FFu; - Uint32 bits; - if (exponent == 0) { - if (mantissa == 0) { - bits = sign; // signed zero - } else { - // Subnormal half: renormalize into a float exponent. - Uint32 e = 127 - 15 + 1; - Uint32 m = mantissa; - while ((m & 0x400u) == 0) { - m <<= 1; - --e; - } - bits = sign | (e << 23) | ((m & 0x3FFu) << 13); - } - } else if (exponent == 31) { - bits = sign | 0x7F800000u | (mantissa << 13); // Inf / NaN - } else { - bits = sign | ((exponent + 112) << 23) | (mantissa << 13); - } - return std::bit_cast(bits); - } - - static Uint16 EncodeFloatToHalfBits(Float value) { - const Uint32 bits = std::bit_cast(value); - const auto sign = static_cast((bits >> 16) & 0x8000u); - const Uint32 exponent = (bits >> 23) & 0xFFu; - const Uint32 mantissa = bits & 0x7FFFFFu; - if (exponent == 0xFF) { // Inf / NaN - return static_cast(sign | 0x7C00u | (mantissa != 0 ? 0x200u : 0u)); - } - const Int32 halfExponent = static_cast(exponent) - 127 + 15; - if (halfExponent >= 31) { - return static_cast(sign | 0x7C00u); // overflow -> Inf - } - if (halfExponent <= 0) { - if (halfExponent < -10) { - return sign; // underflow -> signed zero - } - const Uint32 m = mantissa | 0x800000u; - const Uint32 shift = static_cast(14 - halfExponent); - Uint32 half = m >> shift; - if ((m >> (shift - 1)) & 1u) { - ++half; // round to nearest - } - return static_cast(sign | half); - } - Uint32 half = (static_cast(halfExponent) << 10) | (mantissa >> 13); - if (mantissa & 0x1000u) { - ++half; // round to nearest; a carry into the exponent is the correct result - } - return static_cast(sign | half); - } + using MG_Util::DecodeHalfBitsToFloat; + using MG_Util::EncodeFloatToHalfBits; struct ReadbackChannelMapping { Int sourceChannel[4]; // RGBA source channel feeding each destination channel diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index d22a09f2..a9374109 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -750,6 +750,163 @@ TEST_F(TextureTest, GetInternalformativReportsBasicTextureMetadata) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(TextureTest, BoundTexImage2DExpandsRedUnsignedByteToRgba8) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 pixels[] = { + 10, 20, + 30, 40, + }; + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RED, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + const Uint8 expected[] = { + 10, 0, 0, 255, + 20, 0, 0, 255, + 30, 0, 0, 255, + 40, 0, 0, 255, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexSubImage2DExpandsRgUnsignedByteToRgba8) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + const Uint8 pixels[] = { + 10, 20, + 30, 40, + }; + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_RG, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + const Uint8 expected[] = { + 10, 20, 0, 255, + 30, 40, 0, 255, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage2DReordersBgrUnsignedByteToRgba8) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 pixels[] = { + 1, 2, 3, + 4, 5, 6, + }; + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 1, 0, GL_BGR, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + const Uint8 expected[] = { + 3, 2, 1, 255, + 6, 5, 4, 255, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage2DConvertsRedFloatToRgba8) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const GLfloat pixels[] = { + 0.0f, 0.5f, + 1.0f, 2.0f, // out-of-range values clamp to [0, 1] + }; + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RED, GL_FLOAT, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + const Uint8 expected[] = { + 0, 0, 0, 255, + 128, 0, 0, 255, + 255, 0, 0, 255, + 255, 0, 0, 255, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage2DExpandsRedIntegerUnsignedShortToRgba8ui) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint16 pixels[] = { + 10, 300, // 300 exceeds the 8-bit destination and clamps to 255 + }; + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8UI, 2, 1, 0, GL_RED_INTEGER, GL_UNSIGNED_SHORT, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + const Uint8 expected[] = { + 10, 0, 0, 1, // integer formats default missing alpha to 1, not the type maximum + 255, 0, 0, 1, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage2DExpandsRedToRgba8WithRowLengthAndSkips) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 pixels[] = { + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + }; + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 4); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_ROWS, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RED, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_ROWS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + const Uint8 expected[] = { + 6, 0, 0, 255, + 7, 0, 0, 255, + 10, 0, 0, 255, + 11, 0, 0, 255, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, NormalizeDepth24Stencil8UsesPackedDepthStencilType) { GLenum internalFormat = 0; GLenum format = 0; diff --git a/MobileGL/MG_Util/Math/HalfFloat.h b/MobileGL/MG_Util/Math/HalfFloat.h new file mode 100644 index 00000000..895a83bd --- /dev/null +++ b/MobileGL/MG_Util/Math/HalfFloat.h @@ -0,0 +1,69 @@ +// MobileGL - MobileGL/MG_Util/Math/HalfFloat.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 + +namespace MobileGL::MG_Util { + inline Float DecodeHalfBitsToFloat(Uint16 half) { + const Uint32 sign = static_cast(half & 0x8000u) << 16; + const Uint32 exponent = (half >> 10) & 0x1Fu; + const Uint32 mantissa = half & 0x3FFu; + Uint32 bits; + if (exponent == 0) { + if (mantissa == 0) { + bits = sign; // signed zero + } else { + // Subnormal half: renormalize into a float exponent. + Uint32 e = 127 - 15 + 1; + Uint32 m = mantissa; + while ((m & 0x400u) == 0) { + m <<= 1; + --e; + } + bits = sign | (e << 23) | ((m & 0x3FFu) << 13); + } + } else if (exponent == 31) { + bits = sign | 0x7F800000u | (mantissa << 13); // Inf / NaN + } else { + bits = sign | ((exponent + 112) << 23) | (mantissa << 13); + } + return std::bit_cast(bits); + } + + inline Uint16 EncodeFloatToHalfBits(Float value) { + const Uint32 bits = std::bit_cast(value); + const auto sign = static_cast((bits >> 16) & 0x8000u); + const Uint32 exponent = (bits >> 23) & 0xFFu; + const Uint32 mantissa = bits & 0x7FFFFFu; + if (exponent == 0xFF) { // Inf / NaN + return static_cast(sign | 0x7C00u | (mantissa != 0 ? 0x200u : 0u)); + } + const Int32 halfExponent = static_cast(exponent) - 127 + 15; + if (halfExponent >= 31) { + return static_cast(sign | 0x7C00u); // overflow -> Inf + } + if (halfExponent <= 0) { + if (halfExponent < -10) { + return sign; // underflow -> signed zero + } + const Uint32 m = mantissa | 0x800000u; + const Uint32 shift = static_cast(14 - halfExponent); + Uint32 half = m >> shift; + if ((m >> (shift - 1)) & 1u) { + ++half; // round to nearest + } + return static_cast(sign | half); + } + Uint32 half = (static_cast(halfExponent) << 10) | (mantissa >> 13); + if (mantissa & 0x1000u) { + ++half; // round to nearest; a carry into the exponent is the correct result + } + return static_cast(sign | half); + } +} // namespace MobileGL::MG_Util diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp index 122fe7ce..41fabf01 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp @@ -7,6 +7,8 @@ // End of Source File Header #include "PixelStoreProcessor.h" +#include "MG_Util/Math/HalfFloat.h" +#include namespace MobileGL::MG_Util::PixelStoreProcessor { static SizeT CalculateRowStride(Int width, SizeT pixelSize, Int alignment) { @@ -70,30 +72,523 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { } } - static Bool GetRgba8ByteSwizzleForUnpack(TextureInputFormat inputFormat, TexturePixelDataType inputDataType, - Vector& swizzle) { - if (inputFormat == TextureInputFormat::RGBA) { - if (inputDataType == TexturePixelDataType::UnsignedInt8888) { - swizzle = {TextureSwizzleParam::Alpha, TextureSwizzleParam::Blue, TextureSwizzleParam::Green, - TextureSwizzleParam::Red}; - return true; + // ---- Unpack channel expansion / type conversion ------------------------------------------------------------ + // The shadow mip buffer stores every level in the internal format's canonical layout: its channels in + // R,G,B(,A) order, encoded with the component type the backends upload with (see + // TextureFormatProcessor::NormalizePixelFormat; channelCount * componentSize matches + // GetSizedInternalFormatSizeInBytes for every format listed below). When the client's (format, type) + // does not already produce that byte layout, each texel is decoded to RGBA (float for normalized/float + // formats, integer for *_INTEGER formats, missing G/B = 0 and missing A = 1) and re-encoded. + + namespace { + enum class ShadowComponent { + UNorm8, + SNorm8, + UNorm16, + SNorm16, + UInt8, + Int8, + UInt16, + Int16, + UInt32, + Int32, + Half, + Float32, + }; + + struct InternalShadowLayout { + Int channelCount; + ShadowComponent component; + Bool isInteger; + }; + + SizeT GetShadowComponentSize(ShadowComponent component) { + switch (component) { + case ShadowComponent::UNorm8: + case ShadowComponent::SNorm8: + case ShadowComponent::UInt8: + case ShadowComponent::Int8: + return 1; + case ShadowComponent::UNorm16: + case ShadowComponent::SNorm16: + case ShadowComponent::UInt16: + case ShadowComponent::Int16: + case ShadowComponent::Half: + return 2; + default: + return 4; } - return false; } - if (inputFormat == TextureInputFormat::BGRA) { - if (inputDataType == TexturePixelDataType::UnsignedInt8888) { - swizzle = {TextureSwizzleParam::Green, TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha, - TextureSwizzleParam::Red}; - } else { - swizzle = {TextureSwizzleParam::Blue, TextureSwizzleParam::Green, TextureSwizzleParam::Red, - TextureSwizzleParam::Alpha}; + 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::RGB8: + case TextureInternalFormat::SRGB8: out = {3, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::RGBA8: + case TextureInternalFormat::SRGB8Alpha8: out = {4, ShadowComponent::UNorm8, 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; + case TextureInternalFormat::RGBA8Snorm: out = {4, ShadowComponent::SNorm8, false}; return true; + + case TextureInternalFormat::R16: out = {1, ShadowComponent::UNorm16, false}; return true; + case TextureInternalFormat::RG16: out = {2, ShadowComponent::UNorm16, false}; return true; + case TextureInternalFormat::RGB16: out = {3, ShadowComponent::UNorm16, false}; return true; + case TextureInternalFormat::RGBA16: out = {4, ShadowComponent::UNorm16, false}; return true; + + case TextureInternalFormat::R16Snorm: out = {1, ShadowComponent::SNorm16, false}; return true; + case TextureInternalFormat::RG16Snorm: out = {2, ShadowComponent::SNorm16, false}; return true; + case TextureInternalFormat::RGB16Snorm: out = {3, ShadowComponent::SNorm16, false}; return true; + case TextureInternalFormat::RGBA16Snorm: out = {4, ShadowComponent::SNorm16, false}; return true; + + case TextureInternalFormat::R16F: out = {1, ShadowComponent::Half, false}; return true; + case TextureInternalFormat::RG16F: out = {2, ShadowComponent::Half, false}; return true; + case TextureInternalFormat::RGB16F: out = {3, ShadowComponent::Half, false}; return true; + case TextureInternalFormat::RGBA16F: out = {4, ShadowComponent::Half, false}; return true; + + case TextureInternalFormat::R32F: out = {1, ShadowComponent::Float32, false}; return true; + case TextureInternalFormat::RG32F: out = {2, ShadowComponent::Float32, false}; return true; + case TextureInternalFormat::RGB32F: out = {3, ShadowComponent::Float32, false}; return true; + case TextureInternalFormat::RGBA32F: out = {4, ShadowComponent::Float32, false}; return true; + + case TextureInternalFormat::R8UI: out = {1, ShadowComponent::UInt8, true}; return true; + case TextureInternalFormat::RG8UI: out = {2, ShadowComponent::UInt8, true}; return true; + case TextureInternalFormat::RGB8UI: out = {3, ShadowComponent::UInt8, true}; return true; + case TextureInternalFormat::RGBA8UI: out = {4, ShadowComponent::UInt8, true}; return true; + + case TextureInternalFormat::R8I: out = {1, ShadowComponent::Int8, true}; return true; + case TextureInternalFormat::RG8I: out = {2, ShadowComponent::Int8, true}; return true; + case TextureInternalFormat::RGB8I: out = {3, ShadowComponent::Int8, true}; return true; + case TextureInternalFormat::RGBA8I: out = {4, ShadowComponent::Int8, true}; return true; + + case TextureInternalFormat::R16UI: out = {1, ShadowComponent::UInt16, true}; return true; + case TextureInternalFormat::RG16UI: out = {2, ShadowComponent::UInt16, true}; return true; + case TextureInternalFormat::RGB16UI: out = {3, ShadowComponent::UInt16, true}; return true; + case TextureInternalFormat::RGBA16UI: out = {4, ShadowComponent::UInt16, true}; return true; + + case TextureInternalFormat::R16I: out = {1, ShadowComponent::Int16, true}; return true; + case TextureInternalFormat::RG16I: out = {2, ShadowComponent::Int16, true}; return true; + case TextureInternalFormat::RGB16I: out = {3, ShadowComponent::Int16, true}; return true; + case TextureInternalFormat::RGBA16I: out = {4, ShadowComponent::Int16, true}; return true; + + case TextureInternalFormat::R32UI: out = {1, ShadowComponent::UInt32, true}; return true; + case TextureInternalFormat::RG32UI: out = {2, ShadowComponent::UInt32, true}; return true; + case TextureInternalFormat::RGB32UI: out = {3, ShadowComponent::UInt32, true}; return true; + case TextureInternalFormat::RGBA32UI: out = {4, ShadowComponent::UInt32, true}; return true; + + case TextureInternalFormat::R32I: out = {1, ShadowComponent::Int32, true}; return true; + case TextureInternalFormat::RG32I: out = {2, ShadowComponent::Int32, true}; return true; + case TextureInternalFormat::RGB32I: out = {3, ShadowComponent::Int32, true}; return true; + 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. + return false; + } + } + + struct UnpackChannelMapping { + Int formatPosition[4]; // position of R,G,B,A within the input format's component list; -1 = missing + Int channelCount; + Bool isInteger; + }; + + Bool GetUnpackChannelMapping(TextureInputFormat format, UnpackChannelMapping& out) { + switch (format) { + case TextureInputFormat::Red: out = {{0, -1, -1, -1}, 1, false}; return true; + case TextureInputFormat::RInteger: out = {{0, -1, -1, -1}, 1, true}; return true; + case TextureInputFormat::RG: out = {{0, 1, -1, -1}, 2, false}; return true; + case TextureInputFormat::RGInteger: out = {{0, 1, -1, -1}, 2, true}; return true; + case TextureInputFormat::RGB: out = {{0, 1, 2, -1}, 3, false}; return true; + case TextureInputFormat::RGBInteger: out = {{0, 1, 2, -1}, 3, true}; return true; + case TextureInputFormat::BGR: out = {{2, 1, 0, -1}, 3, false}; return true; + case TextureInputFormat::BGRInteger: out = {{2, 1, 0, -1}, 3, true}; return true; + case TextureInputFormat::RGBA: out = {{0, 1, 2, 3}, 4, false}; return true; + case TextureInputFormat::RGBAInteger: out = {{0, 1, 2, 3}, 4, true}; return true; + case TextureInputFormat::BGRA: out = {{2, 1, 0, 3}, 4, false}; return true; + case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true; + default: + return false; // depth / stencil / unknown + } + } + + struct PackedTypeLayout { + Int fieldCount; + Int width[4]; // bit width of each format component, in component order + Int totalBits; + Bool reversed; // *_REV: the first format component sits in the least significant bits + }; + + Bool GetPackedTypeLayout(TexturePixelDataType type, PackedTypeLayout& out) { + switch (type) { + case TexturePixelDataType::UnsignedByte332: out = {3, {3, 3, 2, 0}, 8, false}; return true; + case TexturePixelDataType::UnsignedByte233Rev: out = {3, {3, 3, 2, 0}, 8, true}; return true; + case TexturePixelDataType::UnsignedShort565: out = {3, {5, 6, 5, 0}, 16, false}; return true; + case TexturePixelDataType::UnsignedShort565Rev: out = {3, {5, 6, 5, 0}, 16, true}; return true; + case TexturePixelDataType::UnsignedShort4444: out = {4, {4, 4, 4, 4}, 16, false}; return true; + case TexturePixelDataType::UnsignedShort4444Rev: out = {4, {4, 4, 4, 4}, 16, true}; return true; + case TexturePixelDataType::UnsignedShort5551: out = {4, {5, 5, 5, 1}, 16, false}; return true; + case TexturePixelDataType::UnsignedShort1555Rev: out = {4, {5, 5, 5, 1}, 16, true}; return true; + case TexturePixelDataType::UnsignedInt8888: out = {4, {8, 8, 8, 8}, 32, false}; return true; + case TexturePixelDataType::UnsignedInt8888Rev: out = {4, {8, 8, 8, 8}, 32, true}; return true; + case TexturePixelDataType::UnsignedInt1010102: out = {4, {10, 10, 10, 2}, 32, false}; return true; + case TexturePixelDataType::UnsignedInt2101010Rev: out = {4, {10, 10, 10, 2}, 32, true}; return true; + default: + return false; // shared-exponent / packed-float / depth-stencil types stay on the legacy path + } + } + + // Base data types whose in-memory encoding equals a shadow component encoding (fast-path check). + Bool GetDirectShadowComponentForType(TexturePixelDataType type, Bool isInteger, ShadowComponent& out) { + switch (type) { + case TexturePixelDataType::UnsignedByte: + out = isInteger ? ShadowComponent::UInt8 : ShadowComponent::UNorm8; + return true; + case TexturePixelDataType::Byte: + out = isInteger ? ShadowComponent::Int8 : ShadowComponent::SNorm8; + return true; + case TexturePixelDataType::UnsignedShort: + out = isInteger ? ShadowComponent::UInt16 : ShadowComponent::UNorm16; + return true; + case TexturePixelDataType::Short: + out = isInteger ? ShadowComponent::Int16 : ShadowComponent::SNorm16; + return true; + case TexturePixelDataType::UnsignedInt: + if (!isInteger) return false; // no 32-bit normalized shadow layout + out = ShadowComponent::UInt32; + return true; + case TexturePixelDataType::Int: + if (!isInteger) return false; + out = ShadowComponent::Int32; + return true; + case TexturePixelDataType::HalfFloat: + if (isInteger) return false; + out = ShadowComponent::Half; + return true; + case TexturePixelDataType::Float: + if (isInteger) return false; + out = ShadowComponent::Float32; + return true; + default: + return false; + } + } + + Bool IsIdentityChannelOrder(const UnpackChannelMapping& mapping) { + for (Int i = 0; i < 4; ++i) { + const Int expected = i < mapping.channelCount ? i : -1; + if (mapping.formatPosition[i] != expected) return false; } return true; } - return false; - } + struct UnpackConversionSpec { + UnpackChannelMapping mapping; + InternalShadowLayout internal; + PackedTypeLayout packed; + Bool isPacked; + TexturePixelDataType type; + SizeT inputPixelSize; + SizeT swapGroupSize; // UNPACK_SWAP_BYTES group: packed word size, or the component size + SizeT internalPixelSize; + }; + + // Returns true when the (format, type) -> internal-format upload needs a per-texel conversion; + // returns false both for layouts that already match the shadow bytes (memcpy fast path) and for + // combinations the converter does not support (legacy copy behavior). + Bool GetUnpackConversionSpec(TextureInternalFormat internal, TextureInputFormat format, + TexturePixelDataType type, UnpackConversionSpec& out) { + InternalShadowLayout layout{}; + if (!GetInternalShadowLayout(internal, layout)) return false; + UnpackChannelMapping mapping{}; + if (!GetUnpackChannelMapping(format, mapping)) return false; + if (mapping.isInteger != layout.isInteger) return false; // rejected upstream; stay safe + + PackedTypeLayout packed{}; + const Bool isPacked = GetPackedTypeLayout(type, packed); + if (isPacked) { + if (packed.fieldCount != mapping.channelCount) return false; + // Byte layout already equals the RGBA8 shadow layout on little-endian. + if (internal == TextureInternalFormat::RGBA8 && format == TextureInputFormat::RGBA && + type == TexturePixelDataType::UnsignedInt8888Rev) { + return false; + } + } else { + ShadowComponent direct{}; + const Bool hasDirect = GetDirectShadowComponentForType(type, mapping.isInteger, direct); + switch (type) { + case TexturePixelDataType::UnsignedByte: + case TexturePixelDataType::Byte: + case TexturePixelDataType::UnsignedShort: + case TexturePixelDataType::Short: + case TexturePixelDataType::UnsignedInt: + case TexturePixelDataType::Int: + break; + case TexturePixelDataType::Float: + case TexturePixelDataType::HalfFloat: + if (mapping.isInteger) return false; // rejected upstream + break; + default: + return false; + } + if (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.packed = packed; + out.isPacked = isPacked; + out.type = type; + out.inputPixelSize = GetInputBytesPerPixel(format, type); + out.swapGroupSize = isPacked ? static_cast(packed.totalBits / 8) + : GetBaseTexturePixelDataTypeSize(type); + out.internalPixelSize = + static_cast(layout.channelCount) * GetShadowComponentSize(layout.component); + return true; + } + + Float DecodeComponentToFloat(const Uint8* p, TexturePixelDataType type) { + switch (type) { + case TexturePixelDataType::UnsignedByte: + return static_cast(*p) / 255.0f; + case TexturePixelDataType::Byte: { + Int8 v; + Memcpy(&v, p, sizeof(v)); + return std::max(static_cast(v) / 127.0f, -1.0f); + } + case TexturePixelDataType::UnsignedShort: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return static_cast(v) / 65535.0f; + } + case TexturePixelDataType::Short: { + Int16 v; + Memcpy(&v, p, sizeof(v)); + return std::max(static_cast(v) / 32767.0f, -1.0f); + } + case TexturePixelDataType::UnsignedInt: { + Uint32 v; + Memcpy(&v, p, sizeof(v)); + return static_cast(static_cast(v) / 4294967295.0); + } + case TexturePixelDataType::Int: { + Int32 v; + Memcpy(&v, p, sizeof(v)); + return static_cast(std::max(static_cast(v) / 2147483647.0, -1.0)); + } + case TexturePixelDataType::HalfFloat: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return DecodeHalfBitsToFloat(v); + } + case TexturePixelDataType::Float: { + Float v; + Memcpy(&v, p, sizeof(v)); + return v; + } + default: + return 0.0f; + } + } + + Int64 DecodeComponentToInt(const Uint8* p, TexturePixelDataType type) { + switch (type) { + case TexturePixelDataType::UnsignedByte: + return *p; + case TexturePixelDataType::Byte: { + Int8 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case TexturePixelDataType::UnsignedShort: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case TexturePixelDataType::Short: { + Int16 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case TexturePixelDataType::UnsignedInt: { + Uint32 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case TexturePixelDataType::Int: { + Int32 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + default: + return 0; + } + } + + Uint32 ReadPackedWord(const Uint8* p, Int totalBits) { + switch (totalBits) { + case 8: + return *p; + case 16: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + default: { + Uint32 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + } + } + + Uint32 ExtractPackedField(Uint32 word, const PackedTypeLayout& packed, Int position, Int& outWidth) { + Int shift; + if (packed.reversed) { + shift = 0; + for (Int i = 0; i < position; ++i) shift += packed.width[i]; + } else { + shift = packed.totalBits; + for (Int i = 0; i <= position; ++i) shift -= packed.width[i]; + } + outWidth = packed.width[position]; + const Uint32 mask = (1u << outWidth) - 1u; + return (word >> shift) & mask; + } + + void EncodeShadowComponentFloat(Uint8* dst, ShadowComponent component, Float v) { + switch (component) { + case ShadowComponent::UNorm8: { + const auto out = static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::SNorm8: { + const auto out = static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::UNorm16: { + const auto out = static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::SNorm16: { + const auto out = static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::Half: { + const Uint16 out = EncodeFloatToHalfBits(v); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::Float32: + Memcpy(dst, &v, sizeof(v)); + break; + default: + break; // integer components never reach the float encoder + } + } + + void EncodeShadowComponentInt(Uint8* dst, ShadowComponent component, Int64 v) { + switch (component) { + case ShadowComponent::UInt8: { + const auto out = static_cast(std::clamp(v, 0, 255)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::Int8: { + const auto out = static_cast(std::clamp(v, -128, 127)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::UInt16: { + const auto out = static_cast(std::clamp(v, 0, 65535)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::Int16: { + const auto out = static_cast(std::clamp(v, -32768, 32767)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::UInt32: { + const auto out = static_cast(std::clamp(v, 0, 4294967295LL)); + Memcpy(dst, &out, sizeof(out)); + break; + } + case ShadowComponent::Int32: { + const auto out = static_cast(std::clamp(v, -2147483648LL, 2147483647LL)); + Memcpy(dst, &out, sizeof(out)); + break; + } + default: + break; // float components never reach the integer encoder + } + } + + void ConvertUnpackRow(const Uint8* src, Uint8* dst, SizeT pixelCount, const UnpackConversionSpec& conv) { + const SizeT dstComponentSize = GetShadowComponentSize(conv.internal.component); + const SizeT srcComponentSize = conv.isPacked ? 0 : GetBaseTexturePixelDataTypeSize(conv.type); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* s = src + i * conv.inputPixelSize; + Uint8* d = dst + i * conv.internalPixelSize; + if (conv.internal.isInteger) { + Int64 rgba[4] = {0, 0, 0, 1}; + 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]; + if (pos < 0) continue; + Int width = 0; + rgba[ch] = ExtractPackedField(word, conv.packed, pos, width); + } + } else { + for (Int ch = 0; ch < 4; ++ch) { + const Int pos = conv.mapping.formatPosition[ch]; + if (pos < 0) continue; + rgba[ch] = DecodeComponentToInt(s + static_cast(pos) * srcComponentSize, conv.type); + } + } + 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) { + const Uint32 word = ReadPackedWord(s, conv.packed.totalBits); + for (Int ch = 0; ch < 4; ++ch) { + const Int pos = conv.mapping.formatPosition[ch]; + if (pos < 0) continue; + Int width = 0; + const Uint32 field = ExtractPackedField(word, conv.packed, pos, width); + rgba[ch] = static_cast(field) / static_cast((1u << width) - 1u); + } + } else { + for (Int ch = 0; ch < 4; ++ch) { + const Int pos = conv.mapping.formatPosition[ch]; + if (pos < 0) continue; + rgba[ch] = + DecodeComponentToFloat(s + static_cast(pos) * srcComponentSize, conv.type); + } + } + for (Int ch = 0; ch < conv.internal.channelCount; ++ch) { + EncodeShadowComponentFloat(d + static_cast(ch) * dstComponentSize, + conv.internal.component, rgba[ch]); + } + } + } + } + } // namespace // assume 8 bit per channel // swizzle.size() == channel count @@ -123,7 +618,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { const Int effectiveWidth = (params.RowLength > 0) ? params.RowLength : width; const Int effectiveHeight = (params.ImageHeight > 0) ? params.ImageHeight : height; const SizeT inputRowStride = CalculateRowStride(effectiveWidth, pixelSize, params.Alignment); - const SizeT outputRowStride = static_cast(width) * pixelSize; + + UnpackConversionSpec conversion{}; + const Bool needConversion = + !isBitmap && GetUnpackConversionSpec(targetInternalFormat, textureInputFormat, inputDataType, conversion); + const SizeT outputPixelSize = needConversion ? conversion.internalPixelSize : pixelSize; + const SizeT outputRowStride = static_cast(width) * outputPixelSize; const Int startX = params.SkipPixels; const Int startY = params.SkipRows; @@ -133,15 +633,16 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { const Int copyHeight = height; const Int copyDepth = depth; - MGLOG_D("%s: start at: (%d, %d, %d), copy size: (%d, %d, %d), i/o row stride: (%d, %dx%d)", __func__, startX, - startY, startZ, copyWidth, copyHeight, copyDepth, inputRowStride, width, pixelSize); + MGLOG_D("%s: start at: (%d, %d, %d), copy size: (%d, %d, %d), i/o row stride: (%d, %dx%d), convert: %d", + __func__, startX, startY, startZ, copyWidth, copyHeight, copyDepth, inputRowStride, width, + outputPixelSize, needConversion ? 1 : 0); if (copyWidth <= 0 || copyHeight <= 0 || copyDepth <= 0) { outSize = 0; return nullptr; } - outSize = static_cast(copyWidth) * copyHeight * copyDepth * pixelSize; + outSize = static_cast(copyWidth) * copyHeight * copyDepth * outputPixelSize; void* outputPixels = malloc(outSize); if (!outputPixels) { outSize = 0; @@ -155,38 +656,42 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { src += static_cast(startY) * inputRowStride; src += static_cast(startX) * pixelSize; - Bool isByteType = + const Bool isByteType = (inputDataType == TexturePixelDataType::UnsignedByte || inputDataType == TexturePixelDataType::Byte); - Vector colorSwizzle; - const Bool needColorSwizzle = - targetInternalFormat == TextureInternalFormat::RGBA8 && - GetRgba8ByteSwizzleForUnpack(textureInputFormat, inputDataType, colorSwizzle); + // UNPACK_SWAP_BYTES applies to the input elements (packed word / component) before conversion. + const Bool conversionSwapsBytes = needConversion && params.SwapBytes && conversion.swapGroupSize > 1; + Vector swapScratch; + if (conversionSwapsBytes) { + swapScratch.resize(static_cast(copyWidth) * pixelSize); + } for (Int z = 0; z < copyDepth; ++z) { const Uint8* layerSrc = src; Uint8* layerDst = dst; for (Int y = 0; y < copyHeight; ++y) { - Memcpy(layerDst, layerSrc, static_cast(copyWidth) * pixelSize); + if (needConversion) { + const Uint8* rowSrc = layerSrc; + if (conversionSwapsBytes) { + Memcpy(swapScratch.data(), layerSrc, static_cast(copyWidth) * pixelSize); + const SizeT groupCount = static_cast(copyWidth) * pixelSize / conversion.swapGroupSize; + SwapBytes(swapScratch.data(), conversion.swapGroupSize, groupCount); + rowSrc = swapScratch.data(); + } + ConvertUnpackRow(rowSrc, layerDst, static_cast(copyWidth), conversion); + } 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 && pixelSize > 1 && !isByteType) { + MGLOG_D("%s: SwapBytes", __func__); + SwapBytes(layerDst, pixelSize, static_cast(copyWidth)); + } - if (params.LSBFirst && isBitmap) { - MGLOG_D("%s: LSBFirst", __func__); - ProcessLSBFirst(layerDst, static_cast(copyWidth), 1); + if (params.LSBFirst && isBitmap) { + MGLOG_D("%s: LSBFirst", __func__); + ProcessLSBFirst(layerDst, static_cast(copyWidth), 1); + } } - if (needColorSwizzle) { - MGLOG_D("%s: Swizzle RGBA8 unpack", __func__); - // MGLOG_D("%s: pixel0 before = %x", __func__, *((Uint32*)layerDst)); - ProcessColorSwizzle(layerDst, static_cast(copyWidth), colorSwizzle); - // MGLOG_D("%s: pixel0 after = %x", __func__, *((Uint32*)layerDst)); - } - // else - // MGLOG_D("%s: pixel0 = %x", __func__, *((Uint32*)layerDst)); - layerSrc += inputRowStride; layerDst += outputRowStride; } From 274c234affce27c0b06fd9c1ad5f2c6cdb5084e3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 05:29:46 -0400 Subject: [PATCH 19/44] [Fix] (MG_State/ErrorState): GL error flags are sticky per error code, not an unbounded queue - repeated same-code errors accumulated and leaked into later unrelated glGetError checks (GL CTS "Texture state reset failed" deinit noise and false "Error during glGetTexImage" failures) --- MobileGL/MG_State/GLState/ErrorState/Error.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_State/GLState/ErrorState/Error.cpp b/MobileGL/MG_State/GLState/ErrorState/Error.cpp index b42c02db..5c0afc85 100644 --- a/MobileGL/MG_State/GLState/ErrorState/Error.cpp +++ b/MobileGL/MG_State/GLState/ErrorState/Error.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "Error.h" +#include #include #include @@ -19,7 +20,15 @@ namespace MobileGL::MG_State::GLState { MGLOG_E("Recording OpenGL error (%s):\n%s", MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(), info->toString().c_str()); - m_errors.push_back(MakeUnique(code, Move(info))); + // GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple + // error flags, each is set only while currently unset — repeated errors of the same + // code are discarded until glGetError reads the flag. Unbounded accumulation leaked + // stale errors into later, unrelated glGetError checks (GL CTS deinit noise). + const Bool alreadyPending = std::any_of(m_errors.begin(), m_errors.end(), + [code](const auto& e) { return e->code == code; }); + if (!alreadyPending) { + m_errors.push_back(MakeUnique(code, Move(info))); + } } } From 12a67f596c650a1807142e3bd444d4eaa0d26257 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 05:52:01 -0400 Subject: [PATCH 20/44] [Fix] (MG_Backend/DirectGLES): upload shadow mips with UNPACK_ALIGNMENT=1 - shadow rows are tightly packed but uploads ran with alignment 4, shifting every row of non-multiple-of-4-width textures by one pixel (R8 7-wide CTS gradients read back diagonally) --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 340627b7..f622dfb6 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1405,7 +1405,11 @@ namespace MobileGL::MG_Backend::DirectGLES { m_prevSkipPixels = s_skipPixels; m_prevImageHeight = s_imageHeight; m_prevSkipImages = s_skipImages; - Apply(4, 0, 0, 0, 0, 0); + // Shadow mip data is tightly packed (ProcessTexturePixelsDataUnpack emits + // width * bpp rows with no padding), so uploads must use UNPACK_ALIGNMENT = 1. + // Alignment 4 made the driver read e.g. 7-byte R8 rows at an 8-byte stride, + // shifting every row of a non-multiple-of-4 upload by one pixel. + Apply(1, 0, 0, 0, 0, 0); } ~ScopedDefaultUnpackState() { From bae222227abe64d0a471989d7129921115b99255 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 06:11:53 -0400 Subject: [PATCH 21/44] [Fix] (MG_Backend/DirectGLES, MG_Impl/GLImpl): ReadPixels - fall back to wide-format conversion when the ES driver rejects a legacy native read combo (Adreno errors on e.g. GL_RED/GL_UNSIGNED_INT and leaves the buffer untouched), and enforce packed-type/format pairing at the state layer via shared ValidateClientFormatTypePairing (GL_RED + GL_UNSIGNED_SHORT_5_6_5 now raises GL_INVALID_OPERATION) --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 23 ++++++++++++ .../GLImpl/Framebuffer/GL_Framebuffer.cpp | 7 ++++ .../MG_Impl/GLImpl/Texture/Validators.cpp | 35 ++++++++++++++----- MobileGL/MG_Impl/GLImpl/Texture/Validators.h | 1 + .../MG_Test/Framebuffer/FramebufferTest.cpp | 34 ++++++++++++++++++ 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index c28e30d6..b1075234 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3760,7 +3760,30 @@ namespace MobileGL::MG_Backend::DirectGLES { } MGLOG_D("ReadPixels: glReadPixels()"); + DrainESErrors(); g_GLESFuncs.glReadPixels(x, y, width, height, format, type, pixels); + const GLenum nativeReadError = g_GLESFuncs.glGetError(); + if (nativeReadError != GL_NO_ERROR) { + // ES drivers only guarantee GL_RGBA/GL_UNSIGNED_BYTE, GL_RGBA_INTEGER/(U)INT, float RGBA and one + // implementation-defined pair; legacy combos like GL_RED/GL_UNSIGNED_INT are rejected by e.g. + // Adreno with a GL error and an untouched destination (GL CTS packed_pixels r8_format_red). The + // failed read wrote nothing (client memory and PBO alike), so re-service the request through the + // wide-format conversion path before any PBO writeback can capture stale contents. The conversion + // helper saves/restores the ES pixel-pack binding and handles the state-layer PBO itself. + DrainESErrors(); + MGLOG_D("ReadPixels: native read of %s/%s failed (%s), retrying via client-format conversion", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), + MG_Util::ConvertGLEnumToString(nativeReadError).c_str()); + if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels)) { + MGLOG_D("ReadPixels: finished via client-format conversion after native failure"); + return; + } + MGLOG_E("ReadPixels: native read of %s/%s failed (%s) and no conversion path covers it, " + "skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), + MG_Util::ConvertGLEnumToString(nativeReadError).c_str()); + return; + } if (usePBO) { // pull back to client memory if PBO is used MGLOG_D("ReadPixels: PBO used, mapping buffer to client memory"); diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index e06f9032..f987cdfa 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -1681,6 +1681,13 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must + // raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators; + // runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM. + if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) { + return false; + } + // Check PBO state const auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index bb060efe..833c63cc 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -235,17 +235,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { format == TextureInputFormat::StencilIndex; } - // Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests - // isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and - // integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path. - Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format, - TextureInternalFormat internalFormat, - TexturePixelDataType type) { + // Client-memory format<->type pairing rules shared by pixel uploads (TexImage*) and readbacks + // (ReadPixels, GetTexImage). Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels + // (glcPackedPixelsTests isFormatValid): packed types constrain the formats they may pair with, and + // integer formats reject floating-point types; violations raise GL_INVALID_OPERATION. + Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type) { const auto recordInvalidOperation = [](const char* message) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput", - message)); + MakeUnique("MG_Impl/GLImpl", "ValidateClientFormatTypePairing", message)); return false; }; @@ -288,6 +286,27 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return recordInvalidOperation("Integer format cannot be used with a floating-point type"); } + return true; + } + + // Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests + // isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and + // integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path. + Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format, + TextureInternalFormat internalFormat, + TexturePixelDataType type) { + const auto recordInvalidOperation = [](const char* message) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput", + message)); + return false; + }; + + if (!ValidateClientFormatTypePairing(format, type)) { + return false; + } + // TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4). if (format == TextureInputFormat::StencilIndex) { return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format"); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index b88fdc6d..32291304 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -23,6 +23,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { Bool ValidateTextureSizeRange(Int width, Int height, Int depth); Bool ValidateTextureInternalFormat(TextureInternalFormat format); Bool ValidateTextureBorderNumber(Int border); + Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type); Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format, TextureInternalFormat internalFormat, TexturePixelDataType type); diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index 70ae7d11..919f06b5 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -221,6 +221,40 @@ TEST_F(FramebufferTest, ReadPixelsAllowsPersistentMappedPixelPackBuffer) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(FramebufferTest, ReadPixelsRejectsMismatchedPackedTypeFormatPairs) { + 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_RGBA8, 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] = {}; + + // Packed RGB type with a non-RGB format must never reach the backend (GL CTS packed_pixels + // reads GL_RED with GL_UNSIGNED_SHORT_5_6_5 and expects an error). + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RED, GL_UNSIGNED_SHORT_5_6_5, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Packed RGBA type with a non-RGBA/BGRA format is rejected as well. + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGB, GL_UNSIGNED_INT_8_8_8_8, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Packed depth-stencil type requires the DEPTH_STENCIL format. + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_INT_24_8, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // A plain RGBA/UNSIGNED_BYTE readback keeps working. + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(FramebufferTest, NamedRenderbufferStorageAndFramebufferAttachDoNotChangeBindings) { GLuint framebuffer = 0; GLuint renderbuffer = 0; From 1cefb9780b89c4bb5aed50215964815e25d55e96 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 06:47:33 -0400 Subject: [PATCH 22/44] [Feat] (MG_State, MG_Util, MG_Impl/GLImpl, MG_Backend/DirectGLES): desktop-GL single-channel client formats GL_GREEN/GL_BLUE/GL_ALPHA and _INTEGER variants - validate and readback via wide-RGBA channel extraction (GL CTS packed_pixels rgba8_format_green/blue read with them), unpack GREEN/BLUE(_INTEGER) TexImage uploads into the named channel with 0/1 defaults per table 3.3; GL_ALPHA keeps the legacy Red upload mapping (R8 storage + 000R swizzle), its readback corrected at the backend to source channel 3 --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 10 ++ .../MG_Impl/GLImpl/Texture/Validators.cpp | 4 +- .../GLState/TextureState/TextureEnum.h | 8 ++ .../MG_Test/Framebuffer/FramebufferTest.cpp | 45 ++++++++- MobileGL/MG_Test/Texture/TextureTest.cpp | 94 +++++++++++++++++++ .../GLToMG/TextureEnumConverter.cpp | 14 +++ .../Converters/GLToStr/GLEnumConverter.cpp | 1 + .../MGToGL/TextureEnumConverter.cpp | 12 +++ .../MGToStr/TextureEnumConverter.cpp | 12 +++ .../MGToVk/TextureEnumConverter.cpp | 10 ++ MobileGL/MG_Util/Metrics/TextureMetrics.cpp | 6 ++ .../MG_Util/Texture/PixelStoreProcessor.cpp | 6 ++ 12 files changed, 220 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index b1075234..61b98e32 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3284,6 +3284,16 @@ namespace MobileGL::MG_Backend::DirectGLES { switch (format) { case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true; case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true; + // Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue): + // the destination holds one component sourced from the named channel of the wide RGBA read. + // GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the + // legacy alpha-texture upload hack. + case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true; + case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true; + case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true; + case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true; + case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true; + case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true; case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true; case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true; case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true; diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index 833c63cc..f9713609 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -178,7 +178,9 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { static Bool IsIntegerColorInputFormat(TextureInputFormat format) { return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger || format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger || - format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger; + format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger || + format == TextureInputFormat::GreenInteger || format == TextureInputFormat::BlueInteger || + format == TextureInputFormat::AlphaInteger; } static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) { diff --git a/MobileGL/MG_State/GLState/TextureState/TextureEnum.h b/MobileGL/MG_State/GLState/TextureState/TextureEnum.h index 7e777493..945d1b59 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureEnum.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureEnum.h @@ -76,6 +76,14 @@ namespace MobileGL { BGRInteger, RGBAInteger, BGRAInteger, + // Desktop-GL single-channel client formats (table 3.3): the data holds one component that + // feeds the G, B or A channel; the remaining channels default to 0 (color) / 1 (alpha). + Green, + Blue, + Alpha, + GreenInteger, + BlueInteger, + AlphaInteger, StencilIndex, DepthComponent, DepthStencil, diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index 919f06b5..2d38e379 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -32,6 +32,8 @@ namespace { Int g_clearNamedFramebufferfvCallCount = 0; Int g_clearNamedFramebufferfiCallCount = 0; Int g_readPixelsCallCount = 0; + GLenum g_lastReadPixelsFormat = GL_NONE; + GLenum g_lastReadPixelsType = GL_NONE; void RecordBlitNamedFramebuffer(const SharedPtr& readFramebuffer, const SharedPtr& drawFramebuffer, @@ -66,8 +68,10 @@ namespace { ++g_clearNamedFramebufferfiCallCount; } - void RecordReadPixels(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*) { + void RecordReadPixels(GLint, GLint, GLsizei, GLsizei, GLenum format, GLenum type, void*) { ++g_readPixelsCallCount; + g_lastReadPixelsFormat = format; + g_lastReadPixelsType = type; } } // namespace @@ -97,6 +101,8 @@ protected: g_clearNamedFramebufferfvCallCount = 0; g_clearNamedFramebufferfiCallCount = 0; g_readPixelsCallCount = 0; + g_lastReadPixelsFormat = GL_NONE; + g_lastReadPixelsType = GL_NONE; MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer = nullptr; MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv = nullptr; MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi = nullptr; @@ -255,6 +261,43 @@ TEST_F(FramebufferTest, ReadPixelsRejectsMismatchedPackedTypeFormatPairs) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(FramebufferTest, ReadPixelsForwardsSingleChannelDesktopClientFormats) { + 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_RGBA8, 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] = {}; + + // Desktop GL treats GL_GREEN/GL_BLUE/GL_ALPHA as valid ReadPixels client formats (GL CTS + // packed_pixels rgba8_format_green failed with GL_INVALID_ENUM before). The state layer must + // validate them and forward the raw enum to the backend, which extracts the source channel + // from a wide RGBA read. + const GLenum singleChannelFormats[] = {GL_GREEN, GL_BLUE, GL_ALPHA}; + Int expectedCallCount = 0; + for (const GLenum format : singleChannelFormats) { + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, format, GL_UNSIGNED_BYTE, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, ++expectedCallCount); + EXPECT_EQ(g_lastReadPixelsFormat, format); + EXPECT_EQ(g_lastReadPixelsType, static_cast(GL_UNSIGNED_BYTE)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + } + + // Packed-type pairing still applies: packed RGB/RGBA types never pair with single-channel formats. + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_GREEN, GL_UNSIGNED_SHORT_5_6_5, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, expectedCallCount); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Integer client formats reject floating-point types. + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_GREEN_INTEGER, GL_FLOAT, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, expectedCallCount); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); +} + TEST_F(FramebufferTest, NamedRenderbufferStorageAndFramebufferAttachDoNotChangeBindings) { GLuint framebuffer = 0; GLuint renderbuffer = 0; diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index a9374109..c3039273 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -329,6 +329,100 @@ TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// Desktop GL table 3.3 lists GREEN and BLUE as TexImage client formats (GL CTS packed_pixels +// rgba8_format_green/blue upload with them and verify the readback): the single input component +// feeds the named channel, the other color channels default to 0 and alpha to 1. +TEST_F(TextureTest, BoundTexImage2DUnpacksGreenAndBlueIntoRgba8Channels) { + GLuint greenTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &greenTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, greenTexture); + + const Uint8 pixels[] = { + 10, 20, + 30, 40, + }; + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_GREEN, GL_UNSIGNED_BYTE, pixels); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto* storedGreen = GetBoundTexture2DLevelBytes(greenTexture); + const Uint8 expectedGreen[] = { + 0, 10, 0, 255, + 0, 20, 0, 255, + 0, 30, 0, 255, + 0, 40, 0, 255, + }; + for (SizeT i = 0; i < sizeof(expectedGreen); ++i) { + EXPECT_EQ(storedGreen[i], expectedGreen[i]) << "byte " << i; + } + + GLuint blueTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &blueTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, blueTexture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_BLUE, GL_UNSIGNED_BYTE, pixels); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto* storedBlue = GetBoundTexture2DLevelBytes(blueTexture); + const Uint8 expectedBlue[] = { + 0, 0, 10, 255, + 0, 0, 20, 255, + 0, 0, 30, 255, + 0, 0, 40, 255, + }; + for (SizeT i = 0; i < sizeof(expectedBlue); ++i) { + EXPECT_EQ(storedBlue[i], expectedBlue[i]) << "byte " << i; + } + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage2DUnpacksGreenIntegerIntoRgba8UiChannels) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 pixels[] = { + 10, 20, + 30, 40, + }; + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8UI, 2, 2, 0, GL_GREEN_INTEGER, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + // Missing integer channels default to R=0, B=0, A=1. + const Uint8 expected[] = { + 0, 10, 0, 1, + 0, 20, 0, 1, + 0, 30, 0, 1, + 0, 40, 0, 1, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } +} + +TEST_F(TextureTest, TexImage2DSingleChannelFormatsKeepIntegerNessRules) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + // Integer-ness of format and internal format must match (both directions). + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_GREEN_INTEGER, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8UI, 2, 2, 0, GL_BLUE, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Integer formats reject floating-point types. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8UI, 2, 2, 0, GL_BLUE_INTEGER, GL_FLOAT, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // Packed types never pair with single-channel formats. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_GREEN, GL_UNSIGNED_SHORT_5_6_5, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); +} + TEST_F(TextureTest, TexImage3DRejectsDepthFormatsForThreeDimensionalTarget) { GLuint texture = 0; MG_Impl::GLImpl::GenTextures(1, &texture); diff --git a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp index f8785806..42ad434f 100644 --- a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp @@ -58,9 +58,17 @@ namespace MobileGL { TextureInputFormat ConvertGLEnumToTextureInputFormat(GLenum format) { switch (format) { + // Legacy carve-out: GL_ALPHA stays mapped to Red so alpha-texture uploads keep landing in + // the R channel of the R8-backed storage (TexImage*_State pairs this with a 0,0,0,R + // swizzle). Readback of GL_ALPHA is corrected at the backend, which maps the raw enum to + // source channel 3 (DirectGLES GetReadbackChannelMapping). case GL_ALPHA: case GL_RED: return TextureInputFormat::Red; + case GL_GREEN: + return TextureInputFormat::Green; + case GL_BLUE: + return TextureInputFormat::Blue; case GL_RG: return TextureInputFormat::RG; case GL_RGB: @@ -73,6 +81,12 @@ namespace MobileGL { return TextureInputFormat::BGRA; case GL_RED_INTEGER: return TextureInputFormat::RInteger; + case GL_GREEN_INTEGER: + return TextureInputFormat::GreenInteger; + case GL_BLUE_INTEGER: + return TextureInputFormat::BlueInteger; + case GL_ALPHA_INTEGER: + return TextureInputFormat::AlphaInteger; case GL_RG_INTEGER: return TextureInputFormat::RGInteger; case GL_RGB_INTEGER: diff --git a/MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp index 07cb4400..a9bb6334 100644 --- a/MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp @@ -535,6 +535,7 @@ namespace MobileGL { CASE(GL_RED_INTEGER) CASE(GL_GREEN_INTEGER) CASE(GL_BLUE_INTEGER) + CASE(GL_ALPHA_INTEGER) CASE(GL_RGB_INTEGER) CASE(GL_RGBA_INTEGER) CASE(GL_BGR_INTEGER) diff --git a/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp index 864cf6de..f97b37bc 100644 --- a/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp @@ -67,6 +67,18 @@ namespace MobileGL { return GL_RGBA_INTEGER; case TextureInputFormat::BGRAInteger: return GL_BGRA_INTEGER; + case TextureInputFormat::Green: + return GL_GREEN; + case TextureInputFormat::Blue: + return GL_BLUE; + case TextureInputFormat::Alpha: + return GL_ALPHA; + case TextureInputFormat::GreenInteger: + return GL_GREEN_INTEGER; + case TextureInputFormat::BlueInteger: + return GL_BLUE_INTEGER; + case TextureInputFormat::AlphaInteger: + return GL_ALPHA_INTEGER; case TextureInputFormat::StencilIndex: return GL_STENCIL_INDEX; case TextureInputFormat::DepthComponent: diff --git a/MobileGL/MG_Util/Converters/MGToStr/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/MGToStr/TextureEnumConverter.cpp index e43f6e20..92b9efb6 100644 --- a/MobileGL/MG_Util/Converters/MGToStr/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/MGToStr/TextureEnumConverter.cpp @@ -67,6 +67,18 @@ namespace MobileGL { return "RGBAInteger"; case TextureInputFormat::BGRAInteger: return "BGRAInteger"; + case TextureInputFormat::Green: + return "Green"; + case TextureInputFormat::Blue: + return "Blue"; + case TextureInputFormat::Alpha: + return "Alpha"; + case TextureInputFormat::GreenInteger: + return "GreenInteger"; + case TextureInputFormat::BlueInteger: + return "BlueInteger"; + case TextureInputFormat::AlphaInteger: + return "AlphaInteger"; case TextureInputFormat::StencilIndex: return "StencilIndex"; case TextureInputFormat::DepthComponent: diff --git a/MobileGL/MG_Util/Converters/MGToVk/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/MGToVk/TextureEnumConverter.cpp index cb6802d8..66e45902 100644 --- a/MobileGL/MG_Util/Converters/MGToVk/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/MGToVk/TextureEnumConverter.cpp @@ -65,6 +65,16 @@ namespace MobileGL { return VK_FORMAT_R8G8B8A8_UINT; case TextureInputFormat::BGRAInteger: return VK_FORMAT_B8G8R8A8_UINT; + // Single-channel desktop client formats: the client memory holds one component per pixel, + // matching the R8 layouts (the channel it feeds is a pixel-transfer concern, not a layout one). + case TextureInputFormat::Green: + case TextureInputFormat::Blue: + case TextureInputFormat::Alpha: + return VK_FORMAT_R8_UNORM; + case TextureInputFormat::GreenInteger: + case TextureInputFormat::BlueInteger: + case TextureInputFormat::AlphaInteger: + return VK_FORMAT_R8_UINT; case TextureInputFormat::StencilIndex: return VK_FORMAT_S8_UINT; case TextureInputFormat::DepthComponent: diff --git a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp index 4bf7da45..c345ec3d 100644 --- a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp +++ b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp @@ -113,6 +113,12 @@ namespace MobileGL { switch (format) { case TextureInputFormat::Red: case TextureInputFormat::RInteger: + case TextureInputFormat::Green: + case TextureInputFormat::GreenInteger: + case TextureInputFormat::Blue: + case TextureInputFormat::BlueInteger: + case TextureInputFormat::Alpha: + case TextureInputFormat::AlphaInteger: return 1; case TextureInputFormat::RG: case TextureInputFormat::RGInteger: diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp index 41fabf01..a667765c 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp @@ -201,6 +201,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { switch (format) { case TextureInputFormat::Red: out = {{0, -1, -1, -1}, 1, false}; return true; case TextureInputFormat::RInteger: out = {{0, -1, -1, -1}, 1, true}; return true; + case TextureInputFormat::Green: out = {{-1, 0, -1, -1}, 1, false}; return true; + case TextureInputFormat::GreenInteger: out = {{-1, 0, -1, -1}, 1, true}; return true; + case TextureInputFormat::Blue: out = {{-1, -1, 0, -1}, 1, false}; return true; + case TextureInputFormat::BlueInteger: out = {{-1, -1, 0, -1}, 1, true}; return true; + case TextureInputFormat::Alpha: out = {{-1, -1, -1, 0}, 1, false}; return true; + case TextureInputFormat::AlphaInteger: out = {{-1, -1, -1, 0}, 1, true}; return true; case TextureInputFormat::RG: out = {{0, 1, -1, -1}, 2, false}; return true; case TextureInputFormat::RGInteger: out = {{0, 1, -1, -1}, 2, true}; return true; case TextureInputFormat::RGB: out = {{0, 1, 2, -1}, 3, false}; return true; From f896c7396f4b707bf777562035aea52f92d232d5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 10:54:48 -0400 Subject: [PATCH 23/44] [Fix] (MG_Impl/GLImpl): TexImage3D - apply ConvertInternalFormatToSized like 2D/1D so unsized-internal 3D uploads get channel/type conversion, skip proxy shadow allocation, auto-generate mipmaps; TexSubImage3D - bound level and region against the target mip --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 23 ++- MobileGL/MG_Test/Texture/TextureTest.cpp | 161 ++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 88201752..d0e958ea 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -779,6 +779,12 @@ namespace MobileGL::MG_Impl::GLImpl { MOBILEGL_ASSERT(nullptr != static_cast(textureObject.get()), "Texture object here should always be an object with mipmap"); auto textureMipmapObject = static_cast(textureObject.get()); + if (static_cast(level) >= textureMipmapObject->GetMipmapLevelCount()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "Texture level is out of range.")); + return; + } const void* originalPixels = pixels; const auto& pixelUnpackBufferObject = @@ -811,6 +817,14 @@ namespace MobileGL::MG_Impl::GLImpl { const SizeT destRowSize = static_cast(texelSize.x()) * internalBpp; const SizeT destSliceSize = static_cast(texelSize.y()) * destRowSize; + if (xoffset + width > static_cast(texelSize.x()) || + yoffset + height > static_cast(texelSize.y()) || + zoffset + depth > static_cast(texelSize.z())) { + MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions"); + free(processedPixels); + return; + } + const auto* srcData = static_cast(processedPixels); Uint8* destData = static_cast(textureMipmapObject->MapMipmapData(textureUploadTarget, level)); if (destData) { @@ -1378,6 +1392,8 @@ namespace MobileGL::MG_Impl::GLImpl { // target and data is not evenly divisible into the number of bytes needed to store in memory a datum // indicated by type. // ======================= Processing ================================ + textureInternalFormat = + MG_Util::ConvertInternalFormatToSized(textureInternalFormat, textureInputFormat, texturePixelDataType); auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget); @@ -1419,7 +1435,11 @@ namespace MobileGL::MG_Impl::GLImpl { auto textureMipmapObject = static_cast(textureObject.get()); // Allocate in TextureObject - textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes}); + if (isProxy) { + MGLOG_D("%s: isProxy = true, not allocating", __func__); + } else { + textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes}); + } if (!originalPixels) { MGLOG_D("%s: No input pixel and no PBO bound, no pixel transfer", __func__); @@ -1446,6 +1466,7 @@ namespace MobileGL::MG_Impl::GLImpl { textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); free(processedPixels); + MaybeAutoGenerateMipmap(target, textureObject, isProxy, level); } void TexImage2D_State(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index c3039273..ab950d29 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -774,6 +774,167 @@ TEST_F(TextureTest, TextureStorage3DAndSubImageModifyNamedObjectOnly) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +namespace { + const Uint8* GetBoundTexture3DLevelBytes(GLuint texture, Uint level = 0) { + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + auto* mipmapObject = static_cast(textureObject.get()); + return static_cast(mipmapObject->MapMipmapData(TextureUploadTarget::Texture3D, level)); + } +} // namespace + +TEST_F(TextureTest, BoundTexImage3DUnsizedRgbaInfersRgba8AndUnpacksBgra8888Rev) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture); + + const Uint8 pixels[] = { + 10, 20, 30, 40, + 50, 60, 70, 80, + 90, 100, 110, 120, + 130, 140, 150, 160, + }; + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, 2, 1, 2, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pixels); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::RGBA8); + + const auto* stored = GetBoundTexture3DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + const Uint8 expected[] = { + 30, 20, 10, 40, + 70, 60, 50, 80, + 110, 100, 90, 120, + 150, 140, 130, 160, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage3DHonorsImageHeightAndSkipImages) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture); + + // Source cuboid is 2x3 per image (IMAGE_HEIGHT = 3) with one leading image skipped; + // the upload reads a 2x2x2 sub-cuboid. + const Uint8 pixels[] = { + // image 0 (skipped) + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + // image 1: rows 0-1 are slice 0, row 2 is padding + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, + // image 2: rows 0-1 are slice 1, row 2 is padding + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, + }; + + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 3); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 1); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 0); + + const auto* stored = GetBoundTexture3DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + const Uint8 expected[] = { + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage3DConvertsRedToRgba8WithImageHeightAndSkips) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture); + + // Source cuboid: ROW_LENGTH = 3 (1-byte texels, alignment 1), IMAGE_HEIGHT = 2, + // skip 1 image, 0 rows, 1 pixel; upload a 2x1x2 sub-cuboid of GL_RED texels. + const Uint8 pixels[] = { + // image 0 (skipped) + 90, 91, 92, + 93, 94, 95, + // image 1: row 0 holds slice 0 at x offset 1, row 1 is padding + 80, 11, 12, + 81, 82, 83, + // image 2: row 0 holds slice 1 at x offset 1, row 1 is padding + 84, 21, 22, + 85, 86, 87, + }; + + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 3); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 2); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 1); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 2, 1, 2, 0, GL_RED, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 0); + + const auto* stored = GetBoundTexture3DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + const Uint8 expected[] = { + 11, 0, 0, 255, 12, 0, 0, 255, + 21, 0, 0, 255, 22, 0, 0, 255, + }; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexSubImage3DUnpacksPackedBgra8888RevIntoCorrectSlice) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture); + + const Uint8 zeros[2 * 2 * 2 * 4] = {}; + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, zeros); + + const Uint8 pixels[] = {10, 20, 30, 40}; + MG_Impl::GLImpl::TexSubImage3D(GL_TEXTURE_3D, 0, 1, 1, 1, 1, 1, 1, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pixels); + + const auto* stored = GetBoundTexture3DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + Uint8 expected[2 * 2 * 2 * 4] = {}; + expected[28] = 30; + expected[29] = 20; + expected[30] = 10; + expected[31] = 40; + for (SizeT i = 0; i < sizeof(expected); ++i) { + EXPECT_EQ(stored[i], expected[i]) << "byte " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexSubImage3DRejectsOutOfRangeLevel) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture); + + const Uint8 zeros[2 * 2 * 2 * 4] = {}; + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, zeros); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint8 pixels[] = {1, 2, 3, 4}; + MG_Impl::GLImpl::TexSubImage3D(GL_TEXTURE_3D, 3, 0, 0, 0, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); +} + TEST_F(TextureTest, NamedTextureVectorParametersAndGettersWorkWithoutBinding) { GLuint texture = 0; MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); From 0005a50517a9fed10a17185dfc5ef5a7a66f309a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 11:08:02 -0400 Subject: [PATCH 24/44] [Feat] (MG_Backend/DirectGLES): packed-type readback encoding - repack wide RGBA reads into all GL 3.3 packed pixel types (3_3_2/2_3_3_REV, 5_6_5(_REV), 4_4_4_4(_REV), 5_5_5_1/1_5_5_5_REV, 8_8_8_8(_REV), 10_10_10_2/2_10_10_10_REV, packed-float 10F_11F_11F_REV and shared-exponent 5_9_9_9_REV) for ReadPixels/GetTexImage; conversion helpers extracted to context-free ReadbackImpl (Utils.cpp) with unit tests asserting exact packed words against the GL CTS pack_* oracle layouts --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 250 +----------- MobileGL/MG_Backend/DirectGLES/Utils.cpp | 380 ++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/Utils.h | 45 +++ .../MG_Test/Framebuffer/FramebufferTest.cpp | 178 ++++++++ 4 files changed, 621 insertions(+), 232 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 61b98e32..35bcc6bb 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3269,69 +3269,13 @@ namespace MobileGL::MG_Backend::DirectGLES { // GL_RGBA/GL_FLOAT for float buffers plus one implementation-defined pair, while desktop GL clients read // back narrower layouts (RED, RG, RGB, BGR, byte-order packed types, ...). For those we read a guaranteed // wide RGBA format into scratch memory and repack into the caller's (format, type) layout on the CPU, - // honoring the client-side PACK pixel-store parameters. + // honoring the client-side PACK pixel-store parameters. The pure repacking helpers live in + // ReadbackImpl (Utils.cpp) so unit tests can assert the exact packed words. - using MG_Util::DecodeHalfBitsToFloat; - using MG_Util::EncodeFloatToHalfBits; - - struct ReadbackChannelMapping { - Int sourceChannel[4]; // RGBA source channel feeding each destination channel - Int channelCount; // destination channel count - Bool isInteger; - }; - - static Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) { - switch (format) { - case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true; - case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true; - // Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue): - // the destination holds one component sourced from the named channel of the wide RGBA read. - // GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the - // legacy alpha-texture upload hack. - case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true; - case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true; - case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true; - case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true; - case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true; - case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true; - case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true; - case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true; - case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true; - case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true; - case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true; - case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true; - case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true; - case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true; - case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true; - case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true; - default: - return false; - } - } - - static Bool IsPackedReadback8888Type(GLenum type) { - return type == GL_UNSIGNED_INT_8_8_8_8 || type == GL_UNSIGNED_INT_8_8_8_8_REV; - } - - static SizeT GetReadbackComponentSize(GLenum type) { - switch (type) { - case GL_UNSIGNED_BYTE: - case GL_BYTE: - return 1; - case GL_UNSIGNED_SHORT: - case GL_SHORT: - case GL_HALF_FLOAT: - return 2; - case GL_UNSIGNED_INT: - case GL_INT: - case GL_FLOAT: - case GL_UNSIGNED_INT_8_8_8_8: - case GL_UNSIGNED_INT_8_8_8_8_REV: - return 4; - default: - return 0; - } - } + using ReadbackImpl::GetReadbackChannelMapping; + using ReadbackImpl::GetReadbackComponentSize; + using ReadbackImpl::GetReadbackDstPixelSize; + using ReadbackImpl::ReadbackChannelMapping; static Bool CanDecodeWideSourceType(GLenum type) { switch (type) { @@ -3379,17 +3323,14 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!GetReadbackChannelMapping(format, mapping)) { return false; } - const Bool packed8888 = IsPackedReadback8888Type(type); - if (packed8888 && mapping.channelCount != 4) { - return false; - } - if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) { + // 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; } + ReadbackImpl::PackedReadbackLayout packedLayout{}; + const Bool isPackedType = ReadbackImpl::GetPackedReadbackLayout(type, packedLayout); const SizeT dstComponentSize = GetReadbackComponentSize(type); - if (dstComponentSize == 0) { - return false; - } if (width <= 0 || height <= 0) { return true; @@ -3473,8 +3414,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // Destination layout is computed from the client-side PACK parameters; only the actual pixel // rows are written so skip regions of the destination stay untouched. const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); - const SizeT dstPixelBytes = - packed8888 ? sizeof(Uint32) : static_cast(mapping.channelCount) * dstComponentSize; 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 + @@ -3497,164 +3436,11 @@ namespace MobileGL::MG_Backend::DirectGLES { for (GLsizei row = 0; row < height; ++row) { const Uint8* srcRow = wide.data() + static_cast(row) * static_cast(width) * srcPixelBytes; - for (GLsizei col = 0; col < width; ++col) { - const Uint8* srcPixel = srcRow + static_cast(col) * srcPixelBytes; - Uint8* dstPixel = convertedRow.data() + static_cast(col) * dstPixelBytes; - if (mapping.isInteger) { - Int64 src[4]; - for (Int c = 0; c < 4; ++c) { - src[c] = wideType == GL_INT - ? static_cast(reinterpret_cast(srcPixel)[c]) - : static_cast(reinterpret_cast(srcPixel)[c]); - } - if (packed8888) { - Uint32 word = 0; - for (Int ch = 0; ch < 4; ++ch) { - const auto v = - static_cast(std::clamp(src[mapping.sourceChannel[ch]], 0, 255)); - word |= type == GL_UNSIGNED_INT_8_8_8_8 ? v << (24 - ch * 8) : v << (ch * 8); - } - Memcpy(dstPixel, &word, sizeof(word)); - } else { - for (Int ch = 0; ch < mapping.channelCount; ++ch) { - const Int64 v = src[mapping.sourceChannel[ch]]; - Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; - switch (type) { - case GL_UNSIGNED_BYTE: - *dstComponent = static_cast(std::clamp(v, 0, 255)); - break; - case GL_BYTE: { - const auto out = static_cast(std::clamp(v, -128, 127)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_UNSIGNED_SHORT: { - const auto out = static_cast(std::clamp(v, 0, 65535)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_SHORT: { - const auto out = static_cast(std::clamp(v, -32768, 32767)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_UNSIGNED_INT: { - const auto out = static_cast(std::clamp(v, 0, 4294967295LL)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_INT: { - const auto out = - static_cast(std::clamp(v, -2147483648LL, 2147483647LL)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - default: - break; - } - } - } - } else { - Float src[4]; - switch (wideType) { - case GL_UNSIGNED_BYTE: - for (Int c = 0; c < 4; ++c) { - src[c] = static_cast(srcPixel[c]) / 255.0f; - } - break; - case GL_BYTE: - for (Int c = 0; c < 4; ++c) { - src[c] = std::max( - static_cast(reinterpret_cast(srcPixel)[c]) / 127.0f, -1.0f); - } - break; - case GL_UNSIGNED_SHORT: - for (Int c = 0; c < 4; ++c) { - src[c] = static_cast(reinterpret_cast(srcPixel)[c]) / 65535.0f; - } - break; - case GL_SHORT: - for (Int c = 0; c < 4; ++c) { - src[c] = std::max( - static_cast(reinterpret_cast(srcPixel)[c]) / 32767.0f, -1.0f); - } - break; - case GL_HALF_FLOAT: - for (Int c = 0; c < 4; ++c) { - src[c] = DecodeHalfBitsToFloat(reinterpret_cast(srcPixel)[c]); - } - break; - default: // GL_FLOAT - for (Int c = 0; c < 4; ++c) { - src[c] = reinterpret_cast(srcPixel)[c]; - } - break; - } - if (packed8888) { - Uint32 word = 0; - for (Int ch = 0; ch < 4; ++ch) { - const auto v = static_cast( - std::llround(std::clamp(src[mapping.sourceChannel[ch]], 0.0f, 1.0f) * 255.0)); - word |= type == GL_UNSIGNED_INT_8_8_8_8 ? v << (24 - ch * 8) : v << (ch * 8); - } - Memcpy(dstPixel, &word, sizeof(word)); - } else { - for (Int ch = 0; ch < mapping.channelCount; ++ch) { - const Float v = src[mapping.sourceChannel[ch]]; - Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; - switch (type) { - case GL_UNSIGNED_BYTE: - *dstComponent = - static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0)); - break; - case GL_BYTE: { - const auto out = - static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_UNSIGNED_SHORT: { - const auto out = - static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_SHORT: { - const auto out = - static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_UNSIGNED_INT: { - const auto out = static_cast( - std::llround(static_cast(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_INT: { - const auto out = static_cast( - std::llround(static_cast(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0)); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - case GL_FLOAT: - Memcpy(dstComponent, &v, sizeof(v)); - break; - case GL_HALF_FLOAT: { - const Uint16 out = EncodeFloatToHalfBits(v); - Memcpy(dstComponent, &out, sizeof(out)); - break; - } - default: - break; - } - } - } - } - } + ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast(width), wideType, + mapping, type); if (packParams.SwapBytes) { - const SizeT groupSize = packed8888 ? sizeof(Uint32) : dstComponentSize; + const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize; if (groupSize > 1) { for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) { std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize); @@ -3696,8 +3482,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // of killing the process; spec-invalid combinations are already rejected with GL errors at the state layer. const Bool useNativeReadback = IsLegacyNativeReadPixelsFormat(format) && IsLegacyNativeReadPixelsType(type); ReadbackChannelMapping conversionMapping{}; - const Bool convertible = - GetReadbackChannelMapping(format, conversionMapping) && GetReadbackComponentSize(type) != 0; + const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) && + GetReadbackDstPixelSize(conversionMapping, type) != 0; if (!useNativeReadback && !convertible) { MGLOG_E("ReadPixels: format %s with type %s is not implemented yet, skipping readback", MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); @@ -3838,8 +3624,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // spec-invalid combinations are already rejected with GL errors at the state layer. const Bool useNativeReadback = IsNativeGetTexImagePair(format, type); ReadbackChannelMapping conversionMapping{}; - const Bool convertible = - GetReadbackChannelMapping(format, conversionMapping) && GetReadbackComponentSize(type) != 0; + const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) && + GetReadbackDstPixelSize(conversionMapping, type) != 0; if (!useNativeReadback && !convertible) { 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()); diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 4b28e477..f446e25a 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -18,6 +18,9 @@ #include #include #include +#include + +#include namespace MobileGL::MG_Backend::DirectGLES { namespace { @@ -448,4 +451,381 @@ namespace MobileGL::MG_Backend::DirectGLES { } } } // namespace Utils + + // ---- Client-format readback conversion helpers ------------------------------------------------- + // ReadPixels/GetTexImage read a guaranteed wide RGBA(_INTEGER) layout from the ES driver and repack + // it on the CPU into the client's (format, type) layout. Everything here is pure byte shuffling so + // unit tests can assert the exact packed words; field positions follow GL 3.3 table 3.6 and mirror + // the GL CTS packed_pixels oracle (glcPackedPixelsTests.cpp pack_UNSIGNED_* helpers). + namespace ReadbackImpl { + using MG_Util::DecodeHalfBitsToFloat; + using MG_Util::EncodeFloatToHalfBits; + + Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) { + switch (format) { + case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true; + case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true; + // Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue): + // the destination holds one component sourced from the named channel of the wide RGBA read. + // GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the + // legacy alpha-texture upload hack. + case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true; + case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true; + case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true; + case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true; + case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true; + case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true; + case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true; + case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true; + case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true; + case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true; + case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true; + case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true; + case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true; + case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true; + case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true; + case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true; + default: + return false; + } + } + + Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out) { + switch (type) { + // Non-REV types pack the first format component starting at the most significant bit, + // *_REV types starting at the least significant bit (GL CTS pack_UNSIGNED_SHORT_5_6_5: + // R bits 15-11; pack_UNSIGNED_SHORT_1_5_5_5_REV: R bits 4-0, A bit 15). + case GL_UNSIGNED_BYTE_3_3_2: out = {3, {3, 3, 2, 0}, {5, 2, 0, 0}, 1, false}; return true; + case GL_UNSIGNED_BYTE_2_3_3_REV: out = {3, {3, 3, 2, 0}, {0, 3, 6, 0}, 1, false}; return true; + case GL_UNSIGNED_SHORT_5_6_5: out = {3, {5, 6, 5, 0}, {11, 5, 0, 0}, 2, false}; return true; + case GL_UNSIGNED_SHORT_5_6_5_REV: out = {3, {5, 6, 5, 0}, {0, 5, 11, 0}, 2, false}; return true; + case GL_UNSIGNED_SHORT_4_4_4_4: out = {4, {4, 4, 4, 4}, {12, 8, 4, 0}, 2, false}; return true; + case GL_UNSIGNED_SHORT_4_4_4_4_REV: out = {4, {4, 4, 4, 4}, {0, 4, 8, 12}, 2, false}; return true; + case GL_UNSIGNED_SHORT_5_5_5_1: out = {4, {5, 5, 5, 1}, {11, 6, 1, 0}, 2, false}; return true; + case GL_UNSIGNED_SHORT_1_5_5_5_REV: out = {4, {5, 5, 5, 1}, {0, 5, 10, 15}, 2, false}; return true; + case GL_UNSIGNED_INT_8_8_8_8: out = {4, {8, 8, 8, 8}, {24, 16, 8, 0}, 4, false}; return true; + case GL_UNSIGNED_INT_8_8_8_8_REV: out = {4, {8, 8, 8, 8}, {0, 8, 16, 24}, 4, false}; return true; + case GL_UNSIGNED_INT_10_10_10_2: out = {4, {10, 10, 10, 2}, {22, 12, 2, 0}, 4, false}; return true; + case GL_UNSIGNED_INT_2_10_10_10_REV: out = {4, {10, 10, 10, 2}, {0, 10, 20, 30}, 4, false}; return true; + // Packed-float RGB types: fields hold unsigned small floats; 5_9_9_9_REV's shared 5-bit + // exponent (bits 31-27) is emitted by EncodeSharedExponentRGB9E5, not a component field. + case GL_UNSIGNED_INT_10F_11F_11F_REV: out = {3, {11, 11, 10, 0}, {0, 11, 22, 0}, 4, true}; return true; + case GL_UNSIGNED_INT_5_9_9_9_REV: out = {3, {9, 9, 9, 0}, {0, 9, 18, 0}, 4, true}; return true; + default: + return false; + } + } + + SizeT GetReadbackComponentSize(GLenum type) { + PackedReadbackLayout packedLayout{}; + if (GetPackedReadbackLayout(type, packedLayout)) { + return packedLayout.byteSize; + } + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: + return 1; + case GL_UNSIGNED_SHORT: + case GL_SHORT: + case GL_HALF_FLOAT: + return 2; + case GL_UNSIGNED_INT: + case GL_INT: + case GL_FLOAT: + return 4; + default: + return 0; + } + } + + SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type) { + PackedReadbackLayout packedLayout{}; + if (GetPackedReadbackLayout(type, packedLayout)) { + if (packedLayout.fieldCount != mapping.channelCount) { + return 0; // 3-field packed types pair with 3-component formats only, 4 with 4 + } + if (mapping.isInteger && packedLayout.isFloatPacked) { + return 0; // packed-float RGB types never pair with integer formats + } + return packedLayout.byteSize; + } + if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) { + return 0; + } + const SizeT componentSize = GetReadbackComponentSize(type); + return componentSize == 0 ? 0 : static_cast(mapping.channelCount) * componentSize; + } + + 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: { + const auto out = static_cast(word); + Memcpy(dst, &out, sizeof(out)); + break; + } + case 2: { + const auto out = static_cast(word); + Memcpy(dst, &out, sizeof(out)); + break; + } + default: + Memcpy(dst, &word, sizeof(word)); + break; + } + } + } // 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; + } + + void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType, + const ReadbackChannelMapping& mapping, GLenum type) { + PackedReadbackLayout packedLayout{}; + const Bool isPacked = GetPackedReadbackLayout(type, packedLayout); + const SizeT dstComponentSize = GetReadbackComponentSize(type); + const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); + const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType); + + for (SizeT col = 0; col < width; ++col) { + const Uint8* srcPixel = src + col * srcPixelBytes; + Uint8* dstPixel = dst + col * dstPixelBytes; + if (mapping.isInteger) { + Int64 srcValues[4]; + for (Int c = 0; c < 4; ++c) { + srcValues[c] = wideType == GL_INT + ? static_cast(reinterpret_cast(srcPixel)[c]) + : static_cast(reinterpret_cast(srcPixel)[c]); + } + if (isPacked) { + // Integer sources clamp each component to the unsigned range of its field + // (GL 3.3 section 4.3.1 final conversion). + Uint32 word = 0; + for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) { + const Int64 fieldMax = (Int64{1} << packedLayout.width[ch]) - 1; + const auto v = static_cast( + std::clamp(srcValues[mapping.sourceChannel[ch]], 0, fieldMax)); + word |= v << packedLayout.shift[ch]; + } + WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize); + } else { + for (Int ch = 0; ch < mapping.channelCount; ++ch) { + const Int64 v = srcValues[mapping.sourceChannel[ch]]; + Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; + switch (type) { + case GL_UNSIGNED_BYTE: + *dstComponent = static_cast(std::clamp(v, 0, 255)); + break; + case GL_BYTE: { + const auto out = static_cast(std::clamp(v, -128, 127)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_SHORT: { + const auto out = static_cast(std::clamp(v, 0, 65535)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_SHORT: { + const auto out = static_cast(std::clamp(v, -32768, 32767)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_INT: { + const auto out = static_cast(std::clamp(v, 0, 4294967295LL)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_INT: { + const auto out = + static_cast(std::clamp(v, -2147483648LL, 2147483647LL)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + default: + break; + } + } + } + } else { + Float srcValues[4]; + switch (wideType) { + case GL_UNSIGNED_BYTE: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = static_cast(srcPixel[c]) / 255.0f; + } + break; + case GL_BYTE: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = std::max( + static_cast(reinterpret_cast(srcPixel)[c]) / 127.0f, -1.0f); + } + break; + case GL_UNSIGNED_SHORT: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = + static_cast(reinterpret_cast(srcPixel)[c]) / 65535.0f; + } + break; + case GL_SHORT: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = std::max( + static_cast(reinterpret_cast(srcPixel)[c]) / 32767.0f, -1.0f); + } + break; + case GL_HALF_FLOAT: + for (Int c = 0; c < 4; ++c) { + srcValues[c] = DecodeHalfBitsToFloat(reinterpret_cast(srcPixel)[c]); + } + break; + default: // GL_FLOAT + for (Int c = 0; c < 4; ++c) { + srcValues[c] = reinterpret_cast(srcPixel)[c]; + } + break; + } + if (isPacked) { + Uint32 word = 0; + if (packedLayout.isFloatPacked) { + const Float fields[3] = {srcValues[mapping.sourceChannel[0]], + srcValues[mapping.sourceChannel[1]], + srcValues[mapping.sourceChannel[2]]}; + word = type == GL_UNSIGNED_INT_5_9_9_9_REV + ? EncodeSharedExponentRGB9E5(fields) + : (EncodeFloatToUnsignedF11(fields[0]) << packedLayout.shift[0]) | + (EncodeFloatToUnsignedF11(fields[1]) << packedLayout.shift[1]) | + (EncodeFloatToUnsignedF10(fields[2]) << packedLayout.shift[2]); + } else { + // Normalized encode: round(clamp(v, 0, 1) * (2^bits - 1)) into each field. + for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) { + const auto fieldMax = static_cast((1u << packedLayout.width[ch]) - 1u); + const auto v = static_cast(std::llround( + std::clamp(srcValues[mapping.sourceChannel[ch]], 0.0f, 1.0f) * fieldMax)); + word |= v << packedLayout.shift[ch]; + } + } + WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize); + } else { + for (Int ch = 0; ch < mapping.channelCount; ++ch) { + const Float v = srcValues[mapping.sourceChannel[ch]]; + Uint8* dstComponent = dstPixel + static_cast(ch) * dstComponentSize; + switch (type) { + case GL_UNSIGNED_BYTE: + *dstComponent = + static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0)); + break; + case GL_BYTE: { + const auto out = + static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_SHORT: { + const auto out = + static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_SHORT: { + const auto out = + static_cast(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_UNSIGNED_INT: { + const auto out = static_cast( + std::llround(static_cast(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_INT: { + const auto out = static_cast( + std::llround(static_cast(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0)); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + case GL_FLOAT: + Memcpy(dstComponent, &v, sizeof(v)); + break; + case GL_HALF_FLOAT: { + const Uint16 out = EncodeFloatToHalfBits(v); + Memcpy(dstComponent, &out, sizeof(out)); + break; + } + default: + break; + } + } + } + } + } + } + } // namespace ReadbackImpl } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 484a8d0f..1068d0cd 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -45,6 +45,51 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace FramebufferImpl {} // namespace FramebufferImpl + // Pure CPU helpers of the client-format readback conversion (ReadPixels/GetTexImage repack a wide + // RGBA(_INTEGER) read into the caller's (format, type) layout). Kept context-free so unit tests can + // exercise the exact packing the GL CTS packed_pixels oracle compares against. + namespace ReadbackImpl { + struct ReadbackChannelMapping { + Int sourceChannel[4]; // RGBA source channel feeding each destination component + Int channelCount; // destination component count + Bool isInteger; + }; + Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping); + + // Byte size of one destination component of `type`; packed types report the packed word size. + // 0 = type not supported by the conversion path. + SizeT GetReadbackComponentSize(GLenum type); + + // Bit-field layout of a GL packed pixel type. width/shift are indexed in the client format's + // component order (matching ReadbackChannelMapping); shift is the LSB position of the field in + // the packed word: non-REV types pack the first component from the MSB, *_REV types from the + // LSB (GL 3.3 table 3.6; field positions mirror the GL CTS glcPackedPixelsTests pack_* oracle). + struct PackedReadbackLayout { + Int fieldCount; // format components stored in the packed word + Int width[4]; // bit width of each component's field + Int shift[4]; // LSB bit position of each component's field + SizeT byteSize; // packed word size in bytes (1, 2 or 4) + Bool isFloatPacked; // 10F_11F_11F_REV / 5_9_9_9_REV: fields hold unsigned small floats + }; + Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out); + + // Unsigned small-float encoders (EXT_packed_float / EXT_texture_shared_exponent semantics). + Uint32 EncodeFloatToUnsignedF11(Float value); + Uint32 EncodeFloatToUnsignedF10(Float value); + Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]); + + // Destination bytes per pixel for a (format mapping, type) readback pair; 0 when the pair is + // not convertible (unknown type, packed field count != format component count, floating-point + // or packed-float type with an integer format). + SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type); + + // Repacks one row of wide RGBA(_INTEGER) texels (4 components of wideType each) into the + // client's (format, type) layout. src holds width * 4 * GetReadbackComponentSize(wideType) + // bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes. + void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType, + const ReadbackChannelMapping& mapping, GLenum type); + } // namespace ReadbackImpl + namespace PrgramImpl { String ProcessOutColorLocations(const String& glslCode); String ForceSupporterOutput(const String& glslCode); diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index 2d38e379..c3b24c5e 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -11,6 +11,7 @@ #include "Includes.h" #include "Init.h" #include +#include #include #include #include @@ -496,3 +497,180 @@ TEST_F(FramebufferTest, BlitNamedFramebufferAllowsDefaultFramebufferZero) { EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), defaultRead); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } + +// ---- Packed-type readback encoding ------------------------------------------------------------------ +// Oracle-independent guard for the DirectGLES client-format readback conversion: feeds known wide RGBA +// rows through ReadbackImpl::ConvertWideReadbackRow and asserts the exact packed words. Field positions +// were hand-computed from GL 3.3 table 3.6 and match the GL CTS packed_pixels comparison functions +// (glcPackedPixelsTests.cpp pack_UNSIGNED_*): non-REV types pack the first format component from the +// most significant bit, *_REV types from the least significant bit. + +namespace { + namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl; + + // Converts a row of wide pixels (4 components of wideType each) into `format`/`type` words. + template + Vector ConvertWideRowToPackedWords(const Vector& wide, GLenum wideType, GLenum format, + GLenum type) { + ReadbackImpl::ReadbackChannelMapping mapping{}; + EXPECT_TRUE(ReadbackImpl::GetReadbackChannelMapping(format, mapping)); + EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(mapping, type), sizeof(WordT)); + const SizeT width = wide.size() / 4; + Vector out(width, static_cast(0)); + ReadbackImpl::ConvertWideReadbackRow(reinterpret_cast(wide.data()), + reinterpret_cast(out.data()), width, wideType, mapping, + type); + return out; + } + + // Normalized encodes read the wide row as RGBA8 (values are v / 255). + template + Vector ConvertRGBA8Row(const Vector& rgba, GLenum format, GLenum type) { + return ConvertWideRowToPackedWords(rgba, GL_UNSIGNED_BYTE, format, type); + } + + // Wide RGBA8 pattern shared by the normalized-encode tests. Expected fields below are + // round(v / 255 * (2^bits - 1)), computed by hand per pixel. + // R G B A + const Vector kRGBA8Row{255, 0, 128, 64, // P0 + 10, 250, 33, 200, // P1 + 85, 170, 255, 0}; // P2 +} // namespace + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565) { + // P0: R=31 G=0 B=round(128*31/255)=16 -> 31<<11 | 0<<5 | 16 = 0xF810 + // P1: R=round(10*31/255)=1 G=round(250*63/255)=62 B=round(33*31/255)=4 -> 1<<11|62<<5|4 = 0x0FC4 + // P2: R=round(85*31/255)=10 G=round(170*63/255)=42 B=31 -> 10<<11|42<<5|31 = 0x555F + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5); + EXPECT_EQ(words[0], 0xF810u); + EXPECT_EQ(words[1], 0x0FC4u); + EXPECT_EQ(words[2], 0x555Fu); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565Rev) { + // REV packs R from the LSB: P2 -> 10 | 42<<5 | 31<<11 = 0xFD4A + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5_REV); + EXPECT_EQ(words[2], 0xFD4Au); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444) { + // P0: R=15 G=0 B=round(128*15/255)=8 A=round(64*15/255)=4 -> 0xF084 + // P2: R=round(85*15/255)=5 G=round(170*15/255)=10 B=15 A=0 -> 0x5AF0 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4); + EXPECT_EQ(words[0], 0xF084u); + EXPECT_EQ(words[2], 0x5AF0u); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444Rev) { + // P0 fields R=15 G=0 B=8 A=4 packed from the LSB -> 15 | 0<<4 | 8<<8 | 4<<12 = 0x480F + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4_REV); + EXPECT_EQ(words[0], 0x480Fu); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort5551) { + // P0: R=31 G=0 B=16 A=round(64/255)=0 -> 31<<11 | 16<<1 = 0xF820 + // P1: R=1 G=round(250*31/255)=30 B=4 A=round(200/255)=1 -> 1<<11|30<<6|4<<1|1 = 0x0F89 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1); + EXPECT_EQ(words[0], 0xF820u); + EXPECT_EQ(words[1], 0x0F89u); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedShort1555Rev) { + // P1 fields R=1 G=30 B=4 A=1 packed from the LSB -> 1 | 30<<5 | 4<<10 | 1<<15 = 0x93C1 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV); + EXPECT_EQ(words[1], 0x93C1u); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedInt2101010Rev) { + // P0: R=1023 G=0 B=round(128*1023/255)=514 A=round(64*3/255)=1 -> 1023|514<<20|1<<30 = 0x602003FF + // P2: R=round(85*1023/255)=341 G=round(170*1023/255)=682 B=1023 A=0 -> 0x3FFAA955 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV); + EXPECT_EQ(words[0], 0x602003FFu); + EXPECT_EQ(words[2], 0x3FFAA955u); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedInt1010102) { + // P2 fields R=341 G=682 B=1023 A=0 packed from the MSB -> 341<<22 | 682<<12 | 1023<<2 = 0x556AAFFC + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_10_10_10_2); + EXPECT_EQ(words[2], 0x556AAFFCu); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedByte332) { + // P0: R=7 G=0 B=round(128*3/255)=2 -> 7<<5 | 2 = 0xE2 + // P1: R=round(10*7/255)=0 G=round(250*7/255)=7 B=round(33*3/255)=0 -> 7<<2 = 0x1C + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_3_3_2); + EXPECT_EQ(words[0], 0xE2u); + EXPECT_EQ(words[1], 0x1Cu); +} + +TEST(PackedReadbackEncodeTest, EncodesUnsignedByte233Rev) { + // P0 fields R=7 G=0 B=2 packed from the LSB -> 7 | 0<<3 | 2<<6 = 0x87 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_2_3_3_REV); + EXPECT_EQ(words[0], 0x87u); +} + +TEST(PackedReadbackEncodeTest, Encodes8888KeepsLegacyByteOrder) { + // Regression for the previously supported types: P0 = (255, 0, 128, 64). + const auto msbFirst = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8); + EXPECT_EQ(msbFirst[0], 0xFF008040u); + const auto lsbFirst = ConvertRGBA8Row(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV); + EXPECT_EQ(lsbFirst[0], 0x408000FFu); +} + +TEST(PackedReadbackEncodeTest, EncodesBGRAWithChannelMapping) { + // BGRA's first format component is Blue: P0 fields B=8 G=0 R=15 A=4 -> 8<<12 | 15<<4 | 4 = 0x80F4 + const auto words = ConvertRGBA8Row(kRGBA8Row, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4); + EXPECT_EQ(words[0], 0x80F4u); +} + +TEST(PackedReadbackEncodeTest, EncodesIntegerRGBA2101010RevWithFieldClamp) { + // Integer sources clamp to each field's unsigned range (10/10/10/2 bits). + const Vector wide{1023u, 1024u, 5u, 4u}; + const auto words = + ConvertWideRowToPackedWords(wide, GL_UNSIGNED_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV); + EXPECT_EQ(words[0], 0xC05FFFFFu); // 1023 | 1023<<10 | 5<<20 | 3<<30 +} + +TEST(PackedReadbackEncodeTest, EncodesIntegerNegativeValuesClampToZero) { + const Vector wide{-5, 2, 100000, 1}; + const auto words = + ConvertWideRowToPackedWords(wide, GL_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV); + EXPECT_EQ(words[0], 0x7FF00800u); // 0 | 2<<10 | 1023<<20 | 1<<30 +} + +TEST(PackedReadbackEncodeTest, EncodesIntegerRGB565) { + const Vector wide{31u, 64u, 2u, 0u}; + const auto words = + ConvertWideRowToPackedWords(wide, GL_UNSIGNED_INT, GL_RGB_INTEGER, GL_UNSIGNED_SHORT_5_6_5); + EXPECT_EQ(words[0], 0xFFE2u); // 31<<11 | 63<<5 | 2 (G clamps 64 -> 63) +} + +TEST(PackedReadbackEncodeTest, EncodesPackedFloat10F11F11FRev) { + // F11(1.0)=0x3C0 F11(0.5)=0x380 F10(0.25)=0x1A0 -> 0x3C0 | 0x380<<11 | 0x1A0<<22 = 0x681C03C0. + // Second pixel: values above 65024 clamp to the max finite F11 (0x7BF), negatives go to zero. + const Vector wide{1.0f, 0.5f, 0.25f, 1.0f, 100000.0f, -1.0f, 0.25f, 1.0f}; + const auto words = ConvertWideRowToPackedWords(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_10F_11F_11F_REV); + EXPECT_EQ(words[0], 0x681C03C0u); + EXPECT_EQ(words[1], 0x680007BFu); +} + +TEST(PackedReadbackEncodeTest, EncodesSharedExponent5999Rev) { + // (1.0, 0.5, 0.25): shared exponent 16, fields 256/128/64 -> 256 | 128<<9 | 64<<18 | 16<<27 + const Vector wide{1.0f, 0.5f, 0.25f, 1.0f}; + const auto words = ConvertWideRowToPackedWords(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV); + EXPECT_EQ(words[0], 0x81010100u); +} + +TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) { + ReadbackImpl::ReadbackChannelMapping rgba{}; + ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGBA, rgba)); + ReadbackImpl::ReadbackChannelMapping rgbInteger{}; + ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGB_INTEGER, rgbInteger)); + + // 3-field packed types never pair with 4-component formats and vice versa. + EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgba, GL_UNSIGNED_SHORT_5_6_5), 0u); + EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_SHORT_4_4_4_4), 0u); + // Packed-float RGB types never pair with integer formats. + 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); +} From 254cf1dc2124afd3d8b0990179e1bc43e7a60ca5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 12:09:11 -0400 Subject: [PATCH 25/44] [Fix] (MG_Util/ShaderTranspiler, MG_State, MG_Impl/GLImpl, MG_Backend/DirectGLES): GL CTS uniform_block - coerce packed/shared block layouts to std140 at source preprocess (glslang rejects them when targeting SPIR-V; std140 is the only UBO layout the pipeline emits), GL-style block reflection (array "[0]" names, per-element struct-array expansion, unused members and declared-but-unread blocks stay active), vec4-padded GL_UNIFORM_BLOCK_DATA_SIZE, std140 array strides for struct-nested arrays (glslang reflects tight strides there), arrayed-block instances share the first instance member set, glDeleteShader-flagged names stay usable while attached, and backend ESSL emits against highp default precision so relaxed block members match across stages (KHR-GL33.shaders.uniform_block on llvmpipe: 659 Fail -> 828/828 Pass) --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 11 + .../MG_Impl/GLImpl/Program/GL_Program.cpp | 9 +- MobileGL/MG_State/GLState/Core.cpp | 4 + MobileGL/MG_State/GLState/Core.h | 3 + .../GLState/ProgramState/ProgramObject.cpp | 30 ++- .../GLState/ProgramState/ProgramObject.h | 97 +++++++- .../GLState/ProgramState/ProgramState.cpp | 38 ++- .../GLState/ProgramState/ProgramState.h | 5 + MobileGL/MG_Test/Program/ProgramTest.cpp | 229 ++++++++++++++++++ MobileGL/MG_Test/Program/ProgramUtilTest.cpp | 54 +++++ .../ShaderSourceProcessor.cpp | 60 +++++ 11 files changed, 522 insertions(+), 18 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index f622dfb6..1a2d120e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2732,6 +2732,17 @@ namespace MobileGL::MG_Backend::DirectGLES { ResolveBackendEsslVersion()); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); + // Emit against highp default precision in every stage. SPIRV-Cross's fragment + // default is mediump, under which a RelaxedPrecision struct member prints with + // NO qualifier; ForceSupporterOutput later swaps the header to highp, silently + // flipping such members to highp. A uniform-block member that stays explicitly + // "mediump" in the vertex stage then mismatches, and the ES driver refuses to + // link ("definitions of uniform block ... do not match"). With highp defaults + // every relaxed member is printed with an explicit qualifier in both stages. + spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES_DEFAULT_FLOAT_PRECISION_HIGHP, + SPVC_TRUE); + spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES_DEFAULT_INT_PRECISION_HIGHP, + SPVC_TRUE); spvcSession.SetOptions(options); diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index ea8a1201..cc36b65f 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -339,6 +339,9 @@ namespace MobileGL::MG_Impl::GLImpl { MakeUnique("MG_Impl/GLImpl", __func__, "Shader is not attached to program.")); return; } + // A shader flagged with glDeleteShader lives on while attached; this detach may + // have been its last GL-visible attachment. + MG_State::pGLContext->ReleaseShaderNameIfOrphaned(shader); } void GetActiveAttrib_State(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size, @@ -1497,9 +1500,13 @@ namespace MobileGL::MG_Impl::GLImpl { MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params); break; case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: { + // Member entries of an arrayed block are recorded against the first instance; + // every instance of the array reports that shared member set (matches + // GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index). + const Int ownerIndex = static_cast(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex)); GLint uniformIndexCount = 0; for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) { - if (programObject->GetActiveUniformBlockIndex(uniformIndex) != static_cast(uniformBlockIndex)) { + if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) { continue; } params[uniformIndexCount++] = static_cast(uniformIndex); diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index d17bff3f..fecbec3e 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -279,6 +279,10 @@ namespace MobileGL::MG_State { return m_programState.MarkShaderObjectForDeletion(index); } + void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) { + return m_programState.ReleaseShaderNameIfOrphaned(index); + } + Bool GLContext::ValidateProgramName(const Uint index) const { return m_programState.ValidateProgramObject(index); } diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index ad65734a..cb0a3c86 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -116,6 +116,9 @@ namespace MobileGL { Uint CreateShader(ShaderStage stage); void MarkProgramForDeletion(Uint index); void MarkShaderForDeletion(Uint index); + // Frees a deletion-flagged shader's name once it lost its last GL-visible + // attachment (call after glDetachShader). + void ReleaseShaderNameIfOrphaned(Uint index); Bool ValidateProgramName(Uint index) const; Bool ValidateShaderName(Uint index) const; const SharedPtr& GetProgramObject(Uint index); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 5acd0bd6..2de41eda 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -360,7 +360,18 @@ namespace MobileGL::MG_State::GLState { } MGLOG_D("ProgramObject %u: DoReflection - building reflection", m_externalIndex); - if (!m_program->buildReflection()) { + // GL-style reflection naming (GL CTS uniform_block relies on all four): + // - BasicArraySuffix: an array uniform is reported as "arr[0]" per the GL spec. + // - StrictArraySuffix: named-block struct arrays expand per element ("s[0].a", + // "s[1].a", ...) following ARB_program_interface_query rules. Default-block + // (loose) uniforms already expand per element without this option. + // - AllBlockVariables: every member of an active named block is active even when + // no shader statement reads it (ES 3.0/GL 3.3 named-block semantics). + // - SharedStd140UBO: a DECLARED uniform block is active even when no member is + // ever read (reflected from the linker objects). PreprocessShaderSource coerces + // every block to std140, so this covers all of them. + if (!m_program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix | + EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) { m_linkStatus = false; m_infoLog = "Build reflection failed."; MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex); @@ -483,7 +494,14 @@ namespace MobileGL::MG_State::GLState { continue; } - const auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name); + // Reflection names an array "texs[0]" while the layout(binding = N) map from the IO + // resolver is keyed by the declared name ("texs"); look up both spellings. + auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name); + if (explicitBinding == m_explicitOpaqueUniformBindings.end() && uniform.name.length() > 3 && + uniform.name.compare(uniform.name.length() - 3, 3, "[0]") == 0) { + explicitBinding = + m_explicitOpaqueUniformBindings.find(uniform.name.substr(0, uniform.name.length() - 3)); + } const int initialUnit = explicitBinding != m_explicitOpaqueUniformBindings.end() ? static_cast(explicitBinding->second) : 0; const Int locationSpan = GetUniformLocationSpan(uniform); @@ -685,7 +703,13 @@ namespace MobileGL::MG_State::GLState { m_globalUboScratch.resize(size); } for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) { - const auto locationIt = m_uniformLocations.find(name); + // SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend + // reflection keys arrays as "arr[0]" (GL naming), so retry with the + // suffix before declaring the uniform unbacked. + auto locationIt = m_uniformLocations.find(name); + if (locationIt == m_uniformLocations.end()) { + locationIt = m_uniformLocations.find(name + "[0]"); + } if (locationIt == m_uniformLocations.end()) { MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in " "m_uniformLocations", diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 715f56a4..82bbfe3a 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -18,6 +18,13 @@ namespace MobileGL::MG_State::GLState { public: ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {} bool ShaderIsAttached(const SharedPtr& shader); + // GL-visible attachment: in the attach list and not pending detach (glDetachShader + // defers the actual removal to the next link). + Bool ShaderIsAttachedGLVisible(const SharedPtr& shader) const { + const auto matches = [&shader](const SharedPtr& s) { return s.get() == shader.get(); }; + if (std::none_of(m_shaders.begin(), m_shaders.end(), matches)) return false; + return std::none_of(m_detachedShaders.begin(), m_detachedShaders.end(), matches); + } bool AttachShader(const SharedPtr& shader); SizeT DetachShader(const SharedPtr& shader); SizeT RemoveShader(const SharedPtr& shader); @@ -45,10 +52,16 @@ namespace MobileGL::MG_State::GLState { const auto it = m_uniformLocations.find(name); if (it != m_uniformLocations.end()) return (Int)it->second; - // "arr[k]" resolves to the location of element k: glslang reflection stores - // arrays under their base name (no "[0]" suffix), and DoReflection reserves - // one location per array element, so element k lives at base + k. - if (name.length() < 4 || name.back() != ']') return -1; + // Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base + // location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves + // to base + k because DoReflection reserves one location per array element. + if (name.empty()) return -1; + if (name.back() != ']') { + const auto suffixedIt = m_uniformLocations.find(name + "[0]"); + if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second; + return -1; + } + if (name.length() < 4) return -1; const SizeT bracket = name.rfind('['); // Require at least one digit between the brackets. if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1; @@ -58,8 +71,13 @@ namespace MobileGL::MG_State::GLState { element = element * 10 + static_cast(name[i] - '0'); if (element > 0x0FFFFFFFu) return -1; } - const auto baseIt = m_uniformLocations.find(name.substr(0, bracket)); - if (baseIt == m_uniformLocations.end()) return -1; + auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]"); + if (baseIt == m_uniformLocations.end()) { + // Legacy key without the "[0]" suffix (defensive; reflection normally + // stores the suffixed form for arrays). + baseIt = m_uniformLocations.find(name.substr(0, bracket)); + if (baseIt == m_uniformLocations.end()) return -1; + } const Int base = (Int)baseIt->second; if (!IsValidUniformLocation(base)) return -1; const Int index = m_uniformIndexInTProgram[base]; @@ -86,6 +104,19 @@ namespace MobileGL::MG_State::GLState { return uniformIndex; } + // Reflection stores an array uniform under "arr[0]"; accept the bare "arr" + // spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for + // robustness against non-suffixed reflection entries. + if (!name.empty() && name.back() != ']') { + const String suffixedName = name + "[0]"; + const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str()); + if (suffixedIndex >= 0 && suffixedIndex < m_activeUniformCount && + m_program->getUniform(suffixedIndex).name == suffixedName) { + return suffixedIndex; + } + return -1; + } + if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1; const String baseName = name.substr(0, name.length() - 3); const Int baseIndex = m_program->getUniformIndex(baseName.c_str()); @@ -136,11 +167,24 @@ namespace MobileGL::MG_State::GLState { } // GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array - // block member; -1 for a default-block uniform. glslang yields arrayStride==0 for the - // default-block case, so gate on block membership to return the spec-mandated -1. + // block member; -1 for a default-block uniform (glslang yields arrayStride==0 there, so gate + // on block membership for the spec-mandated -1). The stride itself is derived from the type + // instead of glslang's reflected arrayStride: for an array nested inside a struct member, + // glslang computes that field against the enclosing STRUCT's (unset) packing and reports a + // tight std430-like stride (ivec2 a[7] -> 8), even though its own member offsets and the + // generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO + // layout is always std140, where every array element stride rounds up to a vec4. GLint GetActiveUniformArrayStride(Uint index) const { const auto& uniform = m_program->getUniform(static_cast(index)); - return (uniform.index < 0) ? -1 : uniform.arrayStride; + if (uniform.index < 0) return -1; + const glslang::TType* type = uniform.getType(); + if (type == nullptr || !type->isArray()) return 0; + if (type->isMatrix()) { + const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0; + const int vectors = rowMajor ? type->getMatrixRows() : type->getMatrixCols(); + return GetActiveUniformMatrixStride(index) * vectors; + } + return 16; // scalars and vectors: std140 rounds the element stride up to a vec4 } // GL_UNIFORM_IS_ROW_MAJOR: 1 only for a row-major matrix in a named block, else 0. The @@ -327,6 +371,11 @@ namespace MobileGL::MG_State::GLState { Uint GetUniformBlockIndex(const char* name) const { auto it = m_uniformBlockIndexByName.find(name); if (it != m_uniformBlockIndexByName.end()) return it->second; + // Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]"; + // a bare "Block" query resolves to the first instance per GL semantics. + const String suffixedName = String(name) + "[0]"; + it = m_uniformBlockIndexByName.find(suffixedName); + if (it != m_uniformBlockIndexByName.end()) return it->second; return 0xFFFFFFFFu; // GL_INVALID_INDEX } Bool IsActiveUniformBlock(Uint index) const { @@ -335,7 +384,11 @@ namespace MobileGL::MG_State::GLState { } Uint GetUBOSizeAt(Uint index) const { if (!IsActiveUniformBlock(index)) return 0; - return m_program->getUniformBlock((Int)index).size; + // glslang reports the unpadded end offset of the last member, but a std140 block + // (like a std140 struct) occupies a vec4-rounded size, and that is what the + // backend compiles: ES drivers reject draws whose bound UBO range is smaller + // than the block (a block ending in ivec3 reported 12 while the driver needs 16). + return (m_program->getUniformBlock((Int)index).size + 15u) & ~15u; } const String& GetUniformBlockName(Uint index) const { @@ -343,8 +396,30 @@ namespace MobileGL::MG_State::GLState { return ubo.name; } + // Uniform entries that belong to an arrayed uniform block are reflected once, against + // the first instance ("Block[0]"); per GL semantics every other instance shares that + // member set. Maps any instance's block index to the index owning the member entries. + Uint GetUniformBlockMemberOwnerIndex(Uint index) const { + const String& name = GetUniformBlockName(index); + if (name.empty() || name.back() != ']') return index; + const SizeT bracket = name.rfind('['); + if (bracket == String::npos) return index; + const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]"); + if (it != m_uniformBlockIndexByName.end()) return it->second; + return index; + } + + // GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: derived from the same active-uniform scan that + // fills GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, so the two queries always agree + // (glslang's numMembers counts declared members, which diverges from the reflected + // entry list for struct arrays and arrayed block instances). Int GetUniformBlockActiveUniformCount(Uint index) const { - return m_program->getUniformBlock((Int)index).numMembers; + const Int ownerIndex = static_cast(GetUniformBlockMemberOwnerIndex(index)); + Int count = 0; + for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) { + if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count; + } + return count; } Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const { diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp index 53e19880..1197f7aa 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp @@ -29,9 +29,18 @@ namespace MobileGL::MG_State::GLState { if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here auto& programObject = m_programObjects[program]; if (programObject != nullptr) { + // Snapshot the attachments: deleting the program is a detach point for shaders + // that were flagged with glDeleteShader while still attached. + const Vector> attachedShaders = programObject->GetAttachedShaders(); programObject->MarkAsDeleted(); programObject.reset(); m_programIndexGenerator.Delete(program); + for (const auto& shader : attachedShaders) { + const Uint shaderName = shader->GetExternalIndex(); + if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) { + ReleaseShaderNameIfOrphaned(shaderName); + } + } } } @@ -66,12 +75,35 @@ namespace MobileGL::MG_State::GLState { if (!CheckIndexAvail(shader, m_shaderObjects)) return; auto& shaderObject = m_shaderObjects[shader]; if (shaderObject != nullptr) { - m_shaderObjects[shader]->MarkAsDeleted(); - m_shaderObjects[shader].reset(); - m_shaderIndexGenerator.Delete(shader); + // glDeleteShader on an attached shader only FLAGS it; the name stays valid (and + // glShaderSource/glCompileShader keep working on it) until the shader is detached + // from every program. The GL CTS compiles shaders through exactly this + // create-attach-delete-source-compile sequence (uniform_block.common.name_matching). + shaderObject->MarkAsDeleted(); + ReleaseShaderNameIfOrphaned(shader); } } + Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr& shaderObject) const { + for (const auto& programObject : m_programObjects) { + if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) { + return true; + } + } + // A program deleted while current vacates its table slot but stays alive as the + // current program; its attachments still count. + return m_currentProgram != nullptr && m_currentProgram->ShaderIsAttachedGLVisible(shaderObject); + } + + void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader) { + if (!CheckIndexAvail(shader, m_shaderObjects)) return; + auto& shaderObject = m_shaderObjects[shader]; + if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return; + if (ShaderHasGLVisibleAttachment(shaderObject)) return; + shaderObject.reset(); + m_shaderIndexGenerator.Delete(shader); + } + Bool ProgramState::ValidateShaderObject(Uint shader) const { return CheckIndexAvail(shader, m_shaderObjects) && m_shaderObjects[shader] != nullptr; } diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.h b/MobileGL/MG_State/GLState/ProgramState/ProgramState.h index afdfe78f..c48ec59f 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.h @@ -26,11 +26,16 @@ namespace MobileGL::MG_State::GLState { Uint CreateShader(ShaderStage stage); const SharedPtr& GetShaderObject(Uint shader); void MarkShaderObjectForDeletion(Uint shader); + // Frees a deletion-flagged shader's name once no program holds a GL-visible + // attachment to it (the deferred half of glDeleteShader-while-attached). + void ReleaseShaderNameIfOrphaned(Uint shader); Bool ValidateShaderObject(Uint shader) const; const SharedPtr& GetCurrentProgram() const { return m_currentProgram; } private: + Bool ShaderHasGLVisibleAttachment(const SharedPtr& shaderObject) const; + template static Bool CheckIndexAvail(const SizeT idx, const Vector& vec) { return idx < vec.size(); diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index 015445a1..a26699bc 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -2053,3 +2053,232 @@ void main() { EXPECT_EQ(readback, 9.0f); // untouched by the overlong write EXPECT_EQ(GetError(), GL_NO_ERROR); } + +// --------------------------------------------------------------------------- +// GL CTS KHR-GL33.shaders.uniform_block regression pack. MobileGL's SPIR-V +// pipeline lays every uniform block out as std140; the frontend implements the +// GL-visible consequences of that choice: packed/shared qualifiers compile (as +// std140), reflection uses GL naming ("arr[0]", per-element struct arrays), +// unused block members stay active, block sizes are vec4-padded, and array +// strides are std140 even for arrays nested inside struct members. +// --------------------------------------------------------------------------- + +TEST_F(ProgramTest, UniformBlockPackedAndSharedLayoutsCompileAsStd140) { + const char* fsSource = R"(#version 330 +layout(packed) uniform PackedBlock { + vec4 pv; +}; +layout(shared, row_major) uniform SharedBlock { + float sf; + mat4 sm; +}; +out vec4 o_color; +void main() { + o_color = pv + vec4(sf) + vec4(sm[0][0]); +})"; + GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource); + + // The blocks land on the implementation's chosen layout: std140 offsets. + const GLuint pv = UniformIndexByName(program, "pv"); + const GLuint sf = UniformIndexByName(program, "sf"); + const GLuint sm = UniformIndexByName(program, "sm"); + ASSERT_NE(pv, GL_INVALID_INDEX); + ASSERT_NE(sf, GL_INVALID_INDEX); + ASSERT_NE(sm, GL_INVALID_INDEX); + EXPECT_EQ(QueryUniformiv(program, pv, GL_UNIFORM_OFFSET), 0); + EXPECT_EQ(QueryUniformiv(program, sf, GL_UNIFORM_OFFSET), 0); + EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_OFFSET), 16); + // The remaining qualifiers in the rewritten layout() list survive. + EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_IS_ROW_MAJOR), 1); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +TEST_F(ProgramTest, UniformBlockReflectsUnusedMembersWithGLNamesAndPaddedSize) { + const char* fsSource = R"(#version 330 +layout(std140) uniform Blk { + float used; + vec4 unusedArr[3]; + ivec3 tail; +}; +out vec4 o_color; +void main() { + o_color = vec4(used); +})"; + GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource); + + const GLuint blockIndex = GetUniformBlockIndex(program, "Blk"); + ASSERT_NE(blockIndex, GL_INVALID_INDEX); + + // All three members are active (unusedArr and tail are never read), the array is + // reported under its GL name "unusedArr[0]", and both spellings resolve. + const GLuint used = UniformIndexByName(program, "used"); + const GLuint unusedSuffixed = UniformIndexByName(program, "unusedArr[0]"); + const GLuint unusedBare = UniformIndexByName(program, "unusedArr"); + const GLuint tail = UniformIndexByName(program, "tail"); + ASSERT_NE(used, GL_INVALID_INDEX); + ASSERT_NE(unusedSuffixed, GL_INVALID_INDEX); + ASSERT_NE(tail, GL_INVALID_INDEX); + EXPECT_EQ(unusedSuffixed, unusedBare); + + char nameBuf[64] = ""; + GLsizei nameLen = 0; + GLint arraySize = 0; + GLenum type = 0; + GetActiveUniform(program, unusedSuffixed, sizeof(nameBuf), &nameLen, &arraySize, &type, nameBuf); + EXPECT_STREQ(nameBuf, "unusedArr[0]"); + EXPECT_EQ(arraySize, 3); + EXPECT_EQ(type, static_cast(GL_FLOAT_VEC4)); + + // std140 layout of the unused members. + EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_OFFSET), 16); + EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_ARRAY_STRIDE), 16); + EXPECT_EQ(QueryUniformiv(program, tail, GL_UNIFORM_OFFSET), 64); + + // GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS agrees with the INDICES list and counts all members. + GLint activeInBlock = 0; + GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &activeInBlock); + ASSERT_EQ(activeInBlock, 3); + GLint indices[3] = {-1, -1, -1}; + GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, indices); + for (GLint index : indices) { + EXPECT_TRUE(index == static_cast(used) || index == static_cast(unusedSuffixed) || + index == static_cast(tail)); + } + + // The block ends with an ivec3 at offset 64 (unpadded end 76); the backend compiles + // the std140 block at its vec4-padded size, and the reported size must cover it or + // buffers sized from this query are too small to draw with. + GLint dataSize = 0; + GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize); + EXPECT_EQ(dataSize, 80); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +TEST_F(ProgramTest, UniformBlockStructArrayExpandsPerElementWithStd140Strides) { + const char* fsSource = R"(#version 330 +struct S { + ivec2 v[2]; + float f; +}; +layout(std140) uniform Blk2 { + S s[2]; +} inst; +out vec4 o_color; +void main() { + o_color = vec4(inst.s[0].f); +})"; + GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource); + + // ARB_program_interface_query naming: one entry per struct array element, prefixed + // with the BLOCK name (not the instance name), basic arrays suffixed with "[0]". + const GLuint v0 = UniformIndexByName(program, "Blk2.s[0].v[0]"); + const GLuint f0 = UniformIndexByName(program, "Blk2.s[0].f"); + const GLuint v1 = UniformIndexByName(program, "Blk2.s[1].v[0]"); + const GLuint f1 = UniformIndexByName(program, "Blk2.s[1].f"); + ASSERT_NE(v0, GL_INVALID_INDEX); + ASSERT_NE(f0, GL_INVALID_INDEX); + ASSERT_NE(v1, GL_INVALID_INDEX); + ASSERT_NE(f1, GL_INVALID_INDEX); + + // std140: ivec2 v[2] rounds each element up to a vec4 (stride 16, NOT the tight 8 + // glslang reflects for arrays nested inside a struct member); struct size rounds to + // 48, giving s[1] members a 48-byte bias. + EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_OFFSET), 0); + EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_ARRAY_STRIDE), 16); + EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_SIZE), 2); + EXPECT_EQ(QueryUniformiv(program, f0, GL_UNIFORM_OFFSET), 32); + EXPECT_EQ(QueryUniformiv(program, v1, GL_UNIFORM_OFFSET), 48); + EXPECT_EQ(QueryUniformiv(program, f1, GL_UNIFORM_OFFSET), 80); + + GLint dataSize = 0; + const GLuint blockIndex = GetUniformBlockIndex(program, "Blk2"); + ASSERT_NE(blockIndex, GL_INVALID_INDEX); + GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize); + EXPECT_EQ(dataSize, 96); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +TEST_F(ProgramTest, UniformBlockInstanceArrayReportsPerInstanceBlocks) { + const char* fsSource = R"(#version 330 +layout(std140) uniform ArrBlk { + vec4 av; +} insts[2]; +out vec4 o_color; +void main() { + o_color = insts[0].av + insts[1].av; +})"; + GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource); + + const GLuint inst0 = GetUniformBlockIndex(program, "ArrBlk[0]"); + const GLuint inst1 = GetUniformBlockIndex(program, "ArrBlk[1]"); + ASSERT_NE(inst0, GL_INVALID_INDEX); + ASSERT_NE(inst1, GL_INVALID_INDEX); + EXPECT_NE(inst0, inst1); + // A bare block name resolves to the first instance. + EXPECT_EQ(GetUniformBlockIndex(program, "ArrBlk"), inst0); + + // Every instance of the array shares the single reflected member set. + GLint count0 = 0; + GLint count1 = 0; + GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count0); + GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count1); + EXPECT_EQ(count0, 1); + EXPECT_EQ(count1, 1); + GLint index0 = -1; + GLint index1 = -1; + GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index0); + GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index1); + EXPECT_EQ(index0, index1); + EXPECT_EQ(static_cast(index0), UniformIndexByName(program, "ArrBlk.av")); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +TEST_F(ProgramTest, DeleteShaderWhileAttachedKeepsNameUsableUntilDetach) { + // GL CTS compiles through exactly this sequence (create, attach, DELETE, source, + // compile): glDeleteShader on an attached shader only flags it, and the name must + // keep working until the last detach. + const char* vsSource = R"(#version 330 +void main() { gl_Position = vec4(0.0); } +)"; + const char* fsSource = R"(#version 330 +out vec4 o_color; +void main() { o_color = vec4(1.0); } +)"; + + GLuint program = CreateProgram(); + GLuint vs = CreateShader(GL_VERTEX_SHADER); + AttachShader(program, vs); + DeleteShader(vs); + EXPECT_EQ(IsShader(vs), GL_TRUE); // still alive: attached + + ShaderSource(vs, 1, &vsSource, nullptr); + CompileShader(vs); + GLint status = GL_FALSE; + GetShaderiv(vs, GL_COMPILE_STATUS, &status); + EXPECT_EQ(status, GL_TRUE); + status = GL_FALSE; + GetShaderiv(vs, GL_DELETE_STATUS, &status); + EXPECT_EQ(status, GL_TRUE); + + GLuint fs = CreateShader(GL_FRAGMENT_SHADER); + AttachShader(program, fs); + DeleteShader(fs); + ShaderSource(fs, 1, &fsSource, nullptr); + CompileShader(fs); + + LinkProgram(program); + GLint linkStatus = GL_FALSE; + char infoLog[1024] = ""; + GetProgramiv(program, GL_LINK_STATUS, &linkStatus); + GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog); + EXPECT_EQ(linkStatus, GL_TRUE) << infoLog; + + // The last GL-visible detach releases the flagged shader's name. + DetachShader(program, vs); + EXPECT_EQ(IsShader(vs), GL_FALSE); + + // Deleting the program releases the other flagged shader. + DeleteProgram(program); + EXPECT_EQ(IsShader(fs), GL_FALSE); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 652826f6..40cbd817 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -1017,3 +1017,57 @@ void main() { ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(binRes->at(0), optimized)); } + +TEST_F(ProgramUtilTest, PreprocessCoercesBlockPackingQualifiersToStd140) { + using namespace MG_Util::ShaderTranspiler; + + // glslang rejects `packed`/`shared` outright when generating SPIR-V, and MobileGL's + // UBO layout is always std140 anyway; the preprocessor rewrites the qualifiers so the + // validation compile, reflection, and generated SPIR-V all agree on std140 (GL CTS + // KHR-GL33.shaders.uniform_block.*.packed/shared). + String source = R"(#version 330 +layout(packed) uniform PackedBlock { vec4 pv; }; +layout(shared, row_major) uniform SharedBlock { mat4 sm; }; +layout ( shared ) uniform SpacedBlock { float sx; }; +layout(std140) uniform KeptBlock { float kx; }; +// A non-layout use of the identifier stays untouched (compute storage qualifier). +void main() { + gl_Position = pv + vec4(sm[0][0]) + vec4(sx) + vec4(kx); +})"; + + PreprocessShaderSource(ShaderStage::Vertex, source); + + EXPECT_EQ(source.find("packed"), String::npos); + EXPECT_EQ(source.find("layout(shared"), String::npos); + EXPECT_NE(source.find("layout(std140) uniform PackedBlock"), String::npos); + EXPECT_NE(source.find("layout(std140, row_major) uniform SharedBlock"), String::npos); + EXPECT_NE(source.find("layout ( std140 ) uniform SpacedBlock"), String::npos); + EXPECT_NE(source.find("layout(std140) uniform KeptBlock"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, + .sourceStr = source, + .flags = ShaderCompileBits::CompileForOpenGL}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + +TEST_F(ProgramUtilTest, PreprocessLeavesComputeSharedStorageQualifierAlone) { + using namespace MG_Util::ShaderTranspiler; + + // `shared` is only a packing qualifier inside layout(...); the compute-shader storage + // qualifier of the same spelling must survive. + String source = R"(#version 430 +layout(local_size_x = 8) in; +shared float sharedScratch[8]; +layout(shared) uniform Blk { float bx; }; +void main() { + sharedScratch[gl_LocalInvocationIndex] = bx; +})"; + + PreprocessShaderSource(ShaderStage::Compute, source); + + EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos); + EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index 43c2d5aa..70e3f62e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -261,6 +261,65 @@ namespace { ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64"); } + // Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to + // `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the + // app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as + // std140 (glslang under a SPIR-V target rejects `packed`/`shared` outright and SPIRV-Cross has + // no other packing for UBOs), so std140 IS this implementation's chosen layout. Rewriting at + // the source level keeps the validation compile, the reflection the app queries, and the + // generated SPIR-V all agreeing on that choice. Both replacement tokens are 6 characters, so + // the rewrite is done in place. + void CoerceUniformBlockPackingToStd140(MobileGL::String& source) { + constexpr const char* layoutToken = "layout"; + constexpr SizeT layoutLen = 6; + + SizeT pos = 0; + while ((pos = source.find(layoutToken, pos)) != MobileGL::String::npos) { + const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(source[pos - 1]); + SizeT probe = pos + layoutLen; + const bool hasRightBoundary = probe >= source.size() || !IsIdentifierChar(source[probe]); + if (!hasLeftBoundary || !hasRightBoundary) { + pos = probe; + continue; + } + + while (probe < source.size() && std::isspace(static_cast(source[probe]))) { + probe++; + } + if (probe >= source.size() || source[probe] != '(') { + pos = probe; + continue; + } + + // Scan the qualifier list; layout qualifier values may contain parenthesized + // constant expressions, so track nesting until the matching ')'. + SizeT cursor = probe + 1; + int depth = 1; + while (cursor < source.size() && depth > 0) { + const char ch = source[cursor]; + if (ch == '(') { + depth++; + } else if (ch == ')') { + depth--; + } else if (IsIdentifierChar(ch) && (cursor == 0 || !IsIdentifierChar(source[cursor - 1]))) { + SizeT identifierEnd = cursor; + while (identifierEnd < source.size() && IsIdentifierChar(source[identifierEnd])) { + identifierEnd++; + } + const SizeT identifierLen = identifierEnd - cursor; + if (identifierLen == 6 && (source.compare(cursor, 6, "packed") == 0 || + source.compare(cursor, 6, "shared") == 0)) { + source.replace(cursor, 6, "std140"); + } + cursor = identifierEnd; + continue; + } + cursor++; + } + pos = cursor; + } + } + void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) { // Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and // ignored in the forced "#version 460 core" profile, so glslang handles them natively. @@ -383,6 +442,7 @@ namespace MobileGL { } FilterUnsupportedGpuShaderInt64(source); + CoerceUniformBlockPackingToStd140(source); // Some shader packs define helpers with built-in GLSL names such as round(), tanh(), or fma(). // These may pass OpenGL-style validation but fail when recompiled for Vulkan/SPIR-V generation. From 6a843b3088a7267adc269490af961061f5c17c1c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 12:39:55 -0400 Subject: [PATCH 26/44] [Fix] (MG_Backend/DirectVulkan): honor scissor in glClear --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e17ed5fe..ef48452e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3598,6 +3598,117 @@ void main() { .depth = MG_State::pGLContext->GetClearDepth(), .stencil = MG_State::pGLContext->GetClearStencil() }; + + // A render-pass loadOp clear always covers the complete attachment, while + // OpenGL glClear is clipped by GL_SCISSOR_TEST. Blaze3D relies on this for + // GuiItemAtlas: animated items clear only their atlas slot before being + // redrawn. Queueing that clear as a loadOp erases every cached static item. + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) { + m_frameContext.BeginCommandRecording(); + } + + auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); + auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*fbo, m_imageIndexAcquired); + if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + activeRenderPass = nullptr; + renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*fbo, m_imageIndexAcquired); + } + if (renderPassEntry->attachmentCount == 0 || + renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { + return; + } + + if (activeRenderPass && activeRenderPass->CompatibleWith(*renderPassEntry)) { + // Materialize any older whole-attachment clear before applying this + // ordered, scissored clear. + ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); + } else { + const Bool began = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry); + MOBILEGL_ASSERT(began, "%s: BeginRenderPass failed", __func__); + } + + VkClearRect clearRect{}; + clearRect.rect = fbo->IsDefaultFramebuffer() + ? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(), + renderPassEntry->extent, + m_swapchainObject.GetPreTransform()) + : MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent); + clearRect.baseArrayLayer = 0; + clearRect.layerCount = 1; + if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) { + return; + } + + Vector clearAttachments; + clearAttachments.reserve(fbo->GetDrawBuffers().size() + 1); + + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + const auto& drawBuffers = fbo->GetDrawBuffers(); + for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) { + const auto attachmentType = drawBuffers[drawBufferIndex]; + if (attachmentType == FramebufferAttachmentType::None) { + continue; + } + const auto& attachment = fbo->GetAttachment(attachmentType); + if (!attachment.IsComplete()) { + continue; + } + + const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex); + if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { + continue; + } + if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { + MGLOG_W("DirectVulkan: scissored glClear with a partial color mask is not supported"); + continue; + } + + MG_State::GLState::ITextureObject* colorTexture = nullptr; + if (attachment.IsTexture()) { + colorTexture = attachment.GetTexture().get(); + } + VkClearAttachment clearAttachment{}; + clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clearAttachment.colorAttachment = drawBufferIndex; + clearAttachment.clearValue.color = { + payload.color.x(), payload.color.y(), payload.color.z(), + ResolveColorClearAlpha(colorTexture, payload.color.w()) + }; + clearAttachments.push_back(clearAttachment); + } + } + + VkImageAspectFlags depthStencilAspects = 0; + if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) { + const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth); + if (depthAttachment.IsComplete()) { + depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT; + } + } + if ((mask & GL_STENCIL_BUFFER_BIT) != 0) { + const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil); + if (stencilAttachment.IsComplete()) { + depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + } + } + if (depthStencilAspects != 0) { + VkClearAttachment clearAttachment{}; + clearAttachment.aspectMask = depthStencilAspects; + clearAttachment.clearValue.depthStencil = {payload.depth, payload.stencil}; + clearAttachments.push_back(clearAttachment); + } + + if (!clearAttachments.empty()) { + vkCmdClearAttachments(frame.commandBuffer, + static_cast(clearAttachments.size()), clearAttachments.data(), + 1, &clearRect); + } + return; + } + m_clearManager->QueueClear(mask, payload, *fbo); m_renderPassManager->QueueRenderbufferClear(mask, payload, *fbo); } From 8bf8f6f9062d036c036a0854249704a7b1ecf4f6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 16:36:28 -0400 Subject: [PATCH 27/44] [Fix] (MG_Backend/DirectGLES, MG_Util/ShaderTranspiler): make the uniform-block cross-stage precision fix surgical - revert the global SPVC ES highp-default options (they changed emission for EVERY fragment shader: sampling code that used to inherit the effective highp default was suddenly printed as explicit mediump, regressing KHR-GL3x.texture_repeat_mode NPOT mip cases on device) and instead strip RelaxedPrecision member decorations from uniform-block-reachable structs in a DirectGLES-only SPIR-V pass, so matched blocks declare identical (highp) member precision in both stages and every other shader keeps its previous emission byte-for-byte --- CMakeLists.txt | 1 + MobileGL/MG_Backend/DirectGLES/Managers.cpp | 25 ++-- .../ShaderTranspiler/ShaderCompiler.cpp | 14 +++ .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 6 + .../StripUboMemberRelaxedPrecisionPass.cpp | 119 ++++++++++++++++++ .../StripUboMemberRelaxedPrecisionPass.h | 44 +++++++ 6 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.h diff --git a/CMakeLists.txt b/CMakeLists.txt index bae83295..88a02fe5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 1a2d120e..bf3aaa7d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2722,6 +2722,20 @@ namespace MobileGL::MG_Backend::DirectGLES { effectiveSpirv = &loweredSpirv; } + // ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints + // a RelaxedPrecision member as explicit "mediump" in the vertex stage and as + // UNQUALIFIED (mediump-by-default) in the fragment stage; after + // ForceSupporterOutput swaps the fragment header to highp, that member reads + // back as highp and the ES driver refuses to link ("definitions of uniform + // block ... do not match"). Strip the hint from block structs so both stages + // declare the member highp; nothing else about emission changes. + Vector uboPrecisionSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl( + *effectiveSpirv, uboPrecisionSpirv) && + !uboPrecisionSpirv.empty()) { + effectiveSpirv = &uboPrecisionSpirv; + } + MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile); @@ -2732,17 +2746,6 @@ namespace MobileGL::MG_Backend::DirectGLES { ResolveBackendEsslVersion()); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); - // Emit against highp default precision in every stage. SPIRV-Cross's fragment - // default is mediump, under which a RelaxedPrecision struct member prints with - // NO qualifier; ForceSupporterOutput later swaps the header to highp, silently - // flipping such members to highp. A uniform-block member that stays explicitly - // "mediump" in the vertex stage then mismatches, and the ES driver refuses to - // link ("definitions of uniform block ... do not match"). With highp defaults - // every relaxed member is printed with an explicit qualifier in both stages. - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES_DEFAULT_FLOAT_PRECISION_HIGHP, - SPVC_TRUE); - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES_DEFAULT_INT_PRECISION_HIGHP, - SPVC_TRUE); spvcSession.SetOptions(options); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index b9e6f100..4833d1d0 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -14,6 +14,7 @@ #include "SpirvPasses/DecomposeWorkgroupVec3Pass.h" #include "SpirvPasses/LowerDrawParametersPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h" +#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" @@ -266,6 +267,19 @@ namespace MobileGL { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } + bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); + + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass( + StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass()); + + return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); + } + bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector& inputBinary, Vector& outputBinary) { using namespace spvtools; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 7917acda..6fb1abbf 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -27,6 +27,12 @@ namespace MobileGL { // Only for backends without native draw-parameter support (DirectGLES). static bool LowerDrawParametersForEssl(const Vector& inputBinary, Vector& outputBinary); + // Drops RelaxedPrecision member decorations from uniform-block structs so + // SPIRV-Cross prints the same (highp) member precision in every stage; ES + // drivers reject cross-stage uniform blocks whose member precisions differ. + // Only for the DirectGLES transpile path. + static bool StripUboMemberRelaxedPrecisionForEssl(const Vector& inputBinary, + Vector& outputBinary); // Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so // shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan // backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex, diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp new file mode 100644 index 00000000..8cc02ec1 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp @@ -0,0 +1,119 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "StripUboMemberRelaxedPrecisionPass.h" + +#include "spirv.hpp" +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_context.h" +#include "source/opt/module.h" + +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + + // Marks `typeId` and every struct type reachable through its members + // (following arrays) for decoration stripping. + void CollectStructTypes(IRContext* context, uint32_t typeId, + std::unordered_set& structTypeIds) { + Instruction* typeInst = context->get_def_use_mgr()->GetDef(typeId); + if (typeInst == nullptr) return; + + switch (typeInst->opcode()) { + case spv::Op::OpTypeStruct: { + if (!structTypeIds.insert(typeId).second) return; // already visited + for (uint32_t member = 0; member < typeInst->NumInOperands(); ++member) { + CollectStructTypes(context, typeInst->GetSingleWordInOperand(member), structTypeIds); + } + break; + } + case spv::Op::OpTypeArray: + case spv::Op::OpTypeRuntimeArray: + CollectStructTypes(context, typeInst->GetSingleWordInOperand(0), structTypeIds); + break; + default: + break; + } + } + } // namespace + + spvtools::opt::Pass::Status StripUboMemberRelaxedPrecisionPass::Process() { + auto* irContext = context(); + auto* defUseMgr = irContext->get_def_use_mgr(); + + // Uniform blocks: StorageClass Uniform variables whose pointee struct carries + // the Block decoration (BufferBlock/StorageBuffer SSBOs are left alone - they + // are not stage-matched by member precision in this pipeline's ESSL output). + std::unordered_set blockStructIds; + for (Instruction& annotation : irContext->module()->annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate) continue; + if (static_cast(annotation.GetSingleWordInOperand(1)) != spv::Decoration::Block) { + continue; + } + blockStructIds.insert(annotation.GetSingleWordInOperand(0)); + } + if (blockStructIds.empty()) return Status::SuccessWithoutChange; + + std::unordered_set structTypeIds; + for (Instruction& variable : irContext->module()->types_values()) { + if (variable.opcode() != spv::Op::OpVariable) continue; + if (static_cast(variable.GetSingleWordInOperand(0)) != + spv::StorageClass::Uniform) { + continue; + } + + Instruction* pointerType = defUseMgr->GetDef(variable.type_id()); + if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) continue; + uint32_t pointeeId = pointerType->GetSingleWordInOperand(1); + + // Instance-arrayed blocks: unwrap the array around the block struct. + Instruction* pointee = defUseMgr->GetDef(pointeeId); + while (pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeArray || + pointee->opcode() == spv::Op::OpTypeRuntimeArray)) { + pointeeId = pointee->GetSingleWordInOperand(0); + pointee = defUseMgr->GetDef(pointeeId); + } + if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) continue; + if (blockStructIds.find(pointeeId) == blockStructIds.end()) continue; + + CollectStructTypes(irContext, pointeeId, structTypeIds); + } + if (structTypeIds.empty()) return Status::SuccessWithoutChange; + + std::vector decorationsToRemove; + for (Instruction& annotation : irContext->module()->annotations()) { + if (annotation.opcode() != spv::Op::OpMemberDecorate) continue; + if (static_cast(annotation.GetSingleWordInOperand(2)) != + spv::Decoration::RelaxedPrecision) { + continue; + } + if (structTypeIds.find(annotation.GetSingleWordInOperand(0)) == structTypeIds.end()) continue; + decorationsToRemove.push_back(&annotation); + } + if (decorationsToRemove.empty()) return Status::SuccessWithoutChange; + + for (Instruction* decoration : decorationsToRemove) { + irContext->KillInst(decoration); + } + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken + StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.h new file mode 100644 index 00000000..a39c0ddb --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.h @@ -0,0 +1,44 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.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 "source/opt/pass.h" +#include "spirv-tools/optimizer.hpp" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // Removes RelaxedPrecision member decorations from every struct type reachable + // from a uniform-block variable (the block struct itself and any structs nested + // in it through members or arrays). + // + // Rationale: ESSL requires matched uniform blocks to declare members with + // identical precision in every stage, but SPIRV-Cross prints a member's + // qualifier relative to the stage's DEFAULT precision (highp in the vertex + // stage, mediump in the fragment stage). A RelaxedPrecision member therefore + // comes out as an explicit "mediump" in the vertex shader but UNQUALIFIED in + // the fragment shader - and once ForceSupporterOutput swaps the fragment + // header to "precision highp float;", that unqualified member reads back as + // highp and the ES driver refuses to link ("definitions of uniform block ... + // do not match", GL CTS KHR-GL33.shaders.uniform_block struct sub-groups). + // Dropping the hint promotes the member to highp in BOTH stages, which is + // always conformant and matches the std140 data layout either way. Only meant + // for the DirectGLES transpile path - block member precision is a per-member + // hint with no layout effect, and no other emission behavior changes. + class StripUboMemberRelaxedPrecisionPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "strip-ubo-member-relaxed-precision"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateStripUboMemberRelaxedPrecisionPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL From 9ed5dbf4836a816e54759ebabd55e210c79f4095 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 18:34:55 -0400 Subject: [PATCH 28/44] [Perf] (MG_Backend/DirectVulkan): return VkBufferResource by raw pointer from GetOrCreateResource to drop per-draw SharedPtr refcounting --- .../DirectVulkan/Renderer/VkBufferManager.cpp | 12 ++++++++---- .../DirectVulkan/Renderer/VkBufferManager.h | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 98e3d2a6..be105242 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -197,16 +197,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { return static_cast(bufferObject.GetBackendResource().get()); } - SharedPtr VkBufferManager::GetOrCreateResource( + VkBufferResource* VkBufferManager::GetOrCreateResource( const SharedPtr& bufferObject) { - auto existing = std::static_pointer_cast(bufferObject->GetBackendResource()); + // Return by raw pointer: the resource is owned for its whole lifetime by the BufferObject's + // backend-resource SharedPtr (already set, or set below), so callers that only dereference + // it avoid a static_pointer_cast + SharedPtr refcount inc/dec on every per-draw buffer bind. + const auto& existing = bufferObject->GetBackendResource(); if (existing) { - return existing; + return static_cast(existing.get()); } auto resource = MakeShared(); + VkBufferResource* raw = resource.get(); bufferObject->SetBackendResource(resource); TrackLiveResource(resource); - return resource; + return raw; } void VkBufferManager::TrackLiveResource(const SharedPtr& resource) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 0e346080..77b2f855 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -124,7 +124,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { private: Bool InitializeTransientArenas(); static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); - SharedPtr GetOrCreateResource(const SharedPtr& bufferObject); + VkBufferResource* GetOrCreateResource(const SharedPtr& bufferObject); static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject); Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags = 0); From 25323bfb8ecd8a51a65a647bd6f1d3cdea69d31e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:44:20 -0400 Subject: [PATCH 29/44] [Fix] (MG_Backend/DirectGLES, MG_Impl/GLImpl): sync GL_TEXTURE_2D_ARRAY textures to the ES backend - the target was skipped as unsupported so array textures never uploaded or bound (every KHR-GL33.pixelstoragemodes.teximage3d case failed); also keep array layer counts constant across mip levels in TexStorage3D and generated-mip storage allocation (only true 3D textures halve depth) --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 7 +++++- MobileGL/MG_Backend/DirectGLES/Managers.h | 2 +- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 25 +++++++++++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index bf3aaa7d..b02257ea 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1715,6 +1715,7 @@ namespace MobileGL::MG_Backend::DirectGLES { 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()), @@ -1790,6 +1791,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(baseSize.y())); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexStorage3D(target, static_cast(mipmapCount), glInternalFormat, static_cast(baseSize.x()), static_cast(baseSize.y()), @@ -1835,6 +1837,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(levelTexelSize.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()), @@ -1893,7 +1896,8 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(levelTexelSize.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()), @@ -1987,6 +1991,7 @@ namespace MobileGL::MG_Backend::DirectGLES { 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()), diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index c1a82b6e..c90fdbaf 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -255,7 +255,7 @@ 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) + target == TextureTarget::Texture1DArray) return false; return true; } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index d0e958ea..77f9a9e3 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -284,10 +284,16 @@ namespace MobileGL::MG_Impl::GLImpl { MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS)); } - Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) { + // Array targets store their layer count in z; layers never participate in mip + // reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level. + Bool DepthParticipatesInMipmapping(TextureTarget target) { + return target == TextureTarget::Texture3D; + } + + Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) { Int maxDimension = std::max( baseTexelSize.x(), - std::max(baseTexelSize.y(), std::max(baseTexelSize.z(), 1))); + std::max(baseTexelSize.y(), depthMips ? std::max(baseTexelSize.z(), 1) : 1)); Uint mipLevelCount = 1; while (maxDimension > 1) { maxDimension = std::max(maxDimension / 2, 1); @@ -296,11 +302,12 @@ namespace MobileGL::MG_Impl::GLImpl { return mipLevelCount; } - IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) { + IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) { return { std::max(baseTexelSize.x() >> static_cast(relativeLevel), 1), std::max(baseTexelSize.y() >> static_cast(relativeLevel), 1), - std::max(baseTexelSize.z() >> static_cast(relativeLevel), 1), + depthMips ? std::max(baseTexelSize.z() >> static_cast(relativeLevel), 1) + : std::max(baseTexelSize.z(), 1), }; } @@ -323,9 +330,10 @@ namespace MobileGL::MG_Impl::GLImpl { } const SizeT bytesPerTexel = baseByteSize / baseTexelCount; - const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize); + const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget()); + const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips); for (Uint level = 1; level < requiredLevelCount; ++level) { - const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level); + const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips); const SizeT levelByteSize = bytesPerTexel * static_cast(levelTexelSize.x()) * static_cast(levelTexelSize.y()) * static_cast(levelTexelSize.z()); @@ -2939,10 +2947,13 @@ namespace MobileGL::MG_Impl::GLImpl { auto* textureMipmapObject = static_cast(textureObject.get()); textureObject->SetInternalFormat(textureInternalFormat); + // Array targets keep their layer count constant across levels; only true 3D + // textures halve depth per level (GL 3.3 §3.9 glTexStorage3D). + const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget()); for (GLsizei level = 0; level < levels; ++level) { const GLsizei levelWidth = std::max(1, width >> level); const GLsizei levelHeight = std::max(1, height >> level); - const GLsizei levelDepth = std::max(1, depth >> level); + const GLsizei levelDepth = depthMips ? std::max(1, depth >> level) : depth; const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight, levelDepth); textureMipmapObject->AllocateStorage(textureUploadTarget, level, From 72532de78049da038cbc82f81c31bc28164a89c5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:44:36 -0400 Subject: [PATCH 30/44] [Fix] (MG_Util/Texture, MG_Util/Converters): canonical packed transfer types for RGBA4/RGB565/RGB10_A2UI - NormalizePixelFormat's default handed backends GL_UNSIGNED_BYTE (and a non-integer GL_RGB transfer format for RGB10_A2UI), so uploads read 4 bytes per texel from 2-byte packed shadow rows (rgba4 layers shifted by 2x slice stride) or were rejected outright; also map GL_RGB565 <-> TextureInternalFormat::RGB5 (the enum had no GL_RGB565 mapping at all, glTexImage* with it failed as unknown) --- .../Converters/GLToMG/TextureEnumConverter.cpp | 3 +++ .../Converters/MGToGL/TextureEnumConverter.cpp | 5 ++++- .../MG_Util/Texture/TextureFormatProcessor.cpp | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp index 42ad434f..5654b5d3 100644 --- a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp @@ -131,6 +131,9 @@ namespace MobileGL { case GL_RGB4: return TextureInternalFormat::RGB4; case GL_RGB5: + // GL_RGB565 (GL 4.1 / ARB_ES2_compatibility, used directly by the GL CTS) is the + // ES-facing rendition of the legacy RGB5 resolution. + case GL_RGB565: return TextureInternalFormat::RGB5; case GL_RGB8: return TextureInternalFormat::RGB8; diff --git a/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp index f97b37bc..bee9ac88 100644 --- a/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp @@ -113,7 +113,10 @@ namespace MobileGL { case TextureInternalFormat::RGB4: return GL_RGB4; case TextureInternalFormat::RGB5: - return GL_RGB5; + // Emit the ES-compatible GL_RGB565 rendition: desktop GL_RGB5 is not a legal + // sized internalformat on OpenGL ES backends, GL_RGB565 is (and GL 4.1+ + // accepts it too via ARB_ES2_compatibility). + return GL_RGB565; case TextureInternalFormat::RGB8: return GL_RGB8; case TextureInternalFormat::RGB8Snorm: diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index aa5fad0f..6d5bea22 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -282,12 +282,17 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { // Color sized other case GL_RGB9_E5: case GL_R11F_G11F_B10F: + case GL_RGB565: *outFormat = GL_RGB; break; case GL_RGB10_A2: case GL_RGB5_A1: + case GL_RGBA4: *outFormat = GL_RGBA; break; + case GL_RGB10_A2UI: + *outFormat = GL_RGBA_INTEGER; + break; // Depth case GL_DEPTH_COMPONENT16: @@ -459,11 +464,23 @@ 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; break; + // The shadow mip keeps these formats' packed client bytes (legacy copy path), + // so the canonical transfer type must stay the packed word — the previous + // default (GL_UNSIGNED_BYTE) made the backend read 4 bytes per texel from a + // 2-byte-per-texel shadow (KHR-GL33.pixelstoragemodes teximage rgba4/rgb565 + // sliced/garbled uploads). + case GL_RGBA4: + *outType = GL_UNSIGNED_SHORT_4_4_4_4; + break; + case GL_RGB565: + *outType = GL_UNSIGNED_SHORT_5_6_5; + break; // Depth case GL_DEPTH_COMPONENT16: From 57030b5fb9741b1282773376a5329125d7c3edd4 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:44:37 -0400 Subject: [PATCH 31/44] [Chore] (MG_Test/Texture): regression tests for 2D-array unpack subcuboid selection and TexStorage3D layer-count semantics, DirectGLES 2D-array target support, packed-format canonical transfer types, and the GL_RGB565 enum round-trip --- MobileGL/MG_Test/Texture/TextureTest.cpp | 138 +++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index ab950d29..3ca79ea3 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -11,11 +11,14 @@ #include "Includes.h" #include "Init.h" #include +#include #include #include #include #include #include +#include +#include #include #include @@ -935,6 +938,141 @@ TEST_F(TextureTest, BoundTexSubImage3DRejectsOutOfRangeLevel) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); } +// The GL CTS KHR-GL33.pixelstoragemodes.teximage3d cases upload GL_TEXTURE_2D_ARRAY +// textures through glTexImage3D with UNPACK_ROW_LENGTH / IMAGE_HEIGHT / SKIP_* set to +// extract a sub-cuboid; this mirrors that shape (scaled down) on the 2D-array target. +TEST_F(TextureTest, BoundTexImage3DOn2DArrayHonorsUnpackSubcuboidSelection) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture); + + // Source cuboid: 3x3 RGBA texels per image, 3 images; skip 1 image, 1 row, 1 pixel; + // upload the 2x2x2 sub-cuboid. Each source byte equals its own offset, so the stored + // shadow bytes must equal the offsets of the selected texels. + Uint8 pixels[3 * 3 * 3 * 4]; + for (SizeT i = 0; i < sizeof(pixels); ++i) { + pixels[i] = static_cast(i); + } + + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 3); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 3); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_ROWS, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 1); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_ROWS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 0); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_EQ(textureObject->GetTarget(), TextureTarget::Texture2DArray); + auto* mipmapObject = static_cast(textureObject.get()); + EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 0), IntVec3(2, 2, 2)); + + const auto* stored = + static_cast(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2DArray, 0)); + ASSERT_NE(stored, nullptr); + SizeT storedIndex = 0; + for (SizeT image = 1; image <= 2; ++image) { // SKIP_IMAGES = 1 + for (SizeT row = 1; row <= 2; ++row) { // SKIP_ROWS = 1 + for (SizeT column = 1; column <= 2; ++column) { // SKIP_PIXELS = 1 + const SizeT srcOffset = image * 36 + row * 12 + column * 4; + for (SizeT b = 0; b < 4; ++b, ++storedIndex) { + EXPECT_EQ(stored[storedIndex], static_cast(srcOffset + b)) << "byte " << storedIndex; + } + } + } + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The shadow mip for packed sized formats keeps the client's packed bytes, so the +// canonical transfer triple must name the packed word type; the old default fallback +// (GL_UNSIGNED_BYTE) made backends read 4 bytes per texel from a 2-byte-per-texel +// shadow (KHR-GL33.pixelstoragemodes rgba4/rgb565 uploads), and GL_RGB10_A2UI got a +// non-integer GL_RGB transfer format the driver rejects outright. +TEST_F(TextureTest, NormalizePixelFormatKeepsPackedTransferTypesForPackedSizedFormats) { + using MG_Util::TextureFormatProcessor::NormalizePixelFormat; + struct { + GLenum internalFormat; + GLenum expectedFormat; + GLenum expectedType; + } cases[] = { + {GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4}, + {GL_RGB565, GL_RGB, GL_UNSIGNED_SHORT_5_6_5}, + {GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV}, + {GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1}, + {GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV}, + }; + for (const auto& c : cases) { + GLenum outInternal = 0, outFormat = 0, outType = 0; + NormalizePixelFormat(c.internalFormat, PixelFormatNormalizeOptionBit::None, &outInternal, &outFormat, + &outType); + EXPECT_EQ(outInternal, c.internalFormat) << "internalformat 0x" << std::hex << c.internalFormat; + EXPECT_EQ(outFormat, c.expectedFormat) << "internalformat 0x" << std::hex << c.internalFormat; + EXPECT_EQ(outType, c.expectedType) << "internalformat 0x" << std::hex << c.internalFormat; + } +} + +// GL_RGB565 (ARB_ES2_compatibility / GL 4.1, used directly by the GL CTS) must round-trip +// through the internal-format enums; it had no GLToMG mapping at all, so glTexImage* with +// GL_RGB565 was rejected as an unknown internal format. +TEST_F(TextureTest, Rgb565InternalFormatRoundTripsThroughEnumConverters) { + EXPECT_EQ(MG_Util::ConvertGLEnumToTextureInternalFormat(GL_RGB565), TextureInternalFormat::RGB5); + EXPECT_EQ(MG_Util::ConvertGLEnumToTextureInternalFormat(GL_RGB5), TextureInternalFormat::RGB5); + // The ES-facing rendition of RGB5 is GL_RGB565 (desktop GL_RGB5 is not a legal sized + // internalformat on OpenGL ES backends). + EXPECT_EQ(MG_Util::ConvertTextureInternalFormatToGLEnum(TextureInternalFormat::RGB5), + static_cast(GL_RGB565)); +} + +// Regression guard: the DirectGLES backend must treat GL_TEXTURE_2D_ARRAY as a +// syncable target — it used to be skipped entirely, so 2D-array textures were never +// uploaded or bound (KHR-GL33.pixelstoragemodes.teximage3d.* failed wholesale). +TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) { + using MobileGL::MG_Backend::DirectGLES::TextureImpl::IsSupportedTextureTarget; + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2DArray)); + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture3D)); + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2D)); + EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::Texture1D)); + EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::Texture1DArray)); + EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::TextureRectangle)); +} + +// 2D-array textures keep their layer count constant across mip levels (GL 3.3 §3.9); +// only true 3D textures halve depth per level. +TEST_F(TextureTest, TexStorage3DOn2DArrayKeepsLayerCountAcrossLevels) { + GLuint arrayTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &arrayTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, arrayTexture); + MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_2D_ARRAY, 3, GL_RGBA8, 8, 8, 4); + + const auto arrayObject = MG_State::pGLContext->GetTextureObject(arrayTexture); + ASSERT_NE(arrayObject, nullptr); + auto* arrayMipmapObject = static_cast(arrayObject.get()); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 0), IntVec3(8, 8, 4)); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 1), IntVec3(4, 4, 4)); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 2), IntVec3(2, 2, 4)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Control: a real 3D texture still halves its depth per level. + GLuint volumeTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &volumeTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, volumeTexture); + MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_3D, 3, GL_RGBA8, 8, 8, 4); + + const auto volumeObject = MG_State::pGLContext->GetTextureObject(volumeTexture); + ASSERT_NE(volumeObject, nullptr); + auto* volumeMipmapObject = static_cast(volumeObject.get()); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 0), IntVec3(8, 8, 4)); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 1), IntVec3(4, 4, 2)); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 2), IntVec3(2, 2, 1)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, NamedTextureVectorParametersAndGettersWorkWithoutBinding) { GLuint texture = 0; MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); From 56db115a1a77bb83e440c78d713eafd3146c7f3f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:10:11 -0400 Subject: [PATCH 32/44] [Fix] (MG_Impl/GLImpl): clamp glClearDepth to [0,1] per GL 3.3 (Vulkan clear values require it) --- MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp index 54d0557b..aa1fe5a1 100644 --- a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp +++ b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp @@ -649,7 +649,9 @@ namespace MobileGL::MG_Impl::GLImpl { } void ClearDepth_State(GLclampd depth) { - MG_State::pGLContext->SetClearDepth(static_cast(depth)); + // GL 3.3 §4.2.3: the clear depth is clamped to [0,1] at specification time (Vulkan clear + // values additionally require it: VUID-VkClearDepthStencilValue-depth-00022). + MG_State::pGLContext->SetClearDepth(ClampUnitFloat(static_cast(depth))); } void ClearColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) { From eb5b4bca3ef6ca93e9f8a04afe2f2ed63aa66521 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:10:46 -0400 Subject: [PATCH 33/44] [Fix] (MG_Backend/DirectVulkan): ignore glClear/glClearBuffer* while GL_RASTERIZER_DISCARD is enabled, and early-out on an empty clear mask --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index ef48452e..f9d1d46e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3586,6 +3586,13 @@ void main() { void VulkanRenderer::Clear(GLbitfield mask) { m_clearManager->CollectGarbage(); + if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { + return; + } + // GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored. + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) { + return; + } auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); MOBILEGL_ASSERT(fbo, "VulkanRenderer::Clear: draw framebuffer not found (fbo == nullptr)"); if (IsUnsupportedFramebufferForDirectVulkan(*fbo)) { @@ -3717,6 +3724,10 @@ void main() { const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { m_clearManager->CollectGarbage(); + // GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored. + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) { + return; + } if (IsUnsupportedFramebufferForDirectVulkan(framebuffer)) { RecordUnsupportedFramebufferError(__func__); return; From 4172959e492adfd83230c359804cdd9acd00e436 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:11:13 -0400 Subject: [PATCH 34/44] [Fix] (MG_Backend/DirectVulkan): honor the front stencil write mask in scissored glClear and drop a redundant compatibility re-check --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index f9d1d46e..cf365635 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3628,7 +3628,9 @@ void main() { return; } - if (activeRenderPass && activeRenderPass->CompatibleWith(*renderPassEntry)) { + // A still-active pass is necessarily compatible here: the block above ended any + // incompatible one and nothing since can change the active pass. + if (activeRenderPass) { // Materialize any older whole-attachment clear before applying this // ordered, scissored clear. ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); @@ -3698,7 +3700,16 @@ void main() { if ((mask & GL_STENCIL_BUFFER_BIT) != 0) { const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil); if (stencilAttachment.IsComplete()) { - depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + // GL 3.3 §4.2.3: the clear is masked by the front stencil write mask. + // vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or + // zero mask can be expressed; treat a partial mask like a partial color mask. + const Uint32 stencilWriteMask = + MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask; + if ((stencilWriteMask & 0xFFu) == 0xFFu) { + depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + } else if (stencilWriteMask != 0) { + MGLOG_W("DirectVulkan: scissored glClear with a partial stencil write mask is not supported"); + } } } if (depthStencilAspects != 0) { From 5184901a5bfe149119593809e4ac71224812eb12 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:12:03 -0400 Subject: [PATCH 35/44] [Fix] (MG_Backend/DirectVulkan): clear every layer of layered framebuffers in vkCmdClearAttachments paths (rename the never-read RenderPassEntry::subpass to layers) --- .../DirectVulkan/Renderer/VkRenderPassManager.cpp | 2 +- .../DirectVulkan/Renderer/VkRenderPassManager.h | 9 +++++---- .../MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp | 6 ++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 54cdc7db..143e2412 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -1015,7 +1015,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { hasDepthStencilAttachment, renderPassSampleCount, extent, - static_cast(framebufferLayers) }; + framebufferLayers }; MGLOG_D("VkRenderPassManager::GetOrCreateRenderPass: hash=0x%llx compatibilityHash=0x%llx attachmentCount=%u colorAttachmentCount=%u samples=%d extent=%dx%d", static_cast(hash), static_cast(compatibilityHash), diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index 9fe0a299..7364dc46 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -68,7 +68,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool hasDepthStencilAttachment = false; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; IntVec2 extent = {0, 0}; - Uint32 subpass = 0; + // VkFramebufferCreateInfo::layers of the entry's framebuffer (>1 for layered GL attachments). + Uint32 layers = 1; RenderPassEntry() = default; RenderPassEntry(const RenderPassEntry&) = delete; @@ -84,7 +85,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment); std::swap(sampleCount, that.sampleCount); std::swap(extent, that.extent); - std::swap(subpass, that.subpass); + std::swap(layers, that.layers); } RenderPassEntry( Uint64 hash, @@ -97,7 +98,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 colorAttachmentCount, Bool hasDepthStencilAttachment, VkSampleCountFlagBits sampleCount, - IntVec2 extent, int subpass): + IntVec2 extent, Uint32 layers): hash(hash), renderPass(renderpass), framebuffer(framebuffer), @@ -109,7 +110,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { hasDepthStencilAttachment(hasDepthStencilAttachment), sampleCount(sampleCount), extent(extent), - subpass(subpass) + layers(layers) {} ~RenderPassEntry() { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index cf365635..a10f7fd6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3646,7 +3646,8 @@ void main() { m_swapchainObject.GetPreTransform()) : MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent); clearRect.baseArrayLayer = 0; - clearRect.layerCount = 1; + // GL 3.3 §4.4.7: clearing a layered framebuffer clears every layer. + clearRect.layerCount = renderPassEntry->layers; if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) { return; } @@ -6885,7 +6886,8 @@ void main() { static_cast(activeRenderPass->extent.y()) }; clearRect.baseArrayLayer = 0; - clearRect.layerCount = 1; + // Compatible entries share the framebuffer layer count; layered attachments clear every layer. + clearRect.layerCount = compatibleRenderPassEntry.layers; for (const auto& pending : compatibleRenderPassEntry.pendingClearAttachments) { if (!pending.hasInlinePayload && pending.key.texture == nullptr) { From 37ef2cb600af2c7534f15466e501dbf487c4351c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:13:25 -0400 Subject: [PATCH 36/44] [Fix] (MG_Backend/DirectVulkan): materialize mid-pass pending color clears at the subpass color slot index, not the compacted description index --- .../MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp | 1 + .../MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h | 5 +++++ MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 143e2412..9a6ca4ce 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -718,6 +718,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (hasClear) { pendingClearAttachments.emplace_back(PendingClearAttachmentInfo { .attachmentIndex = attachmentIndex, + .colorAttachmentSlot = i, .key = VkClearManager::MakePendingClearKey(att) }); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index 7364dc46..7fe370a6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -27,7 +27,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; struct PendingClearAttachmentInfo { + // Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space). Uint32 attachmentIndex = 0; + // Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL + // draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete. + // Only meaningful for color clears. + Uint32 colorAttachmentSlot = 0; PendingClearKey key{}; MG_State::GLState::RenderbufferObject* renderbuffer = nullptr; Bool hasInlinePayload = false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index a10f7fd6..973cfdc7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -6908,7 +6908,9 @@ void main() { clearAttachment.clearValue.depthStencil = {1.0f, 0}; if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) { clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - clearAttachment.colorAttachment = pending.attachmentIndex; + // VkClearAttachment::colorAttachment indexes the subpass pColorAttachments (draw-buffer + // slot space, with UNUSED holes), not the compacted attachment descriptions. + clearAttachment.colorAttachment = pending.colorAttachmentSlot; clearAttachment.clearValue.color = { clearPayload.color.x(), clearPayload.color.y(), From 5bd8fa8c4e037b9760cd8add07381f9875d15a63 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:15:23 -0400 Subject: [PATCH 37/44] [Perf] (MG_Backend/DirectVulkan): route full-coverage scissored glClear back to the deferred loadOp path, skip render-pass churn for no-op clears, and drop the per-clear heap Vector (extracted PrepareScissoredClear) --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 248 ++++++++++-------- .../DirectVulkan/Renderer/VulkanRenderer.h | 8 + 2 files changed, 145 insertions(+), 111 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 973cfdc7..4d21757c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3584,6 +3584,64 @@ void main() { 1, &memoryBarrier, 0, nullptr, 0, nullptr); } + VulkanRenderer::ScissoredClearPrep VulkanRenderer::PrepareScissoredClear( + const MG_State::GLState::FramebufferObject& framebuffer, VkClearRect& outClearRect) { + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) { + m_frameContext.BeginCommandRecording(); + } + + auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); + auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired); + if (renderPassEntry->attachmentCount == 0 || + renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { + return ScissoredClearPrep::NoOp; + } + + VkClearRect clearRect{}; + clearRect.rect = framebuffer.IsDefaultFramebuffer() + ? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(), + renderPassEntry->extent, + m_swapchainObject.GetPreTransform()) + : MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent); + clearRect.baseArrayLayer = 0; + // GL 3.3 §4.4.7: clearing a layered framebuffer clears every layer. + clearRect.layerCount = renderPassEntry->layers; + if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) { + return ScissoredClearPrep::NoOp; + } + // A scissor that covers the whole target is a whole-surface clear; the deferred loadOp + // path is equivalent and cheaper (no render pass churn, loadOp=CLEAR on tilers). + if (clearRect.rect.offset.x == 0 && clearRect.rect.offset.y == 0 && + clearRect.rect.extent.width == static_cast(renderPassEntry->extent.x()) && + clearRect.rect.extent.height == static_cast(renderPassEntry->extent.y())) { + return ScissoredClearPrep::NotNeeded; + } + + if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + activeRenderPass = nullptr; + // Re-resolve: ending the pass updates tracked attachment layouts, which feed the + // entry's load ops and initial layouts. + renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired); + } + // A still-active pass is necessarily compatible here: the block above ended any + // incompatible one and nothing since can change the active pass. + if (activeRenderPass) { + // Materialize any older whole-attachment clear before applying this + // ordered, scissored clear. + ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); + } else { + const Bool began = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry); + MOBILEGL_ASSERT(began, "%s: BeginRenderPass failed", __func__); + if (!began) { + return ScissoredClearPrep::NoOp; + } + } + outClearRect = clearRect; + return ScissoredClearPrep::Ready; + } + void VulkanRenderer::Clear(GLbitfield mask) { m_clearManager->CollectGarbage(); if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { @@ -3611,121 +3669,89 @@ void main() { // GuiItemAtlas: animated items clear only their atlas slot before being // redrawn. Queueing that clear as a loadOp erases every cached static item. if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { - auto& frame = m_frameContext.GetCurrent(); - if (!frame.isCommandRecording) { - m_frameContext.BeginCommandRecording(); - } - - auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); - auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*fbo, m_imageIndexAcquired); - if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { - VkRenderPassManager::EndRenderPass(frame.commandBuffer); - activeRenderPass = nullptr; - renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*fbo, m_imageIndexAcquired); - } - if (renderPassEntry->attachmentCount == 0 || - renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { - return; - } - - // A still-active pass is necessarily compatible here: the block above ended any - // incompatible one and nothing since can change the active pass. - if (activeRenderPass) { - // Materialize any older whole-attachment clear before applying this - // ordered, scissored clear. - ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); - } else { - const Bool began = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry); - MOBILEGL_ASSERT(began, "%s: BeginRenderPass failed", __func__); - } - VkClearRect clearRect{}; - clearRect.rect = fbo->IsDefaultFramebuffer() - ? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(), - renderPassEntry->extent, - m_swapchainObject.GetPreTransform()) - : MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent); - clearRect.baseArrayLayer = 0; - // GL 3.3 §4.4.7: clearing a layered framebuffer clears every layer. - clearRect.layerCount = renderPassEntry->layers; - if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) { + switch (PrepareScissoredClear(*fbo, clearRect)) { + case ScissoredClearPrep::NoOp: + return; + case ScissoredClearPrep::NotNeeded: + break; // full-coverage scissor: the deferred whole-surface path below is equivalent + case ScissoredClearPrep::Ready: { + VkClearAttachment clearAttachments[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS + 1]; + Uint32 clearAttachmentCount = 0; + + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + const auto& drawBuffers = fbo->GetDrawBuffers(); + for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) { + const auto attachmentType = drawBuffers[drawBufferIndex]; + if (attachmentType == FramebufferAttachmentType::None) { + continue; + } + const auto& attachment = fbo->GetAttachment(attachmentType); + if (!attachment.IsComplete()) { + continue; + } + + const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex); + if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { + continue; + } + if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { + MGLOG_W("DirectVulkan: scissored glClear with a partial color mask is not supported"); + continue; + } + + MG_State::GLState::ITextureObject* colorTexture = nullptr; + if (attachment.IsTexture()) { + colorTexture = attachment.GetTexture().get(); + } + VkClearAttachment clearAttachment{}; + clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clearAttachment.colorAttachment = drawBufferIndex; + clearAttachment.clearValue.color = { + payload.color.x(), payload.color.y(), payload.color.z(), + ResolveColorClearAlpha(colorTexture, payload.color.w()) + }; + clearAttachments[clearAttachmentCount++] = clearAttachment; + } + } + + VkImageAspectFlags depthStencilAspects = 0; + if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) { + const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth); + if (depthAttachment.IsComplete()) { + depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT; + } + } + if ((mask & GL_STENCIL_BUFFER_BIT) != 0) { + const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil); + if (stencilAttachment.IsComplete()) { + // GL 3.3 §4.2.3: the clear is masked by the front stencil write mask. + // vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or + // zero mask can be expressed; treat a partial mask like a partial color mask. + const Uint32 stencilWriteMask = + MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask; + if ((stencilWriteMask & 0xFFu) == 0xFFu) { + depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + } else if (stencilWriteMask != 0) { + MGLOG_W("DirectVulkan: scissored glClear with a partial stencil write mask is not supported"); + } + } + } + if (depthStencilAspects != 0) { + VkClearAttachment clearAttachment{}; + clearAttachment.aspectMask = depthStencilAspects; + clearAttachment.clearValue.depthStencil = {payload.depth, payload.stencil}; + clearAttachments[clearAttachmentCount++] = clearAttachment; + } + + if (clearAttachmentCount != 0) { + vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer, + clearAttachmentCount, clearAttachments, + 1, &clearRect); + } return; } - - Vector clearAttachments; - clearAttachments.reserve(fbo->GetDrawBuffers().size() + 1); - - if ((mask & GL_COLOR_BUFFER_BIT) != 0) { - const auto& drawBuffers = fbo->GetDrawBuffers(); - for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) { - const auto attachmentType = drawBuffers[drawBufferIndex]; - if (attachmentType == FramebufferAttachmentType::None) { - continue; - } - const auto& attachment = fbo->GetAttachment(attachmentType); - if (!attachment.IsComplete()) { - continue; - } - - const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex); - if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { - continue; - } - if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { - MGLOG_W("DirectVulkan: scissored glClear with a partial color mask is not supported"); - continue; - } - - MG_State::GLState::ITextureObject* colorTexture = nullptr; - if (attachment.IsTexture()) { - colorTexture = attachment.GetTexture().get(); - } - VkClearAttachment clearAttachment{}; - clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - clearAttachment.colorAttachment = drawBufferIndex; - clearAttachment.clearValue.color = { - payload.color.x(), payload.color.y(), payload.color.z(), - ResolveColorClearAlpha(colorTexture, payload.color.w()) - }; - clearAttachments.push_back(clearAttachment); - } } - - VkImageAspectFlags depthStencilAspects = 0; - if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) { - const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth); - if (depthAttachment.IsComplete()) { - depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT; - } - } - if ((mask & GL_STENCIL_BUFFER_BIT) != 0) { - const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil); - if (stencilAttachment.IsComplete()) { - // GL 3.3 §4.2.3: the clear is masked by the front stencil write mask. - // vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or - // zero mask can be expressed; treat a partial mask like a partial color mask. - const Uint32 stencilWriteMask = - MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask; - if ((stencilWriteMask & 0xFFu) == 0xFFu) { - depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; - } else if (stencilWriteMask != 0) { - MGLOG_W("DirectVulkan: scissored glClear with a partial stencil write mask is not supported"); - } - } - } - if (depthStencilAspects != 0) { - VkClearAttachment clearAttachment{}; - clearAttachment.aspectMask = depthStencilAspects; - clearAttachment.clearValue.depthStencil = {payload.depth, payload.stencil}; - clearAttachments.push_back(clearAttachment); - } - - if (!clearAttachments.empty()) { - vkCmdClearAttachments(frame.commandBuffer, - static_cast(clearAttachments.size()), clearAttachments.data(), - 1, &clearRect); - } - return; } m_clearManager->QueueClear(mask, payload, *fbo); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 0e657099..91fa40bd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -131,6 +131,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer, const RenderPassEntry& compatibleRenderPassEntry); + enum class ScissoredClearPrep { + NotNeeded, // scissor covers the whole target — take the deferred whole-surface path instead + NoOp, // nothing to clear (degenerate target or empty scissor rect) + Ready, // a render pass is active; record vkCmdClearAttachments with the returned rect + }; + ScissoredClearPrep PrepareScissoredClear(const MG_State::GLState::FramebufferObject& framebuffer, + VkClearRect& outClearRect); + void Clear(GLbitfield mask); void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil); void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value); From 80268385632b9f9751bcaf0abc738e175623202c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 19:17:41 -0400 Subject: [PATCH 38/44] [Fix] (MG_Backend/DirectVulkan): honor scissor in glClearBuffer*/glClearNamedFramebuffer* and clamp their depth clear values to [0,1] --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 139 +++++++++++++++--- .../DirectVulkan/Renderer/VulkanRenderer.h | 4 + 2 files changed, 119 insertions(+), 24 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 4d21757c..a82a62e1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -21,6 +21,7 @@ #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Metrics/TextureMetrics.h" #include +#include #include #include #include @@ -3771,6 +3772,53 @@ void main() { return; } + // Validate (buffer, drawbuffer) up front so GL errors fire regardless of which clear + // path is taken below. + switch (buffer) { + case GL_COLOR: + if (drawbuffer < 0 || + drawbuffer >= static_cast(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) { + RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range"); + return; + } + break; + case GL_DEPTH: + if (drawbuffer != 0) { + RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0"); + return; + } + break; + case GL_STENCIL: + if (drawbuffer != 0) { + RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0"); + return; + } + break; + case GL_DEPTH_STENCIL: + if (drawbuffer != 0) { + RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0"); + return; + } + break; + default: + RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target"); + return; + } + + // GL 3.3 §4.2.3: ClearBuffer* is clipped by GL_SCISSOR_TEST exactly like Clear. + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { + VkClearRect clearRect{}; + switch (PrepareScissoredClear(framebuffer, clearRect)) { + case ScissoredClearPrep::NoOp: + return; + case ScissoredClearPrep::NotNeeded: + break; // full-coverage scissor: the deferred whole-surface path below is equivalent + case ScissoredClearPrep::Ready: + RecordScissoredClearBuffer(framebuffer, buffer, drawbuffer, clearPayload, clearRect); + return; + } + } + auto queueAttachmentClear = [&](FramebufferAttachmentType attachmentType) { if (attachmentType == FramebufferAttachmentType::None) { return; @@ -3787,43 +3835,84 @@ void main() { }; switch (buffer) { - case GL_COLOR: { - if (drawbuffer < 0 || - drawbuffer >= static_cast(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) { - RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range"); - return; - } + case GL_COLOR: queueAttachmentClear(framebuffer.GetDrawBuffers()[drawbuffer]); return; - } case GL_DEPTH: - if (drawbuffer != 0) { - RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0"); - return; - } queueAttachmentClear(FramebufferAttachmentType::Depth); return; case GL_STENCIL: - if (drawbuffer != 0) { - RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0"); - return; - } queueAttachmentClear(FramebufferAttachmentType::Stencil); return; case GL_DEPTH_STENCIL: - if (drawbuffer != 0) { - RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0"); - return; - } queueAttachmentClear(FramebufferAttachmentType::Depth); queueAttachmentClear(FramebufferAttachmentType::Stencil); return; default: - RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target"); return; } } + void VulkanRenderer::RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer, + GLenum buffer, GLint drawbuffer, + const ClearAttachmentPayload& clearPayload, + const VkClearRect& clearRect) { + VkClearAttachment clearAttachment{}; + + if (buffer == GL_COLOR) { + const auto attachmentType = framebuffer.GetDrawBuffers()[drawbuffer]; + if (attachmentType == FramebufferAttachmentType::None) { + return; + } + const auto& attachment = framebuffer.GetAttachment(attachmentType); + if (!attachment.IsComplete()) { + return; + } + const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast(drawbuffer)); + if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { + return; + } + if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { + MGLOG_W("DirectVulkan: scissored glClearBuffer with a partial color mask is not supported"); + return; + } + MG_State::GLState::ITextureObject* colorTexture = nullptr; + if (attachment.IsTexture()) { + colorTexture = attachment.GetTexture().get(); + } + clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clearAttachment.colorAttachment = static_cast(drawbuffer); + clearAttachment.clearValue.color = { + clearPayload.color.x(), clearPayload.color.y(), clearPayload.color.z(), + ResolveColorClearAlpha(colorTexture, clearPayload.color.w()) + }; + } else { + VkImageAspectFlags aspects = 0; + if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask() && + framebuffer.GetAttachment(FramebufferAttachmentType::Depth).IsComplete()) { + aspects |= VK_IMAGE_ASPECT_DEPTH_BIT; + } + if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0 && + framebuffer.GetAttachment(FramebufferAttachmentType::Stencil).IsComplete()) { + // GL 3.3 §4.2.3: the clear is masked by the front stencil write mask (see Clear). + const Uint32 stencilWriteMask = + MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask; + if ((stencilWriteMask & 0xFFu) == 0xFFu) { + aspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + } else if (stencilWriteMask != 0) { + MGLOG_W("DirectVulkan: scissored glClearBuffer with a partial stencil write mask is not supported"); + } + } + if (aspects == 0) { + return; + } + clearAttachment.aspectMask = aspects; + clearAttachment.clearValue.depthStencil = {clearPayload.depth, clearPayload.stencil}; + } + + vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer, 1, &clearAttachment, 1, &clearRect); + } + void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); @@ -3836,7 +3925,8 @@ void main() { void VulkanRenderer::ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { ClearAttachmentPayload payload{}; payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; - payload.depth = depth; + // Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022). + payload.depth = std::clamp(depth, 0.0f, 1.0f); payload.stencil = static_cast(stencil); QueueClearBufferPayload(buffer, drawbuffer, payload); } @@ -3853,7 +3943,7 @@ void main() { break; case GL_DEPTH: payload.mask = GL_DEPTH_BUFFER_BIT; - payload.depth = value[0]; + payload.depth = std::clamp(value[0], 0.0f, 1.0f); break; default: break; @@ -3875,7 +3965,7 @@ void main() { break; case GL_DEPTH: payload.mask = GL_DEPTH_BUFFER_BIT; - payload.depth = value[0]; + payload.depth = std::clamp(value[0], 0.0f, 1.0f); break; default: break; @@ -3891,7 +3981,8 @@ void main() { } ClearAttachmentPayload payload{}; payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; - payload.depth = depth; + // Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022). + payload.depth = std::clamp(depth, 0.0f, 1.0f); payload.stencil = static_cast(stencil); QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 91fa40bd..b64de3b2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -283,6 +283,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload); + void RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer, + GLenum buffer, GLint drawbuffer, + const ClearAttachmentPayload& clearPayload, + const VkClearRect& clearRect); // ---- Submission fence tracking (GL sync objects) ---- // One record per vkQueueSubmit still in flight, in ascending submit From f61675e9ce3cd22cf9d32613d5e5a52a0ef30ea2 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 09:40:21 -0400 Subject: [PATCH 39/44] [Fix] (MG_Impl/Texture): validate the bound texture before dereferencing it in TexSubImage2D, and stop recording an error when glDeleteTextures is handed unknown names --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 9 +- .../GLState/TextureState/TextureState.cpp | 3 + MobileGL/MG_Test/Texture/TextureTest.cpp | 143 ++++++++++++++++++ 3 files changed, 151 insertions(+), 4 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 77f9a9e3..676dd0e2 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -887,11 +887,11 @@ namespace MobileGL::MG_Impl::GLImpl { auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& textureObject = bindingSlot.GetBoundObject(); + if (!TextureImpl::ValidateTextureObject(textureObject)) return; TextureInternalFormat textureInternalFormat = textureObject->GetFormat(); MGLOG_D("%s: working on texture %d", __func__, textureObject->GetExternalIndex()); // ===================== Error Checking ============================== - if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width, yoffset, height)) return; if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureInternalFormat, texturePixelDataType)) @@ -2329,7 +2329,7 @@ namespace MobileGL::MG_Impl::GLImpl { for (SizeT i = 0; i < static_cast(n); ++i) { Uint textureName = textures[i]; if (textureName == 0) continue; - if (!TextureImpl::ValidateTextureName(textureName, true)) continue; + if (!MG_State::pGLContext->ValidateTextureName(textureName)) continue; MG_State::pGLContext->MarkTextureObjectForDeletion(textureName); } } @@ -2552,6 +2552,9 @@ namespace MobileGL::MG_Impl::GLImpl { return; } + // GL 3.3 core 3.8.1: a name that GenTextures never returned - or that has since been deleted - + // is not a legal bind target in the core profile (no application-generated names), and the error + // is INVALID_OPERATION, not INVALID_VALUE. if (!MG_State::pGLContext->ValidateTextureName(texture)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -2559,8 +2562,6 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - if (!TextureImpl::ValidateTextureName(texture, true)) return; - // ======================= Processing ================================ Bool doesTextureExist = MG_State::pGLContext->ValidateTextureObject(texture); if (!doesTextureExist) { diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index 31617504..b2bfc78f 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -110,6 +110,9 @@ namespace MobileGL::MG_State::GLState { BumpTextureBindGeneration(); m_textureObjects.erase(index); } + // Release the name itself even when GenTextures only reserved it and no bind ever + // instantiated an object: GL 3.3 core 3.8.1 makes a deleted name unused again (so a + // later bind of it must fail), and the reservation has to return to the free list. m_indexGenerator.Delete(index); } } diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 3ca79ea3..e14afdac 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -8,6 +8,8 @@ #include +#include + #include "Includes.h" #include "Init.h" #include @@ -134,6 +136,147 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(TextureTest, GenThenBindCreatesObjectForUnsizedPackedBgraSubImageUpload) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + ASSERT_NE(texture, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateTextureName(texture)); + // GenTextures only reserves the name; the object appears on first bind. + ASSERT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_FALSE); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_TRUE(MG_State::pGLContext->ValidateTextureObject(texture)); + EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_TRUE); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 1, 0, GL_BGRA, + GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); + const Uint8 pixels[] = { + 10, 20, 30, 40, + 50, 60, 70, 80, + }; + MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_BGRA, + GL_UNSIGNED_INT_8_8_8_8_REV, pixels); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + const Uint8 expected[] = { + 30, 20, 10, 40, + 70, 60, 50, 80, + }; + EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// GL 3.3 core 3.8.1: DeleteTextures makes the name unused again whether or not a bind ever +// instantiated an object, so the reservation must go back to the generator's free list rather +// than leaking, and binding the dead name afterwards must fail. +TEST_F(TextureTest, DeleteGeneratedButUnboundNameReleasesReservationAndBindFails) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + ASSERT_NE(texture, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateTextureName(texture)); + ASSERT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + + MG_Impl::GLImpl::DeleteTextures(1, &texture); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(texture)); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_FALSE); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + + // The freed reservation is recycled (the generator's free list is LIFO, so the very same + // name comes back) - a delete that skipped the release would hand out a fresh name here. + GLuint recycled = 0; + MG_Impl::GLImpl::GenTextures(1, &recycled); + EXPECT_EQ(recycled, texture); + EXPECT_TRUE(MG_State::pGLContext->ValidateTextureName(recycled)); +} + +TEST_F(TextureTest, DeleteInstantiatedTextureInvalidatesNameUntilRegenerated) { + GLuint textures[2] = {}; + MG_Impl::GLImpl::GenTextures(2, textures); + ASSERT_NE(textures[0], 0u); + ASSERT_NE(textures[1], 0u); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[0]); + ASSERT_TRUE(MG_State::pGLContext->ValidateTextureObject(textures[0])); + MG_Impl::GLImpl::DeleteTextures(1, &textures[0]); + + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(textures[0])); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(textures[0])); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[1]); + const auto fallbackObject = MG_State::pGLContext->GetTextureObject(textures[1]); + ASSERT_NE(fallbackObject, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[0]); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + fallbackObject); +} + +TEST_F(TextureTest, DeleteUnknownNamesIsSilentButBindUnknownNameIsInvalid) { + GLuint validTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &validTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, validTexture); + const auto boundObject = MG_State::pGLContext->GetTextureObject(validTexture); + ASSERT_NE(boundObject, nullptr); + + constexpr GLuint unknownNames[] = {0, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteTextures(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, unknownNames[1]); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + boundObject); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(unknownNames[1])); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(unknownNames[1])); +} + +TEST_F(TextureTest, BindTextureUnitEnumAsNameIsSilentNoOp) { + GLuint validTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &validTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, validTexture); + const auto boundObject = MG_State::pGLContext->GetTextureObject(validTexture); + ASSERT_NE(boundObject, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + constexpr GLuint textureUnitEnum = GL_TEXTURE7; + ASSERT_FALSE(MG_State::pGLContext->ValidateTextureName(textureUnitEnum)); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textureUnitEnum); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + boundObject); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(textureUnitEnum)); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(textureUnitEnum)); +} + +TEST_F(TextureTest, TexSubImage2DWithoutBoundTextureReportsErrorInsteadOfDereferencingNull) { + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint8 pixel[] = {1, 2, 3, 4}; + MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); +} + TEST_F(TextureTest, TextureStorageAndSubImageModifyNamedObjectOnly) { GLuint namedTexture = 0; GLuint boundTexture = 0; From 346cd417ca8f6cd9f55adbeb0d25d976007463c7 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 12:26:38 -0400 Subject: [PATCH 40/44] [Fix] (MG_Backend/DirectVulkan): fix ERROR-level vertex stream build --- MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index a82a62e1..84af9d4d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2151,8 +2151,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); + "programHash=%llu location=%u type=0x%x", + static_cast(programObj.hash), location, glType); return false; } From 5d6b54402167a33d5d16b4b073b543cda6b9f734 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 12:28:05 -0400 Subject: [PATCH 41/44] [Fix] (MG_Impl/Texture): support anisotropic sampler parameters --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 17 +++ .../Renderer/VkSamplerManager.cpp | 5 + .../MG_Impl/GLImpl/Sampler/GL_Sampler.cpp | 41 +++++- .../MG_Impl/GLImpl/Sampler/Validators.cpp | 20 +++ .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 44 ++++++- .../GLState/SamplerState/SamplerObject.cpp | 11 ++ .../GLState/SamplerState/SamplerObject.h | 3 + .../BackendLoader/BackendLoaderTest.cpp | 28 +++- MobileGL/MG_Test/Texture/TextureTest.cpp | 122 ++++++++++++++++++ .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 3 + .../MG_Util/BackendLoaders/OpenGL/Loader.h | 3 + 11 files changed, 284 insertions(+), 13 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index b02257ea..80bdad4b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2159,6 +2159,16 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_LOD, samplerParams.maxLod); m_cacheSamplerParameters.maxLod = samplerParams.maxLod; } + if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) { + if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) { + g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, + samplerParams.maxAnisotropy); + } + // Unsupported GLES backends intentionally treat anisotropy as a + // frontend-only no-op; remember the observed value so the cache + // remains coherent without issuing an illegal enum every sync. + m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; + } DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); @@ -3040,6 +3050,13 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_LOD, samplerParams.maxLod); m_cacheSamplerParameters.maxLod = samplerParams.maxLod; } + if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) { + if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) { + g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_ANISOTROPY_EXT, + samplerParams.maxAnisotropy); + } + m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; + } #undef SYNC_SAMPLER_PARAM_IF_CHANGED m_isInitialized = true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index 495e75af..2d837a92 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -99,6 +99,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod))); const auto lodBias = sampler.GetLodBias(); XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias))); + // Anisotropy is currently an accepted frontend-only state on DirectVulkan. + // Keep it out of the key so changing this no-op does not manufacture duplicate + // VkSamplers while sampler versioning still exposes the new frontend value. const auto compareMode = sampler.GetCompareMode(); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); const auto compareFunc = ResolveCompareFunc(sampler, texture); @@ -125,6 +128,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT()); samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR()); samplerInfo.mipLodBias = sampler.GetLodBias(); + // DirectVulkan does not yet plumb samplerAnisotropy feature/limit discovery; + // preserve the accepted frontend state without requesting an unsupported feature. samplerInfo.anisotropyEnable = VK_FALSE; samplerInfo.maxAnisotropy = 1.0f; samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE; diff --git a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp index ceec7248..0f31503e 100644 --- a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp @@ -14,7 +14,13 @@ namespace MobileGL::MG_Impl::GLImpl { namespace { - Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isInteger) { + Float ReadSamplerScalar(const void* param, Bool isFloat, Bool isUnsignedInteger) { + if (isFloat) return *(const GLfloat*)param; + if (isUnsignedInteger) return static_cast(*(const GLuint*)param); + return static_cast(*(const GLint*)param); + } + + Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) { if (param == nullptr) return false; switch (pname) { @@ -22,6 +28,13 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_MAX_LOD: case GL_TEXTURE_LOD_BIAS: return true; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (ReadSamplerScalar(param, isFloat, isUnsignedInteger) >= 1.0f) return true; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", "SetSamplerParam_State", + "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0.")); + return false; default: break; } @@ -29,14 +42,15 @@ namespace MobileGL::MG_Impl::GLImpl { if (isFloat) { return SamplerImpl::ValidateSamplerFloatParam(pname, *(const GLfloat*)param); } - if (isInteger) { + if (isUnsignedInteger) { return SamplerImpl::ValidateSamplerIntParam(pname, static_cast(*(const GLuint*)param)); } return SamplerImpl::ValidateSamplerIntParam(pname, *(const GLint*)param); } } // namespace - void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, bool isInteger) { + void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, + bool isUnsignedInteger) { if (param == nullptr) return; if (!SamplerImpl::ValidateSamplerName(sampler)) return; @@ -47,7 +61,7 @@ namespace MobileGL::MG_Impl::GLImpl { } auto& samplerObj = MG_State::pGLContext->GetSamplerObject(sampler); if (!SamplerImpl::ValidateSamplerObject(sampler)) return; - if (!ValidateSamplerParameterValue(pname, param, isFloat, isInteger)) return; + if (!ValidateSamplerParameterValue(pname, param, isFloat, isUnsignedInteger)) return; using namespace MG_Util; switch (pname) { @@ -76,6 +90,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: samplerObj->SetLodBias(*(const GLfloat*)param); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + samplerObj->SetMaxAnisotropy(ReadSamplerScalar(param, isFloat, isUnsignedInteger)); + break; case GL_TEXTURE_COMPARE_MODE: samplerObj->SetCompareMode(MG_Util::ConvertGLEnumToSamplerCompareMode(*(const GLint*)param)); break; @@ -89,7 +106,8 @@ namespace MobileGL::MG_Impl::GLImpl { } } - void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, bool isInteger) { + void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, + bool isUnsignedInteger) { if (params == nullptr) return; if (!SamplerImpl::ValidateSamplerName(sampler)) return; @@ -129,6 +147,15 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: *(GLfloat*)params = samplerObj->GetLodBias(); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (isFloat) { + *(GLfloat*)params = samplerObj->GetMaxAnisotropy(); + } else if (isUnsignedInteger) { + *(GLuint*)params = static_cast(samplerObj->GetMaxAnisotropy()); + } else { + *(GLint*)params = static_cast(samplerObj->GetMaxAnisotropy()); + } + break; case GL_TEXTURE_COMPARE_MODE: *(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()); break; @@ -240,7 +267,7 @@ namespace MobileGL::MG_Impl::GLImpl { } void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) { - SetSamplerParam_State(sampler, pname, param, false, true); + SetSamplerParam_State(sampler, pname, param, false, false); } void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) { @@ -268,7 +295,7 @@ namespace MobileGL::MG_Impl::GLImpl { } void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) { - GetSamplerParam_State(sampler, pname, params, false, true); + GetSamplerParam_State(sampler, pname, params, false, false); } void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) { diff --git a/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp index 83934ce3..9a028d31 100644 --- a/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp @@ -99,6 +99,16 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl { case GL_TEXTURE_LOD_BIAS: return true; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (!(param >= 1.0f)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", "ValidateSamplerFloatParam", + "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0.")); + return false; + } + return true; + case GL_TEXTURE_BORDER_COLOR: if (param < 0.0f || param > 1.0f) { MG_State::pGLContext->RecordError( @@ -125,6 +135,16 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl { } return true; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (param < 1) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", "ValidateSamplerIntParam", + "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.")); + return false; + } + return true; + default: return ValidateSamplerParam(pname, static_cast(param)); } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 676dd0e2..fc0c852a 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -74,6 +74,16 @@ namespace MobileGL::MG_Impl::GLImpl { return true; } + Bool ValidateMaxAnisotropy(Float maxAnisotropy, const char* caller) { + if (maxAnisotropy >= 1.0f) return true; + + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", caller, + "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0.")); + return false; + } + template void WithTemporarilyBoundNamedTexture(const SharedPtr& textureObject, Fn&& fn) { @@ -466,6 +476,9 @@ namespace MobileGL::MG_Impl::GLImpl { Bool ValidateTextureParameterForTarget(const SharedPtr& textureObject, GLenum pname, GLint param, const char* caller) { const auto target = textureObject->GetTarget(); + if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) { + return false; + } if ((pname == GL_TEXTURE_BASE_LEVEL || pname == GL_TEXTURE_MAX_LEVEL) && param < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, @@ -495,7 +508,8 @@ namespace MobileGL::MG_Impl::GLImpl { (pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T || pname == GL_TEXTURE_WRAP_R || pname == GL_TEXTURE_MIN_FILTER || pname == GL_TEXTURE_MAG_FILTER || pname == GL_TEXTURE_MIN_LOD || pname == GL_TEXTURE_MAX_LOD || pname == GL_TEXTURE_LOD_BIAS || pname == GL_TEXTURE_COMPARE_MODE || - pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR)) { + pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR || + pname == GL_TEXTURE_MAX_ANISOTROPY_EXT)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", caller, @@ -586,6 +600,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: textureObject->GetSamplerObject()->SetLodBias((GLfloat)param); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + textureObject->GetSamplerObject()->SetMaxAnisotropy(static_cast(param)); + break; case GL_GENERATE_MIPMAP: g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != GL_FALSE); break; @@ -602,7 +619,10 @@ namespace MobileGL::MG_Impl::GLImpl { void TextureParameterObjectf_State(const SharedPtr& textureObject, GLenum pname, GLfloat param, const char* caller) { if (!textureObject) return; - if (!ValidateTextureParameterForTarget(textureObject, pname, static_cast(param), caller)) return; + if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) return; + const GLint validationParam = + pname == GL_TEXTURE_MAX_ANISOTROPY_EXT ? 1 : static_cast(param); + if (!ValidateTextureParameterForTarget(textureObject, pname, validationParam, caller)) return; switch (pname) { case GL_TEXTURE_MAG_FILTER: @@ -648,6 +668,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: textureObject->GetSamplerObject()->SetLodBias(param); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + textureObject->GetSamplerObject()->SetMaxAnisotropy(param); + break; case GL_GENERATE_MIPMAP: g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f); break; @@ -717,6 +740,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = (GLint)MG_Util::ConvertSamplerCompareFuncToGLEnum( textureObject->GetSamplerObject()->GetSamplerCompareFunc()); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + *params = static_cast(textureObject->GetSamplerObject()->GetMaxAnisotropy()); + break; default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, @@ -1105,6 +1131,10 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_LOD_BIAS: textureObject->GetSamplerObject()->SetLodBias(param); break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (!ValidateMaxAnisotropy(param, __func__)) return; + textureObject->GetSamplerObject()->SetMaxAnisotropy(param); + break; case GL_GENERATE_MIPMAP: g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f); break; @@ -1912,6 +1942,11 @@ namespace MobileGL::MG_Impl::GLImpl { textureObject->GetSamplerObject()->GetSamplerCompareFunc()); } break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (params) { + *params = static_cast(textureObject->GetSamplerObject()->GetMaxAnisotropy()); + } + break; case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE: if (params) { *params = GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE; @@ -2058,6 +2093,11 @@ namespace MobileGL::MG_Impl::GLImpl { textureObject->GetSamplerObject()->GetSamplerCompareFunc()); } break; + case GL_TEXTURE_MAX_ANISOTROPY_EXT: + if (params) { + *params = textureObject->GetSamplerObject()->GetMaxAnisotropy(); + } + break; default: MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "GetTexParameterfv_State", diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp index 68569872..fad9abb7 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp @@ -78,6 +78,13 @@ namespace MobileGL { ++m_version; } + void SamplerObject::SetMaxAnisotropy(Float maxAnisotropy) { + if (maxAnisotropy == m_samplerParameters.maxAnisotropy) return; + + m_samplerParameters.maxAnisotropy = maxAnisotropy; + ++m_version; + } + void SamplerObject::SetSamplerCompareFunc(SamplerCompareFunc func) { if (func == m_samplerParameters.compareFunc) return; @@ -128,6 +135,10 @@ namespace MobileGL { return m_samplerParameters.lodBias; } + Float SamplerObject::GetMaxAnisotropy() const { + return m_samplerParameters.maxAnisotropy; + } + SamplerCompareMode SamplerObject::GetCompareMode() const { return m_samplerParameters.compareMode; } diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h index 16fda9b0..54352396 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.h @@ -65,6 +65,7 @@ namespace MobileGL { Float minLod = -1000.0f; Float maxLod = 1000.0f; Float lodBias = 0.0f; + Float maxAnisotropy = 1.0f; SamplerCompareFunc compareFunc = SamplerCompareFunc::Always; SamplerCompareMode compareMode = SamplerCompareMode::None; }; @@ -83,6 +84,7 @@ namespace MobileGL { void SetMipmapMode(SamplerMipmapMode mode); void SetLodRange(Float minLod, Float maxLod); void SetLodBias(Float bias); + void SetMaxAnisotropy(Float maxAnisotropy); void SetSamplerCompareFunc(SamplerCompareFunc func); void SetCompareMode(SamplerCompareMode mode); @@ -95,6 +97,7 @@ namespace MobileGL { Float GetMinLod() const; Float GetMaxLod() const; Float GetLodBias() const; + Float GetMaxAnisotropy() const; SamplerCompareMode GetCompareMode() const; SamplerCompareFunc GetSamplerCompareFunc() const; Uint GetExternalIndex() const; diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index ff5adb4a..8ee37032 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,7 @@ namespace { GLenum errorRaisedByDraw = GL_NO_ERROR; GLenum pendingError = GL_NO_ERROR; + std::vector extensions; GLuint nextBufferId = 1; GLuint nextShaderId = 1; @@ -86,7 +88,7 @@ namespace { *data = 1; break; case GL_NUM_EXTENSIONS: - *data = 0; + *data = static_cast(g_fake.extensions.size()); break; default: // Leave the caller's defaults for every other capability query. @@ -114,9 +116,10 @@ namespace { return reinterpret_cast(""); } }; - // GL_NUM_EXTENSIONS reports 0 above, so this is never reached; it exists so the - // table stays complete if the extension loop ever runs. - funcs.glGetStringi = [](GLenum, GLuint) -> const GLubyte* { return nullptr; }; + funcs.glGetStringi = [](GLenum name, GLuint index) -> const GLubyte* { + if (name != GL_EXTENSIONS || index >= g_fake.extensions.size()) return nullptr; + return reinterpret_cast(g_fake.extensions[index].c_str()); + }; funcs.glGetFloatv = [](GLenum pname, GLfloat* data) { switch (pname) { // Two-component range queries. @@ -400,3 +403,20 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) { EXPECT_FALSE(conformingCaps.IndirectDrawInstanceIdIncludesBaseInstance); ExpectProbeReleasedAllObjects(); } + +TEST(TextureAnisotropyCapabilities, ExtensionPresenceIsDetectedExactly) { + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + const auto funcs = MakeFakeGLESFunctions(); + + MobileGL::MG_External::GLESCapabilities absentCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs)); + EXPECT_FALSE(absentCaps.SupportsTextureFilterAnisotropy); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.extensions.emplace_back("GL_EXT_texture_filter_anisotropic"); + MobileGL::MG_External::GLESCapabilities presentCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(presentCaps, funcs)); + EXPECT_TRUE(presentCaps.SupportsTextureFilterAnisotropy); +} diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index e14afdac..56bed14d 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -136,6 +137,127 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(TextureTest, TextureMaxAnisotropyDefaultsToOneAndRoundTripsWithoutRedundantVersionBumps) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + const auto& samplerObject = textureObject->GetSamplerObject(); + ASSERT_NE(samplerObject, nullptr); + + GLfloat floatValue = 0.0f; + MG_Impl::GLImpl::GetTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue); + EXPECT_FLOAT_EQ(floatValue, 1.0f); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 1.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint16 initialVersion = samplerObject->GetVersion(); + MG_Impl::GLImpl::TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 4.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 4.0f); + EXPECT_EQ(samplerObject->GetVersion(), static_cast(initialVersion + 1)); + + GLint integerValue = 0; + MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, &integerValue); + EXPECT_EQ(integerValue, 4); + MG_Impl::GLImpl::GetTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue); + EXPECT_FLOAT_EQ(floatValue, 4.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint16 setVersion = samplerObject->GetVersion(); + MG_Impl::GLImpl::TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 4.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 8.0f); + EXPECT_EQ(samplerObject->GetVersion(), static_cast(setVersion + 1)); +} + +TEST_F(TextureTest, TextureMaxAnisotropyBelowOneIsInvalidValueAndPreservesState) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + const auto& samplerObject = textureObject->GetSamplerObject(); + ASSERT_NE(samplerObject, nullptr); + const Uint16 initialVersion = samplerObject->GetVersion(); + + MG_Impl::GLImpl::TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0.5f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 1.0f); + EXPECT_EQ(samplerObject->GetVersion(), initialVersion); + + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 1.0f); + EXPECT_EQ(samplerObject->GetVersion(), initialVersion); +} + +TEST_F(TextureTest, SamplerMaxAnisotropyUsesTheSameStateAndValidationSemantics) { + GLuint sampler = 0; + MG_Impl::GLImpl::GenSamplers(1, &sampler); + ASSERT_NE(sampler, 0u); + + GLfloat floatValue = 0.0f; + MG_Impl::GLImpl::GetSamplerParameterfv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(floatValue, 1.0f); + + const auto& samplerObject = MG_State::pGLContext->GetSamplerObject(sampler); + ASSERT_NE(samplerObject, nullptr); + const Uint16 initialVersion = samplerObject->GetVersion(); + + MG_Impl::GLImpl::SamplerParameterf(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 6.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 6.0f); + EXPECT_EQ(samplerObject->GetVersion(), static_cast(initialVersion + 1)); + + GLint integerValue = 0; + MG_Impl::GLImpl::GetSamplerParameteriv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &integerValue); + EXPECT_EQ(integerValue, 6); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint16 setVersion = samplerObject->GetVersion(); + MG_Impl::GLImpl::SamplerParameterf(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 6.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + MG_Impl::GLImpl::SamplerParameterf(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0.25f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 6.0f); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 6.0f); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + const GLint signedInvalidValue = -1; + MG_Impl::GLImpl::SamplerParameterIiv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &signedInvalidValue); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 6.0f); + EXPECT_EQ(samplerObject->GetVersion(), setVersion); + + const GLuint unsignedValue = 10; + MG_Impl::GLImpl::SamplerParameterIuiv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &unsignedValue); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(samplerObject->GetMaxAnisotropy(), 10.0f); + EXPECT_EQ(samplerObject->GetVersion(), static_cast(setVersion + 1)); + + GLuint queriedUnsignedValue = 0; + MG_Impl::GLImpl::GetSamplerParameterIuiv(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, &queriedUnsignedValue); + EXPECT_EQ(queriedUnsignedValue, unsignedValue); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, GenThenBindCreatesObjectForUnsizedPackedBgraSubImageUpload) { GLuint texture = 0; MG_Impl::GLImpl::GenTextures(1, &texture); diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 5e6df140..9b9aa97c 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -799,6 +799,9 @@ namespace MobileGL::MG_Util::BackendLoader { if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) { caps.SupportsNorm16Texture = true; } + if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) { + caps.SupportsTextureFilterAnisotropy = true; + } if (std::strcmp(extension, "GL_EXT_base_instance") == 0) { caps.SupportsBaseInstance = true; } diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index ef0283b7..f07f4b00 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1031,6 +1031,9 @@ namespace MobileGL { String GLESShadingLanguageVersionString; Bool SupportsPersistentMapping = false; Bool SupportsNorm16Texture = false; + // GL_EXT_texture_filter_anisotropic is present, so sampler/texture + // anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES. + Bool SupportsTextureFilterAnisotropy = false; Bool SupportsBaseInstance = false; // GL_EXT_disjoint_timer_query is present in the extension string. Bool SupportsDisjointTimerQuery = false; From b8db5095810d6e3355db75eb0182696b641c2143 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 17:18:07 -0400 Subject: [PATCH 42/44] [Fix] (MG_Util/ShaderTranspiler): normalize legacy desktop shaders to GLSL 330 --- MobileGL/MG_Test/Program/ProgramUtilTest.cpp | 247 +++++++++++++++- .../ShaderSourceProcessor.cpp | 266 +++++++++++++++--- 2 files changed, 464 insertions(+), 49 deletions(-) diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 40cbd817..1b272dd3 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -107,7 +107,7 @@ void main() { PreprocessShaderSource(ShaderStage::Vertex, source); - EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_EQ(source.find("#version 330 core\n"), 0); EXPECT_NE(source.find("in vec3 position;"), String::npos); EXPECT_NE(source.find("out vec2 uv;"), String::npos); EXPECT_EQ(source.find("attribute"), String::npos); @@ -136,7 +136,7 @@ void main() { PreprocessShaderSource(ShaderStage::Fragment, source); - EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_EQ(source.find("#version 330 core\n"), 0); EXPECT_NE(source.find("out vec4 mg_FragColor;\n"), String::npos); EXPECT_NE(source.find("in vec2 uv;"), String::npos); EXPECT_NE(source.find("texture(texture0, uv)"), String::npos); @@ -153,6 +153,243 @@ void main() { } } +TEST_F(ProgramUtilTest, PreprocessMinecraft112BlurShaderKeepsLegacySampleIdentifier) { + using namespace MG_Util::ShaderTranspiler; + + // assets/minecraft/shaders/program/blur.fsh from the unmodified Minecraft 1.12 client jar. + String source = R"(#version 120 + +uniform sampler2D DiffuseSampler; + +varying vec2 texCoord; +varying vec2 oneTexel; + +uniform vec2 InSize; + +uniform vec2 BlurDir; +uniform float Radius; + +void main() { + vec4 blurred = vec4(0.0); + float totalStrength = 0.0; + float totalAlpha = 0.0; + float totalSamples = 0.0; + for(float r = -Radius; r <= Radius; r += 1.0) { + vec4 sample = texture2D(DiffuseSampler, texCoord + oneTexel * r * BlurDir); + + // Accumulate average alpha + totalAlpha = totalAlpha + sample.a; + totalSamples = totalSamples + 1.0; + + // Accumulate smoothed blur + float strength = 1.0 - abs(r / Radius); + totalStrength = totalStrength + strength; + blurred = blurred + sample; + } + gl_FragColor = vec4(blurred.rgb / (Radius * 2.0 + 1.0), totalAlpha); +} +)"; + + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find("#version 330 core\n"), 0); + EXPECT_NE(source.find("vec4 sample = texture(DiffuseSampler"), String::npos); + EXPECT_NE(source.find("totalAlpha = totalAlpha + sample.a;"), String::npos); + EXPECT_NE(source.find("float totalSamples = 0.0;"), String::npos); + EXPECT_NE(source.find("totalSamples = totalSamples + 1.0;"), String::npos); + EXPECT_NE(source.find("blurred = blurred + sample;"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + +TEST_F(ProgramUtilTest, PreprocessLegacySampleInterfaceIdentifiersKeepNames) { + using namespace MG_Util::ShaderTranspiler; + + String vertexSource = R"(#version 150 +attribute vec3 sample; + +void main() { + gl_Position = vec4(sample, 1.0); +} +)"; + PreprocessShaderSource(ShaderStage::Vertex, vertexSource); + + EXPECT_EQ(vertexSource.find("#version 330 core\n"), 0); + EXPECT_NE(vertexSource.find("in vec3 sample;"), String::npos); + + ShaderAttrib vertexAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource}; + auto vertexResult = ShaderCompiler::CompileShader(vertexAttrib); + if (!vertexResult) { + FAIL() << "errc: " << vertexResult.error().errc << "\nlog: " << vertexResult.error().log + << "\nsource:\n" << vertexSource; + } + + String fragmentSource = R"(#version 150 +uniform sampler2D sample; +varying vec2 texCoord; + +void main() { + gl_FragColor = texture2D(sample, texCoord); +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, fragmentSource); + + EXPECT_EQ(fragmentSource.find("#version 330 core\n"), 0); + EXPECT_NE(fragmentSource.find("uniform sampler2D sample;"), String::npos); + EXPECT_NE(fragmentSource.find("texture(sample, texCoord)"), String::npos); + + ShaderAttrib fragmentAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fragmentSource}; + auto fragmentResult = ShaderCompiler::CompileShader(fragmentAttrib); + if (!fragmentResult) { + FAIL() << "errc: " << fragmentResult.error().errc << "\nlog: " << fragmentResult.error().log + << "\nsource:\n" << fragmentSource; + } +} + +TEST_F(ProgramUtilTest, PreprocessEsslVersionsRemainVulkanCompatible) { + using namespace MG_Util::ShaderTranspiler; + + const auto verifyVersion = [](const char* inputVersion, const char* expectedVersion) { + SCOPED_TRACE(inputVersion); + String source = inputVersion; + source += R"( +precision mediump float; +out vec4 fragColor; + +void main() { + fragColor = vec4(1.0); +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find(expectedVersion), 0); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } + }; + + // Preserve the pre-existing desktop-core route: the current resource table cannot parse ESSL built-ins. + verifyVersion("#version 300 es", "#version 460 core\n"); + verifyVersion("#version 310 es", "#version 460 core\n"); +} + +TEST_F(ProgramUtilTest, PreprocessModernDesktopVersionsRecognizesUtf8Bom) { + using namespace MG_Util::ShaderTranspiler; + + const auto verifyVersion = [](const char* inputVersion) { + SCOPED_TRACE(inputVersion); + String source = "\xef\xbb\xbf"; + source += inputVersion; + source += R"( +out vec4 fragColor; + +void main() { + fragColor = vec4(1.0); +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_EQ(source.find("\xef\xbb\xbf"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } + }; + + verifyVersion("#version 400 core"); + verifyVersion("#version 460 core"); +} + +TEST_F(ProgramUtilTest, PreprocessUsesRealSpacedVersionDirectiveForInjectedOutput) { + using namespace MG_Util::ShaderTranspiler; + + String source = R"(// #version 460 core +/* "#version 400 core" */ +#line 7 "#version 460 core" +# version 120 +varying vec2 uv; + +void main() { + gl_FragColor = vec4(uv, 0.0, 1.0); +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + const SizeT versionPos = source.find("#version 330 core\n"); + const SizeT outputPos = source.find("out vec4 mg_FragColor;\n"); + EXPECT_NE(versionPos, String::npos); + EXPECT_EQ(outputPos, versionPos + std::strlen("#version 330 core\n")); + EXPECT_NE(source.find("// #version 460 core"), String::npos); + EXPECT_EQ(source.find("#line"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + +TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtVersion460) { + using namespace MG_Util::ShaderTranspiler; + + String source = R"(#version 400 core +sample in vec4 interpolatedColor; +out vec4 fragColor; + +void main() { + fragColor = interpolatedColor; +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } +} + +TEST_F(ProgramUtilTest, PreprocessGpuShader5SampleQualifierUsesVersion460) { + using namespace MG_Util::ShaderTranspiler; + + for (const char* extension : {"GL_ARB_gpu_shader5", "GL_NV_gpu_shader5"}) { + SCOPED_TRACE(extension); + String source = "#version 150\n#extension "; + source += extension; + source += R"( : enable +sample in vec4 interpolatedColor; +out vec4 fragColor; + +void main() { + fragColor = interpolatedColor; +} +)"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source; + } + } +} + TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderModernizesFragData) { using namespace MG_Util::ShaderTranspiler; @@ -164,7 +401,7 @@ void main() { PreprocessShaderSource(ShaderStage::Fragment, source); - EXPECT_EQ(source.find("#version 460 core\n"), 0); + EXPECT_EQ(source.find("#version 330 core\n"), 0); EXPECT_NE(source.find("layout(location = 0) out vec4 mg_FragData[8];\n"), String::npos); EXPECT_NE(source.find("mg_FragData[0] = vec4(1.0);"), String::npos); EXPECT_NE(source.find("mg_FragData[1].a = 0.5;"), String::npos); @@ -182,7 +419,7 @@ TEST_F(ProgramUtilTest, PreprocessKeepsDefaultPrecisionStatements) { // Mirrors the GL CTS helper shaders (e.g. glcPixelStorageModesTests): the old qualifier strip // turned "precision highp float;" into invalid "precision float;". Precision qualifiers are - // legal (and ignored) in the forced 460 core profile, so they now pass through untouched. + // legal (and ignored) in the normalized desktop core profile, so they now pass through untouched. String source = R"(#version 330 precision highp float; precision mediump int; @@ -212,7 +449,7 @@ TEST_F(ProgramUtilTest, PreprocessKeepsPrecisionInLegacyShaderForGlslang) { using namespace MG_Util::ShaderTranspiler; // Legacy ES-style shader: precision statements and qualifier macros are left for glslang - // (its preprocessor expands the #define; the 460 core parse ignores the qualifiers). + // (its preprocessor expands the #define; the normalized 330 core parse ignores the qualifiers). String source = R"(#define HIGHP_OR_DEFAULT highp precision HIGHP_OR_DEFAULT float; precision mediump int; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index 70e3f62e..5466f24e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -19,6 +19,220 @@ namespace { return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; } + bool IsIdentifierStart(char ch) { + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; + } + + MobileGL::String MaskCommentsAndQuotedText(const MobileGL::String& source) { + enum class Region { Code, SingleLineComment, MultiLineComment, QuotedText }; + + MobileGL::String masked = source; + Region region = Region::Code; + char quote = '\0'; + bool escaped = false; + + for (SizeT pos = 0; pos < source.size(); pos++) { + const char ch = source[pos]; + const char next = pos + 1 < source.size() ? source[pos + 1] : '\0'; + + if (region == Region::Code) { + if (ch == '/' && next == '/') { + masked[pos] = ' '; + masked[pos + 1] = ' '; + pos++; + region = Region::SingleLineComment; + } else if (ch == '/' && next == '*') { + masked[pos] = ' '; + masked[pos + 1] = ' '; + pos++; + region = Region::MultiLineComment; + } else if (ch == '"' || ch == '\'') { + masked[pos] = ' '; + quote = ch; + escaped = false; + region = Region::QuotedText; + } + continue; + } + + if (region == Region::SingleLineComment) { + if (ch == '\n' || ch == '\r') { + region = Region::Code; + } else { + masked[pos] = ' '; + } + continue; + } + + if (region == Region::MultiLineComment) { + if (ch == '*' && next == '/') { + masked[pos] = ' '; + masked[pos + 1] = ' '; + pos++; + region = Region::Code; + } else if (ch != '\n' && ch != '\r') { + masked[pos] = ' '; + } + continue; + } + + if (ch != '\n' && ch != '\r') { + masked[pos] = ' '; + } + if (escaped) { + escaped = false; + } else if (ch == '\\') { + escaped = true; + } else if (ch == quote) { + region = Region::Code; + } + } + + return masked; + } + + void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) { + while (pos < lineEnd && std::isspace(static_cast(source[pos]))) { + pos++; + } + } + + MobileGL::String ReadDirectiveIdentifier(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) { + if (pos >= lineEnd || !IsIdentifierStart(source[pos])) { + return {}; + } + + const SizeT start = pos++; + while (pos < lineEnd && IsIdentifierChar(source[pos])) { + pos++; + } + return source.substr(start, pos - start); + } + + bool HasUtf8Bom(const MobileGL::String& source) { + return source.size() >= 3 && static_cast(source[0]) == 0xef && + static_cast(source[1]) == 0xbb && static_cast(source[2]) == 0xbf; + } + + struct ShaderLanguageInfo { + unsigned version = 110; + MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core; + SizeT versionDirectiveStart = MobileGL::String::npos; + SizeT versionDirectiveEnd = MobileGL::String::npos; + bool hasUtf8Bom = false; + bool enablesGpuShader5 = false; + + bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; } + }; + + ShaderLanguageInfo InspectShaderLanguage(const MobileGL::String& source) { + const MobileGL::String code = MaskCommentsAndQuotedText(source); + ShaderLanguageInfo info; + info.hasUtf8Bom = HasUtf8Bom(source); + + SizeT lineStart = 0; + while (lineStart < code.size()) { + SizeT lineEnd = code.find('\n', lineStart); + const bool hasLineBreak = lineEnd != MobileGL::String::npos; + if (!hasLineBreak) { + lineEnd = code.size(); + } + + SizeT probe = lineStart; + if (lineStart == 0 && info.hasUtf8Bom) { + probe = 3; + } + SkipDirectiveWhitespace(code, probe, lineEnd); + if (probe < lineEnd && code[probe] == '#') { + const SizeT directiveStart = probe; + probe++; + SkipDirectiveWhitespace(code, probe, lineEnd); + const MobileGL::String directive = ReadDirectiveIdentifier(code, probe, lineEnd); + + if (directive == "version" && !info.HasVersionDirective()) { + SkipDirectiveWhitespace(code, probe, lineEnd); + unsigned version = 0; + bool hasVersionDigits = false; + while (probe < lineEnd && code[probe] >= '0' && code[probe] <= '9') { + hasVersionDigits = true; + version = version * 10 + static_cast(code[probe] - '0'); + probe++; + } + if (hasVersionDigits) { + info.version = version; + info.versionDirectiveStart = directiveStart; + info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0); + SkipDirectiveWhitespace(code, probe, lineEnd); + const MobileGL::String profile = ReadDirectiveIdentifier(code, probe, lineEnd); + if (profile == "es" || profile == "ES") { + info.profile = MobileGL::ShaderProfile::ES; + } else if (profile == "compatibility") { + info.profile = MobileGL::ShaderProfile::Compatibility; + } else { + info.profile = MobileGL::ShaderProfile::Core; + } + } + } else if (directive == "extension") { + SkipDirectiveWhitespace(code, probe, lineEnd); + const MobileGL::String extension = ReadDirectiveIdentifier(code, probe, lineEnd); + SkipDirectiveWhitespace(code, probe, lineEnd); + if (probe < lineEnd && code[probe] == ':') { + probe++; + SkipDirectiveWhitespace(code, probe, lineEnd); + const MobileGL::String behavior = ReadDirectiveIdentifier(code, probe, lineEnd); + const bool isGpuShader5 = extension == "GL_ARB_gpu_shader5" || + extension == "GL_NV_gpu_shader5"; + const bool enablesExtension = behavior == "enable" || behavior == "require" || + behavior == "warn"; + // Gate the whole source if it ever opts into either extension. This is deliberately + // conservative around conditional directives and keeps legal sample qualifiers intact. + info.enablesGpuShader5 = info.enablesGpuShader5 || (isGpuShader5 && enablesExtension); + } + } + } + + lineStart = lineEnd + (hasLineBreak ? 1 : 0); + } + + return info; + } + + MobileGL::String GetNormalizedVersionDirective(const ShaderLanguageInfo& info) { + if (info.profile == MobileGL::ShaderProfile::ES) { + // Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan + // glslang resource table cannot parse its ESSL built-ins today, even at ESSL 310, whereas the same + // source is accepted through the normalized desktop core path. + return "#version 460 core\n"; + } + + // Keep compatibility-profile handling on its pre-existing 460 path. Vulkan glslang does not accept that + // profile today, and this legacy-sample fix must not broaden or otherwise alter that separate limitation. + if (info.profile == MobileGL::ShaderProfile::Compatibility) { + return "#version 460 compatibility\n"; + } + + const bool useLegacyDesktopVersion = + info.version < 400 && !info.enablesGpuShader5; + return useLegacyDesktopVersion ? "#version 330 core\n" : "#version 460 core\n"; + } + + void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) { + const MobileGL::String replacement = GetNormalizedVersionDirective(info); + if (info.HasVersionDirective()) { + source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart, + replacement); + if (info.hasUtf8Bom) { + source.erase(0, 3); + } + return; + } + + if (info.hasUtf8Bom) { + source.erase(0, 3); + } + source.insert(0, replacement); + } + bool HasSingleLineFunctionDefinition(const MobileGL::String& source, const MobileGL::String& functionName) { SizeT lineStart = 0; while (lineStart < source.size()) { @@ -153,12 +367,8 @@ namespace { } SizeT FindAfterVersionDirective(const MobileGL::String& source) { - const SizeT versionPos = source.find("#version"); - if (versionPos == MobileGL::String::npos) { - return 0; - } - const SizeT lineEnd = source.find('\n', versionPos); - return lineEnd == MobileGL::String::npos ? source.size() : lineEnd + 1; + const ShaderLanguageInfo info = InspectShaderLanguage(source); + return info.HasVersionDirective() ? info.versionDirectiveEnd : 0; } bool IsExtensionAdvertised(MobileGL::GLExtension extension) { @@ -322,7 +532,7 @@ namespace { void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) { // Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and - // ignored in the forced "#version 460 core" profile, so glslang handles them natively. + // ignored in the normalized desktop core profiles, so glslang handles them natively. ReplaceIdentifier(source, "texture2D", "texture"); ReplaceIdentifier(source, "texture2DProj", "textureProj"); @@ -367,6 +577,11 @@ namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { void PreprocessShaderSource(ShaderStage stage, String& source) { + // Normalize while the inspector's source span still refers to the untouched input. Later passes + // remove comments and directives, so any subsequent insertion re-inspects the current source. + const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source); + NormalizeVersionDirective(source, originalLanguage); + // remove multi-line comment size_t commentStartPos = source.find("/*"); while (commentStartPos != String::npos) { @@ -404,43 +619,6 @@ namespace MobileGL { noperspectivePos = source.find(str_np); } - // force #version - ShaderProfile profile = ShaderProfile::Core; - SizeT versionPos = source.find("#version"); - SizeT lineEnd = source.find('\n', versionPos); - - if (versionPos != String::npos) { - String versionLine = source.substr(versionPos, lineEnd - versionPos); - - if (versionLine.find("ES") != String::npos) - profile = ShaderProfile::ES; - else if (versionLine.find("compatibility") != String::npos) - profile = ShaderProfile::Compatibility; - else - profile = ShaderProfile::Core; - } else { - profile = ShaderProfile::Core; - source.insert(0, "#version 460 core\n"); - versionPos = 0; - lineEnd = source.find('\n', versionPos); - } - - SizeT firstLineEnd = lineEnd; - - if (profile != ShaderProfile::ES) { - constexpr const char* versionDirectiveCore = "#version 460 core\n"; - constexpr const char* versionDirectiveCompat = "#version 460 compatibility\n"; - - const char* replacement = - (profile == ShaderProfile::Compatibility) ? versionDirectiveCompat : versionDirectiveCore; - - if (firstLineEnd != String::npos) { - source.replace(versionPos, firstLineEnd - versionPos + 1, replacement); - } else { - source = replacement; - } - } - FilterUnsupportedGpuShaderInt64(source); CoerceUniformBlockPackingToStd140(source); From a08669df72c24e4758f7e9488683cdae7c55e084 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 22:11:43 -0400 Subject: [PATCH 43/44] [Fix] (MG_Impl/GLImpl): stop recording GL errors on the delete/query paths of every object family - glDeleteBuffers/VertexArrays/Renderbuffers/Framebuffers must silently ignore unknown names and glIsTexture must never raise, while glBindSampler now reports INVALID_OPERATION like the other bind entry points --- MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp | 5 ++++- .../MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp | 8 ++++++-- MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp | 11 ++++++++++- MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp | 4 +++- .../MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp | 4 +++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp index b27a0665..b47b492e 100644 --- a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp @@ -441,7 +441,10 @@ namespace MobileGL::MG_Impl::GLImpl { for (SizeT i = 0; i < static_cast(n); ++i) { Uint bufferName = buffers[i]; if (bufferName == 0) continue; - if (!BufferImpl::ValidateBufferName(bufferName, true)) continue; + // GL 3.3 core 2.9: names that do not correspond to an existing buffer are silently + // ignored here, so probe with the non-recording query - the shared validator would + // record INVALID_OPERATION, which is only correct on the bind path. + if (!MG_State::pGLContext->ValidateBufferName(bufferName)) continue; MG_State::pGLContext->MarkBufferObjectForDeletion(bufferName); } } diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index f987cdfa..a8bc603d 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -1201,7 +1201,9 @@ namespace MobileGL::MG_Impl::GLImpl { for (SizeT i = 0; i < static_cast(n); ++i) { Uint bufferName = renderbuffers[i]; if (bufferName == 0) continue; - if (!FramebufferImpl::ValidateRenderbufferName(bufferName)) continue; + // GL 3.3 core 4.4.2: unknown names are silently ignored on delete; the shared bind-path + // validator would record INVALID_OPERATION instead. + if (!MG_State::pGLContext->ValidateRenderbufferName(bufferName)) continue; MG_State::pGLContext->MarkRenderbufferObjectForDeletion(bufferName); } } @@ -1224,7 +1226,9 @@ namespace MobileGL::MG_Impl::GLImpl { for (SizeT i = 0; i < static_cast(n); ++i) { Uint bufferName = framebuffers[i]; if (bufferName == 0) continue; - if (!FramebufferImpl::ValidateFramebufferName(bufferName)) continue; + // GL 3.3 core 4.4.1: unknown names are silently ignored on delete; the shared bind-path + // validator would record INVALID_OPERATION instead. + if (!MG_State::pGLContext->ValidateFramebufferName(bufferName)) continue; MG_State::pGLContext->MarkFramebufferObjectForDeletion(bufferName); } } diff --git a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp index 0f31503e..2c78160c 100644 --- a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp @@ -233,7 +233,16 @@ namespace MobileGL::MG_Impl::GLImpl { if (sampler == 0) { textureUnit.SetSamplerObject(nullptr); } else { - if (!SamplerImpl::ValidateSamplerName(sampler)) return; + // GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already + // deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same + // name, which is why this cannot go through the shared SamplerImpl validator. + if (!MG_State::pGLContext->ValidateSamplerName(sampler)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "BindSampler_State", + std::format("Invalid sampler name {}", sampler))); + return; + } Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler); if (!doesSamplerObjectCreated) { MG_State::pGLContext->CreateSamplerObject(sampler); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index fc0c852a..4654a311 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -1753,7 +1753,9 @@ namespace MobileGL::MG_Impl::GLImpl { GLboolean IsTexture_State(GLuint texture) { // ======================= Processing ================================ - if (!TextureImpl::ValidateTextureName(texture, true)) return GL_FALSE; + // GL 3.3 core 6.1.4: IsTexture generates no error - an unknown, deleted or merely reserved + // name is just GL_FALSE. Probing with the recording validator (as every other Is* entry + // point already avoids doing) would leave a spurious INVALID_VALUE behind. return MG_State::pGLContext->ValidateTextureObject(texture) ? GL_TRUE : GL_FALSE; } diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp index 9e40c3b0..221cb974 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp @@ -288,7 +288,9 @@ namespace MobileGL::MG_Impl::GLImpl { GLuint vao = arrays[i]; if (vao == 0) continue; - if (!VertexArrayImpl::ValidateVertexArrayName(vao)) continue; + // GL 3.3 core 2.10: unknown names are silently ignored on delete; the shared bind-path + // validator would record INVALID_OPERATION instead. + if (!MG_State::pGLContext->ValidateVertexArrayName(vao)) continue; if (MG_State::pGLContext->GetBoundVertexArray() && MG_State::pGLContext->GetBoundVertexArray() == MG_State::pGLContext->GetVertexArrayObject(vao)) { From efd7b473886729d858539f5fb18d172e0aeb22cd Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 22:12:12 -0400 Subject: [PATCH 44/44] [Chore] (MG_Test): assert exact GL error counts - name-lifecycle regression tests per object family, plus fixtures that drain on setup and fail the test that leaks an unconsumed error --- MobileGL/MG_Test/Buffer/BufferTest.cpp | 75 ++++++++++++- .../MG_Test/Framebuffer/FramebufferTest.cpp | 101 ++++++++++++++++++ MobileGL/MG_Test/Texture/TextureTest.cpp | 60 ++++++++++- .../MG_Test/VertexArray/VertexArrayTest.cpp | 72 ++++++++++++- 4 files changed, 299 insertions(+), 9 deletions(-) diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index 2220a19d..fcd72d04 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -8,6 +8,8 @@ #include +#include + #include "Includes.h" #include "Init.h" #include @@ -20,9 +22,31 @@ using namespace MobileGL; class BufferTest : public ::testing::Test { protected: - void SetUp() override { MobileGL::Initialize(); } + // GL error flags are sticky per error code and the context outlives an individual test in this + // binary, so drain whatever an earlier test left pending - otherwise an error-code assertion + // here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite. + static void DrainPendingGlErrors() { + for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) { + } + } - void TearDown() override {} + // The call under test must raise exactly the expected error and nothing more: a second pending + // error means one entry point queued several, which GetError() would hand out at an unrelated + // call site later on. + static void ExpectSingleGlError(GLenum expected) { + EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + } + + void SetUp() override { + MobileGL::Initialize(); + DrainPendingGlErrors(); + } + + void TearDown() override { + // Attribute a leaked error to the test that caused it instead of to whoever runs next. + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } }; TEST_F(BufferTest, Binding) { @@ -105,6 +129,53 @@ TEST_F(BufferTest, GenerateManyNames_NoPrematureCreation) { } } +// GL 3.3 core 2.9 name lifecycle. The same three rules are asserted per object family (see the +// texture/vertex-array/framebuffer/renderbuffer suites): a deleted or never-generated name is +// INVALID_OPERATION to bind, deleting one is silent, and a generated-but-never-bound reservation +// is still released so the name gets recycled. +TEST_F(BufferTest, DeleteOfUnknownOrAlreadyDeletedBufferNameIsSilent) { + GLuint buffer = 0; + MG_Impl::GLImpl::GenBuffers(1, &buffer); + ASSERT_NE(buffer, 0u); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Double delete, name 0 and a never-generated name must all be ignored without an error. + MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const GLuint unknownNames[] = {0u, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteBuffers(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(BufferTest, DeleteGeneratedButUnboundBufferNameReleasesReservationAndBindFails) { + GLuint buffer = 0; + MG_Impl::GLImpl::GenBuffers(1, &buffer); + ASSERT_NE(buffer, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateBufferName(buffer)); + + MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(buffer)); + + MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLuint recycled = 0; + MG_Impl::GLImpl::GenBuffers(1, &recycled); + EXPECT_EQ(recycled, buffer); +} + +TEST_F(BufferTest, BindNeverGeneratedBufferNameIsInvalidOperation) { + // Not a small literal: other tests in this binary share the context and generate names in + // bulk, so a low number may well be a legitimately reserved name here. + MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, std::numeric_limits::max()); + ExpectSingleGlError(GL_INVALID_OPERATION); +} + TEST_F(BufferTest, AcquireMemory) { auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform); Vector bufferNames; diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index c3b24c5e..c8354f9a 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -8,6 +8,8 @@ #include +#include + #include "Includes.h" #include "Init.h" #include @@ -78,8 +80,30 @@ namespace { class FramebufferTest : public ::testing::Test { protected: + // GL error flags are sticky per error code and the context outlives an individual test in this + // binary, so drain whatever an earlier test left pending - otherwise an error-code assertion + // here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite. + static void DrainPendingGlErrors() { + for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) { + } + } + + // The call under test must raise exactly the expected error and nothing more: a second pending + // error means one entry point queued several, which GetError() would hand out at an unrelated + // call site later on. + static void ExpectSingleGlError(GLenum expected) { + EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + } + + void TearDown() override { + // Attribute a leaked error to the test that caused it instead of to whoever runs next. + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } + void SetUp() override { MobileGL::Initialize(); + DrainPendingGlErrors(); const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0); ASSERT_NE(defaultFramebuffer, nullptr); MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(defaultFramebuffer); @@ -122,6 +146,83 @@ TEST_F(FramebufferTest, CreateFramebuffersCreatesObjectsImmediately) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// GL 3.3 core 4.4.1/4.4.2 name lifecycle - mirrors the rules asserted for the other object +// families: deleting an unknown name is silent, a released reservation is recycled, and binding +// a dead name is INVALID_OPERATION. +TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedFramebufferNameIsSilent) { + GLuint framebuffer = 0; + MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer); + ASSERT_NE(framebuffer, 0u); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Not a small literal: other tests in this binary share the context and generate names in + // bulk, so a low number may well be a legitimately reserved name here. + const GLuint unknownNames[] = {0u, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteFramebuffers(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(FramebufferTest, DeleteGeneratedButUnboundFramebufferNameReleasesReservationAndBindFails) { + GLuint framebuffer = 0; + MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer); + ASSERT_NE(framebuffer, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateFramebufferName(framebuffer)); + + MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateFramebufferName(framebuffer)); + + MG_Impl::GLImpl::BindFramebuffer(GL_FRAMEBUFFER, framebuffer); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLuint recycled = 0; + MG_Impl::GLImpl::GenFramebuffers(1, &recycled); + EXPECT_EQ(recycled, framebuffer); +} + +TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedRenderbufferNameIsSilent) { + GLuint renderbuffer = 0; + MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer); + ASSERT_NE(renderbuffer, 0u); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Not a small literal: other tests in this binary share the context and generate names in + // bulk, so a low number may well be a legitimately reserved name here. + const GLuint unknownNames[] = {0u, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteRenderbuffers(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(FramebufferTest, DeleteGeneratedButUnboundRenderbufferNameReleasesReservationAndBindFails) { + GLuint renderbuffer = 0; + MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer); + ASSERT_NE(renderbuffer, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer)); + + MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer)); + + MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLuint recycled = 0; + MG_Impl::GLImpl::GenRenderbuffers(1, &recycled); + EXPECT_EQ(recycled, renderbuffer); +} + TEST_F(FramebufferTest, DefaultFramebufferIdentityTracksFramebufferNameZero) { const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0); ASSERT_NE(defaultFramebuffer, nullptr); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 56bed14d..1e6cd5b2 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -29,7 +29,32 @@ using namespace MobileGL; class TextureTest : public ::testing::Test { protected: - void SetUp() override { MobileGL::Initialize(); } + // GL error flags are sticky per error code and the context outlives an individual test in this + // binary, so anything an earlier test left pending would be handed to the next GetError() call - + // which silently turns error-code assertions into reads of someone else's error. Bounded because + // there is one flag per code; a runaway would otherwise hang the suite. + static void DrainPendingGlErrors() { + for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) { + } + } + + // The call under test must raise exactly the expected error and nothing more: a second pending + // error means one entry point queued several (e.g. a shared validator firing before the + // specific check), which GetError() would hand out at unrelated call sites later on. + static void ExpectSingleGlError(GLenum expected) { + EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + } + + void SetUp() override { + MobileGL::Initialize(); + DrainPendingGlErrors(); + } + + void TearDown() override { + // Attribute a leaked error to the test that caused it instead of to whoever runs next. + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } }; namespace { @@ -258,6 +283,31 @@ TEST_F(TextureTest, SamplerMaxAnisotropyUsesTheSameStateAndValidationSemantics) EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// GL 3.3 core 3.8.2: BindSampler rejects a never-generated or already-deleted name with +// INVALID_OPERATION, while SamplerParameter* on the same name is INVALID_VALUE - the two paths +// must not share one validator. Delete of an unknown name stays silent. +TEST_F(TextureTest, BindSamplerRejectsUnknownNameWithInvalidOperationUnlikeSamplerParameter) { + GLuint sampler = 0; + MG_Impl::GLImpl::GenSamplers(1, &sampler); + ASSERT_NE(sampler, 0u); + MG_Impl::GLImpl::BindSampler(0, sampler); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Deleting is silent, twice over, and the name is dead afterwards. + MG_Impl::GLImpl::DeleteSamplers(1, &sampler); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::DeleteSamplers(1, &sampler); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindSampler(0, sampler); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // Same dead name through SamplerParameter*: INVALID_VALUE, so the two paths cannot share one + // validator - and neither may queue the other's code alongside its own. + MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + ExpectSingleGlError(GL_INVALID_VALUE); +} + TEST_F(TextureTest, GenThenBindCreatesObjectForUnsizedPackedBgraSubImageUpload) { GLuint texture = 0; MG_Impl::GLImpl::GenTextures(1, &texture); @@ -307,10 +357,12 @@ TEST_F(TextureTest, DeleteGeneratedButUnboundNameReleasesReservationAndBindFails EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(texture)); EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + // IsTexture answers about a dead name without raising anything (GL 3.3 core 6.1.4). EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_FALSE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); - EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + ExpectSingleGlError(GL_INVALID_OPERATION); EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); // The freed reservation is recycled (the generator's free list is LIFO, so the very same @@ -340,7 +392,7 @@ TEST_F(TextureTest, DeleteInstantiatedTextureInvalidatesNameUntilRegenerated) { ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[0]); - EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + ExpectSingleGlError(GL_INVALID_OPERATION); EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) .GetBindingSlot(TextureTarget::Texture2D) .GetBoundObject(), @@ -359,7 +411,7 @@ TEST_F(TextureTest, DeleteUnknownNamesIsSilentButBindUnknownNameIsInvalid) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, unknownNames[1]); - EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + ExpectSingleGlError(GL_INVALID_OPERATION); EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) .GetBindingSlot(TextureTarget::Texture2D) .GetBoundObject(), diff --git a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp index 0ce7ba85..45fd94ba 100644 --- a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp +++ b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp @@ -8,6 +8,8 @@ #include +#include + #include "Includes.h" #include "Init.h" @@ -35,9 +37,31 @@ protected: return vbo; } - void SetUp() override { MobileGL::Initialize(); } + // GL error flags are sticky per error code and the context outlives an individual test in this + // binary, so drain whatever an earlier test left pending - otherwise an error-code assertion + // here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite. + static void DrainPendingGlErrors() { + for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) { + } + } - void TearDown() override {} + // The call under test must raise exactly the expected error and nothing more: a second pending + // error means one entry point queued several, which GetError() would hand out at an unrelated + // call site later on. + static void ExpectSingleGlError(GLenum expected) { + EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + } + + void SetUp() override { + MobileGL::Initialize(); + DrainPendingGlErrors(); + } + + void TearDown() override { + // Attribute a leaked error to the test that caused it instead of to whoever runs next. + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } }; TEST_F(VertexArrayTest, GenerateAndBindVAO) { @@ -57,6 +81,46 @@ TEST_F(VertexArrayTest, GenerateAndBindVAO) { // Do not detect if it supports default VAO } +// GL 3.3 core 2.10 name lifecycle - the same three rules the other object families assert: +// deleting an unknown name is silent, a released reservation is recycled, and binding a dead +// name is INVALID_OPERATION. +TEST_F(VertexArrayTest, DeleteOfUnknownOrAlreadyDeletedVertexArrayNameIsSilent) { + GLuint vao = 0; + MG_Impl::GLImpl::GenVertexArrays(1, &vao); + ASSERT_NE(vao, 0u); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteVertexArrays(1, &vao); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteVertexArrays(1, &vao); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Not a small literal: other tests in this binary share the context and generate names in + // bulk, so a low number may well be a legitimately reserved name here. + const GLuint unknownNames[] = {0u, std::numeric_limits::max()}; + MG_Impl::GLImpl::DeleteVertexArrays(2, unknownNames); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(VertexArrayTest, DeleteGeneratedButUnboundVertexArrayNameReleasesReservationAndBindFails) { + GLuint vao = 0; + MG_Impl::GLImpl::GenVertexArrays(1, &vao); + ASSERT_NE(vao, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateVertexArrayName(vao)); + + MG_Impl::GLImpl::DeleteVertexArrays(1, &vao); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateVertexArrayName(vao)); + + MG_Impl::GLImpl::BindVertexArray(vao); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLuint recycled = 0; + MG_Impl::GLImpl::GenVertexArrays(1, &recycled); + EXPECT_EQ(recycled, vao); +} + TEST_F(VertexArrayTest, VertexAttributeSetup) { Vector vaoNames; MobileGL::MG_State::pGLContext->GenVertexArrayNames(1, vaoNames); @@ -264,7 +328,9 @@ TEST_F(VertexArrayTest, VertexBindingIndexIsBoundedByTheAdvertisedAttribLimit) { const GLuint outOfRange = MG_Impl::GLImpl::VertexArrayImpl::GetMaxVertexAttribs(); MG_Impl::GLImpl::VertexAttribBinding(0, outOfRange); - EXPECT_TRUE(MG_State::pGLContext->HasGLError()); + // Asserting the exact code (rather than just "some error") also consumes it, so the next test + // does not inherit it - GL error flags are sticky and this context is shared. + ExpectSingleGlError(GL_INVALID_VALUE); } // The default attribute -> binding-point mapping is the identity. It used to be a 16-element literal