From 1b0be9a99795f5b90ccf7b2257b5974d406a1ba0 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 22:36:56 -0400 Subject: [PATCH 01/31] [Docs] (android-plugin): specify POST format capability tables --- ...gl-post-format-capability-tables-design.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-15-mobilegl-post-format-capability-tables-design.md diff --git a/docs/superpowers/specs/2026-07-15-mobilegl-post-format-capability-tables-design.md b/docs/superpowers/specs/2026-07-15-mobilegl-post-format-capability-tables-design.md new file mode 100644 index 00000000..bc8d019f --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-mobilegl-post-format-capability-tables-design.md @@ -0,0 +1,78 @@ +# MobileGL POST Format Capability Tables + +## Goal + +Expose the format-capability results used during MobileGL backend startup in the Android plugin's driver POST screen. The screen must show the exact `Full`, `Caveat`, or `None` result for every backend, target, internal format, and capability without duplicating the backend's detection rules. + +## Existing Architecture + +- `DriverPost.cpp` probes the device GLES and Vulkan drivers before `MobileGL::Initialize()` and returns a `BackendPostReport` for each backend. +- `DriverPostJni.cpp` serializes those reports to JSON for `PostActivity`. +- `PostActivity` uses platform Android views and already supports collapsible check details and a collapsible raw report. +- Backend startup fills a `FormatCapabilityCache` in the DirectGLES and DirectVulkan `InitCapabilities()` paths. `FullCaps` takes precedence over `CaveatCaps`; an absent bit means `None`. +- The capability matrix contains 12 targets, 75 internal formats, and 14 capability columns per backend. + +## Selected Approach + +Extract callable format-probe entry points from the existing DirectGLES and DirectVulkan implementations. Backend startup and the POST will call these same functions, so their results cannot drift. + +The POST will run each probe while its temporary driver resources are still valid: + +- DirectGLES: after the GLES function table and capabilities have been populated, while the 1x1 pbuffer context is current. +- DirectVulkan: after selecting the physical device, while the Vulkan instance and physical device handles remain valid. + +The resulting optional `FormatCapabilityCache` will be stored in each `BackendPostReport`. Failure to obtain a format table will not discard the existing POST checks or change their verdict; the UI will instead report that the format table is unavailable. + +## JSON Contract + +The JNI report will add an optional `formatCapabilities` object to each backend. To avoid repeating tens of thousands of status strings, the object will contain: + +- one ordered capability-name array; +- one entry per target; +- one compact row per internal format containing the format name, a Full bitmask, and a Caveat bitmask. + +Java resolves each cell in this order: + +1. Full bit present: `Full`. +2. Otherwise Caveat bit present: `Caveat`. +3. Otherwise: `None`. + +This preserves the backend's current precedence and keeps the raw JSON reasonably small. + +## Android UI + +Each backend section keeps its existing verdict, renderer string, and check table. A new `Format capabilities` subsection follows it. + +- Each of the 11 texture targets and `Renderbuffer` is a separate, initially collapsed table. +- Target headers can be expanded independently. +- Table content is created on expansion and removed when collapsed, preventing the activity from retaining roughly 27,000 status views. +- Each expanded table is placed in a horizontal scroll container. +- The first column contains internal-format names. The remaining columns use the ordered capability names from the JSON report. +- Every status cell displays its status text and uses the conventional color mapping: + - `Full`: green background with white text. + - `Caveat`: yellow background with black text. + - `None`: red background with white text. +- Header and format-name cells use neutral dark backgrounds consistent with the existing POST theme. +- The existing raw-report toggle remains available at the end of the screen. + +## Performance and Lifecycle + +- The existing single-flight native POST and cached JSON behavior remains unchanged. +- Format tables are lazily materialized and discarded on collapse. +- The JSON carries bitmasks rather than repeated `Full`, `Caveat`, and `None` strings. +- Existing configuration-change handling remains unchanged. + +## Validation + +1. Run focused source checks and `git diff --check`. +2. Build the Android plugin APK with the repository's current Gradle workflow. +3. If an Android target is connected, install the APK and open `PostActivity`. +4. Verify both backend sections, all target toggles, horizontal scrolling, visible cell text, and the green/yellow/red mapping. +5. Confirm collapsing a table removes its generated content and expanding it recreates the same values. + +## Non-Goals + +- Changing the meanings of `Full`, `Caveat`, or `None`. +- Changing POST verdict rules. +- Displaying sample-count vectors in this iteration. +- Replacing the existing platform-view UI with Compose, AppCompat, or WebView. From 4453f1910dc6e8275d306fc332eba317559a5250 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:09:31 -0400 Subject: [PATCH 02/31] [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 e8d9a913d8f053e2869101a3825257b57b2cec79 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:18:46 -0400 Subject: [PATCH 03/31] [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 315e9cb19430c647e092f065e89c5668f14a960c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 22:56:31 -0400 Subject: [PATCH 04/31] [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 74641792496a2e5e9c362eaa805a92e2bc316712 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 22:57:27 -0400 Subject: [PATCH 05/31] [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 cb6af449848b643e326e74b7b992a8e0642a4f96 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:14:19 -0400 Subject: [PATCH 06/31] [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 c6ed9429be4caef133d3ad3a83851151528f6a2e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:19:47 -0400 Subject: [PATCH 07/31] [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 37a7f35a271ed77fe7e2656a0675b6563af9b4eb Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 15 Jul 2026 23:22:13 -0400 Subject: [PATCH 08/31] [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 3a40778c4b6680101b3beb05f23af1713ef511c8 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 01:50:33 -0400 Subject: [PATCH 09/31] [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 273c7ebcf078e3cf431ae2da5333665fe750d34c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 02:23:18 -0400 Subject: [PATCH 10/31] [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 8bdab8005bb419de4a288d5b2a76e978129c9301 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 02:36:07 -0400 Subject: [PATCH 11/31] [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 5b38f619614545712b11b4806adfc9106b9589b5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:06:03 -0400 Subject: [PATCH 12/31] [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 7514587b5a86815fd582c07dfc1d447260a198ce Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:31:32 -0400 Subject: [PATCH 13/31] [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 8496e7c7eb1d1bc739b8d332b69bd6267e6ec8b5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:40:54 -0400 Subject: [PATCH 14/31] [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 35626da5c4112e3b4e13c72aadf66624b24e1f2c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:35:50 -0400 Subject: [PATCH 15/31] [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 1dc217b32cb0816374c72dc5c90049078bb07e7b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:54:33 -0400 Subject: [PATCH 16/31] [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 e9bd520d9b699af3cbaa4cdca34cd4ca9be277be Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 03:54:33 -0400 Subject: [PATCH 17/31] [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 763d4c3207c2bcdcb9b0361150a71d91e13806b6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 04:11:48 -0400 Subject: [PATCH 18/31] [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 b8dc4a6004df09653ad5967b5aca17ee3a0e512a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 04:39:57 -0400 Subject: [PATCH 19/31] [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 164bfd810b979171fbe99a92840261500c9fec98 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 05:29:46 -0400 Subject: [PATCH 20/31] [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 37a050b106541a9a96b46b10b28545ce6fcf2f64 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 05:52:01 -0400 Subject: [PATCH 21/31] [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 a1a8a18575b27e3e3b0cd40e897af226a306a135 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 06:11:53 -0400 Subject: [PATCH 22/31] [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 6ca48e40fe2a6201145528921c2bfbef73d8bb27 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 06:47:33 -0400 Subject: [PATCH 23/31] [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 9f302373d67ef6de2c497b0e27f61b298c82031b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 10:54:48 -0400 Subject: [PATCH 24/31] [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 b831dae8d516349ce39be8a2e84f2bd52ff79e67 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 11:08:02 -0400 Subject: [PATCH 25/31] [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 4d1613ba55a9c52fbd86d501e9b532468a2cf73e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 12:09:11 -0400 Subject: [PATCH 26/31] [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 249c1ca574ee575eb9ff9a2766275d3b54dba3ea Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 12:39:55 -0400 Subject: [PATCH 27/31] [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 62a0ad5639d4664a8e5de9bc908df28c1a4c6327 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 16:36:28 -0400 Subject: [PATCH 28/31] [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 e2f873c95cf1bcb2c03795378ff8b21e0b459091 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 22:33:30 -0400 Subject: [PATCH 29/31] [Fix] (MG_Util/ShaderTranspiler): retry a legacy shader at 460 when it fails to parse as normalized 330 core, so sources using 420-era syntax without the matching #extension line keep compiling as they did on real drivers --- MobileGL/MG_Test/Program/ProgramUtilTest.cpp | 75 +++++++++++++++++++ .../ShaderTranspiler/ShaderCompiler.cpp | 58 ++++++++++---- .../ShaderSourceProcessor.cpp | 15 ++++ .../ShaderTranspiler/ShaderSourceProcessor.h | 8 ++ 4 files changed, 142 insertions(+), 14 deletions(-) diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 1b272dd3..afbf2586 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -556,6 +556,81 @@ TEST_F(ProgramUtilTest, CompileSimpleVertexShader) { } } +// Legacy desktop sources are normalized to "#version 330 core", which is stricter than the 460 they +// used to be forced to. A shader declaring 330 while using 420-era syntax without the matching +// #extension line is accepted by real drivers, so CompileShader retries it at 460 instead of failing. +TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenLegacyVersionRejects420Syntax) { + using namespace MG_Util::ShaderTranspiler; + String source = R"(#version 330 +layout(binding = 0) uniform sampler2D InSampler; +in vec2 texCoord; +out vec4 fragColor; +void main() { + fragColor = texture(InSampler, texCoord); +})"; + PreprocessShaderSource(ShaderStage::Fragment, source); + // The normal path still emits 330 - the retry must not become the default. + ASSERT_EQ(source.find("#version 330 core"), 0u); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + if (!res) { + FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log; + } + + // Same source compiled for the OpenGL environment must take the retry too. + ShaderAttrib glAttrib{ + .shaderType = GL_FRAGMENT_SHADER, .sourceStr = source, .flags = ShaderCompileBits::CompileForOpenGL}; + auto glRes = ShaderCompiler::CompileShader(glAttrib); + if (!glRes) { + FAIL() << "errc: " << glRes.error().errc << "\nlog: " << glRes.error().log; + } +} + +TEST_F(ProgramUtilTest, CompileShaderStillFailsWithOriginalDiagnosticsWhenRetryCannotHelp) { + using namespace MG_Util::ShaderTranspiler; + String source = R"(#version 330 +in vec2 texCoord; +out vec4 fragColor; +void main() { + fragColor = thisFunctionDoesNotExist(texCoord); +})"; + PreprocessShaderSource(ShaderStage::Fragment, source); + + ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto res = ShaderCompiler::CompileShader(attrib); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().errc, -2); + EXPECT_NE(res.error().log.find("thisFunctionDoesNotExist"), String::npos) << res.error().log; +} + +TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDesktopCore) { + using namespace MG_Util::ShaderTranspiler; + + String normalized = "#version 330 core\nvoid main() {}\n"; + EXPECT_TRUE(RetargetLegacyVersionDirectiveTo460(normalized)); + EXPECT_EQ(normalized.find("#version 460 core"), 0u); + + // Already modern: nothing to retarget. + String modern = "#version 460 core\nvoid main() {}\n"; + EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(modern)); + EXPECT_EQ(modern.find("#version 460 core"), 0u); + + // ES and compatibility sources keep what they declared. + String es = "#version 300 es\nvoid main() {}\n"; + EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(es)); + EXPECT_EQ(es.find("#version 300 es"), 0u); + + String compat = "#version 330 compatibility\nvoid main() {}\n"; + EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(compat)); + EXPECT_EQ(compat.find("#version 330 compatibility"), 0u); + + // A commented-out directive is not the real one. + String commented = "// #version 330 core\nvoid main() {}\n"; + EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(commented)); + EXPECT_EQ(commented.find("#version 460"), String::npos); +} + const char* fs = R"(#version 150 uniform sampler2D InSampler; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 4833d1d0..bd60797b 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -18,6 +18,7 @@ #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" +#include "ShaderSourceProcessor.h" #include #include @@ -133,27 +134,23 @@ namespace MobileGL { return Resources; } - Result> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) { - auto shaderType = attrib.shaderType; - auto& sourceStr = attrib.sourceStr; - - auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType); - if (lang == EShLanguage::EShLangCount) { - ResultInfo r; - r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType); - r.errc = -1; - return std::unexpected(r); - } - + // One parse attempt. A glslang::TShader cannot be re-parsed, so a retry has to build a + // fresh one with byte-identical setup - hence a single factored body rather than two + // copies that could drift apart. + static Result> ParseShaderSource(EShLanguage lang, GLenum shaderType, + const String& source, + Flags flags) { SharedPtr res; auto& tshader = res; tshader = MakeShared(lang); - const char* src[] = {sourceStr.data()}; + // setStrings gets no length array, so it relies on NUL termination: source must be an + // owning buffer that outlives parse(), never a StringView's substring. + const char* src[] = {source.c_str()}; tshader->setStrings(src, 1); tshader->setNanMinMaxClamp(true); tshader->setInvertY(true); tshader->setPreamble("#undef VULKAN\n"); - if (attrib.flags & ShaderCompileBits::CompileForOpenGL) { + if (flags & ShaderCompileBits::CompileForOpenGL) { tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450); tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450); tshader->setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3); @@ -182,6 +179,39 @@ namespace MobileGL { return res; } + Result> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) { + auto shaderType = attrib.shaderType; + + auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType); + if (lang == EShLanguage::EShLangCount) { + ResultInfo r; + r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType); + r.errc = -1; + return std::unexpected(r); + } + + const String source(attrib.sourceStr); + auto result = ParseShaderSource(lang, shaderType, source, attrib.flags); + if (result) return result; + + // Legacy desktop sources are normalized to "#version 330 core", which parses under + // stricter rules than the 460 they used to be forced to: a shader declaring 330 while + // using e.g. layout(binding=...) without the matching #extension line compiles on real + // drivers but is rejected here. Retry once at 460 before reporting failure; a genuinely + // broken shader fails both attempts and keeps its original diagnostics. + String retrySource = source; + if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) { + return result; + } + + auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags); + if (!retryResult) return result; + + MGLOG_D("CompileShader: %s only parsed after retargeting its legacy #version to 460", + ConvertGLEnumToString(shaderType).c_str()); + return retryResult; + } + Result> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) { SharedPtr program = MakeShared(); for (auto& s : attrib.shaders) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index 5466f24e..87a82d69 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -633,6 +633,21 @@ namespace MobileGL { InjectDepthRangeBuiltinShim(stage, source); } + Bool RetargetLegacyVersionDirectiveTo460(String& source) { + // Re-inspect rather than searching for the literal directive: it is not necessarily at + // offset 0 (a BOM or comments may precede it) and a commented-out "#version" elsewhere + // must not be mistaken for the real one. + const ShaderLanguageInfo info = InspectShaderLanguage(source); + if (!info.HasVersionDirective()) return false; + // Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and + // compatibility shaders keep whatever they declared. + if (info.profile != ShaderProfile::Core || info.version >= 400) return false; + + source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart, + "#version 460 core\n"); + return true; + } + } // namespace ShaderTranspiler } // namespace MG_Util } // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h index f9d1c510..b86cf153 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h @@ -20,6 +20,14 @@ namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { void PreprocessShaderSource(ShaderStage stage, String& source); + + // Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down + // from a legacy desktop version back up to "#version 460 core". Returns false (leaving + // the source untouched) for anything else: ES, compatibility, or an already-modern + // declaration. Exists so a shader that only parses under the laxer 460 rules - e.g. it + // uses 420-era syntax without the matching #extension line, which real drivers tend to + // accept - can be retried instead of failing to compile. + Bool RetargetLegacyVersionDirectiveTo460(String& source); } // namespace ShaderTranspiler } // namespace MG_Util } // namespace MobileGL \ No newline at end of file From 870d882fef1739a6c4e18216a6db64fee5c39fc3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 22:41:54 -0400 Subject: [PATCH 30/31] [Fix] (MG_Backend/DirectGLES, MG_Impl/GLImpl, MG_Util): GL CTS packed_pixels + texture_swizzle readback overhaul - canonical shadow layouts for legacy sized/unsized/packed internal formats (RGB5->RGB565, RGB10/12->RGB16, RGBA2->RGBA4, RGB10_A2(UI)/RGB9_E5/R11F_G11F_B10F packed-word shadows with per-texel encode/decode incl. 5_9_9_9_REV and 10F_11F_11F_REV client types), GL_UNSIGNED_INT_10_10_10_2 pixel type mapping, conversion-first GetTexImage with CPU-shadow fallback for non-attachable formats and stale-temp-FBO detach, narrow implementation read pairs + SNORM read candidates + 2_10_10_10_REV wide-read decode with RGBA expansion, PACK image/skip and SWAP_BYTES honored on the CPU repack (never in ES), state-reset conformance (default-texture TexParameter/TexImage/TexBuffer no-ops, renderbuffer 0 unbind, vertex attrib 0 current value, ActiveTexture up to combined units, UBO binding count clamp), FramebufferTexture3D/TextureLayer slice attachments via glFramebufferTextureLayer, capability-driven FBO UNSUPPORTED for non-renderable colors, ReadPixels integer-ness mismatch error, single-value texture swizzle validation, and DirectGLES 1D/1D-array/2D-array texture emulation (2D/2D-array backend targets matching SPIRV-Cross ES 1D-as-2D shaders) --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 641 +++++++++++------- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 105 +-- MobileGL/MG_Backend/DirectGLES/Managers.h | 47 +- MobileGL/MG_Backend/DirectGLES/Utils.cpp | 75 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 4 +- .../GLImpl/Framebuffer/GL_Framebuffer.cpp | 209 +++++- .../MG_Impl/GLImpl/Framebuffer/Validators.cpp | 3 +- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 6 +- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 109 ++- .../MG_Impl/GLImpl/Texture/Validators.cpp | 4 +- MobileGL/MG_Impl/GLImpl/Texture/Validators.h | 2 + .../GLImpl/VertexArray/GL_VertexArray.cpp | 13 +- .../MG_Test/Framebuffer/FramebufferTest.cpp | 83 +++ MobileGL/MG_Test/Texture/TextureTest.cpp | 222 ++++++ .../MG_Test/VertexArray/VertexArrayTest.cpp | 5 +- .../GLToMG/TextureEnumConverter.cpp | 2 + MobileGL/MG_Util/Math/SmallFloat.h | 116 ++++ MobileGL/MG_Util/Metrics/TextureMetrics.cpp | 10 +- .../MG_Util/Texture/PixelStoreProcessor.cpp | 356 +++++++++- .../MG_Util/Texture/PixelStoreProcessor.h | 8 + .../Texture/TextureFormatProcessor.cpp | 82 ++- 21 files changed, 1683 insertions(+), 419 deletions(-) create mode 100644 MobileGL/MG_Util/Math/SmallFloat.h diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 35bcc6bb..342d9924 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1036,7 +1036,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get()); if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue; - GLenum targetGL = MG_Util::ConvertTextureTargetToGLEnum(target); + GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); backendTextureIt->second->Bind(targetGL, unit); } @@ -1847,7 +1847,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!exist) { backendObj = MakeShared(); } - backendObj->Bind(target, unit); + backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit); } return true; } @@ -2137,10 +2137,10 @@ namespace MobileGL::MG_Backend::DirectGLES { continue; } - GLenum textureTarget = - MG_Util::ConvertTextureUploadTargetToGLEnum(attachmentObject.GetTextureUploadTarget()); + GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( + attachmentObject.GetTextureUploadTarget()); if (textureTarget == GL_UNKNOWN_MGL) { - textureTarget = MG_Util::ConvertTextureTargetToGLEnum(texture->GetTarget()); + textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(texture->GetTarget()); } const GLuint backendFBOId = backendFBO->GetBackendFramebufferId(); @@ -2637,7 +2637,9 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - backendTexture->Bind(target, unitIndex); + const GLenum backendTarget = + TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target)); + backendTexture->Bind(backendTarget, unitIndex); DebugImpl::ErrorLopper::Clear(); // ANGLE/Mesa may validate the currently bound FBO while generating mipmaps. // Also detach the source texture from synced FBO objects for ANGLE's validation. @@ -2645,8 +2647,8 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Clear(); // Bind a complete internal FBO that does not reference the source texture. ScopedCompleteFramebufferBinding completeFramebuffer; - g_GLESFuncs.glGenerateMipmap(target); - RecordGLError("glGenerateMipmap", target, texture->GetFormat()); + g_GLESFuncs.glGenerateMipmap(backendTarget); + RecordGLError("glGenerateMipmap", backendTarget, texture->GetFormat()); } const GLubyte* GetString(GLenum name) { @@ -3068,88 +3070,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); } - static Int GetFloatReadbackChannelCount(GLenum format) { - switch (format) { - case GL_RED: - return 1; - case GL_RGBA: - return 4; - default: - return 0; - } - } - - static Bool ReadPixelsFloatViaUnsignedByte(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, - void* pixels) { - if (width <= 0 || height <= 0) { - return true; - } - const Int dstChannels = GetFloatReadbackChannelCount(format); - if (dstChannels == 0) { - return false; - } - - const GLenum readFormat = format == GL_RED ? GL_RED : GL_RGBA; - const Int readChannels = format == GL_RED ? 1 : 4; - Vector raw(static_cast(width) * static_cast(height) * - static_cast(readChannels)); - - GLint prevPixelPackBuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - g_GLESFuncs.glReadPixels(x, y, width, height, readFormat, GL_UNSIGNED_BYTE, raw.data()); - const GLenum readError = g_GLESFuncs.glGetError(); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); - if (readError != GL_NO_ERROR) { - MGLOG_E("ReadPixels: GL_FLOAT fallback read failed: %s", - MG_Util::ConvertGLEnumToString(readError).c_str()); - return true; - } - - const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); - const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); - const SizeT dstPixelBytes = static_cast(dstChannels) * sizeof(Float); - const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT dstOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + - static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; - const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + - static_cast(width) * dstPixelBytes; - Vector packed(packedSize, 0); - - for (GLsizei row = 0; row < height; ++row) { - const Uint8* srcRow = raw.data() + static_cast(row) * static_cast(width) * - static_cast(readChannels); - auto* dstRow = reinterpret_cast(packed.data() + dstOffset + - static_cast(row) * dstRowStride); - for (GLsizei col = 0; col < width; ++col) { - const Uint8* src = srcRow + static_cast(col) * static_cast(readChannels); - Float* dst = dstRow + static_cast(col) * static_cast(dstChannels); - // TODO: extend readback packing to all desktop GL read formats instead of only normalized RED/RGBA. - for (Int component = 0; component < dstChannels; ++component) { - dst[component] = static_cast(src[component]) / 255.0f; - } - } - } - - const auto& pixelPackBufferObject = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (pixelPackBufferObject) { - const SizeT pboOffset = reinterpret_cast(pixels); - if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) { - MGLOG_E("ReadPixels: GL_FLOAT fallback PBO is too small"); - return true; - } - pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); - } else if (pixels != nullptr && !packed.empty()) { - Memcpy(pixels, packed.data(), packed.size()); - } - return true; - } - static Bool ReadPixelsDepthFloatViaUnsignedInt(GLint x, GLint y, GLsizei width, GLsizei height, void* pixels) { if (width <= 0 || height <= 0) { return true; @@ -3180,29 +3100,29 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + static_cast(width) * dstPixelBytes; - Vector packed(packedSize, 0); - - for (GLsizei row = 0; row < height; ++row) { - const Uint32* srcRow = raw.data() + static_cast(row) * static_cast(width); - auto* dstRow = reinterpret_cast(packed.data() + dstOffset + - static_cast(row) * dstRowStride); - for (GLsizei col = 0; col < width; ++col) { - // TODO: preserve native depth precision when GLES exposes float depth readback directly. - dstRow[col] = static_cast(static_cast(srcRow[col]) / 4294967295.0); - } - } - + // Only actual pixel rows are written so PACK skip/row-length gap regions stay untouched. const auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (pixelPackBufferObject) { - const SizeT pboOffset = reinterpret_cast(pixels); - if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) { - MGLOG_E("ReadPixels: depth GL_FLOAT fallback PBO is too small"); - return true; + const SizeT pboOffset = reinterpret_cast(pixels); + if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { + MGLOG_E("ReadPixels: depth GL_FLOAT fallback PBO is too small"); + return true; + } + Vector rowBuf(static_cast(width)); + for (GLsizei row = 0; row < height; ++row) { + const Uint32* srcRow = raw.data() + static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + // TODO: preserve native depth precision when GLES exposes float depth readback directly. + rowBuf[col] = static_cast(static_cast(srcRow[col]) / 4294967295.0); + } + const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend( + {rowBuf.data(), static_cast(width) * sizeof(Float)}, pboOffset + rowOffset); + } else if (pixels != nullptr) { + Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), + static_cast(width) * sizeof(Float)); } - pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); - } else if (pixels != nullptr && !packed.empty()) { - Memcpy(pixels, packed.data(), packed.size()); } return true; } @@ -3237,29 +3157,29 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + static_cast(width) * dstPixelBytes; - Vector packed(packedSize, 0); - - for (GLsizei row = 0; row < height; ++row) { - const Uint8* srcRow = raw.data() + static_cast(row) * static_cast(width); - auto* dstRow = reinterpret_cast(packed.data() + dstOffset + - static_cast(row) * dstRowStride); - for (GLsizei col = 0; col < width; ++col) { - // TODO: switch to native uint stencil readback if the GLES backend exposes it. - dstRow[col] = srcRow[col]; - } - } - + // Only actual pixel rows are written so PACK skip/row-length gap regions stay untouched. const auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (pixelPackBufferObject) { - const SizeT pboOffset = reinterpret_cast(pixels); - if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) { - MGLOG_E("ReadPixels: stencil GL_UNSIGNED_INT fallback PBO is too small"); - return true; + const SizeT pboOffset = reinterpret_cast(pixels); + if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { + MGLOG_E("ReadPixels: stencil GL_UNSIGNED_INT fallback PBO is too small"); + return true; + } + Vector rowBuf(static_cast(width)); + for (GLsizei row = 0; row < height; ++row) { + const Uint8* srcRow = raw.data() + static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + // TODO: switch to native uint stencil readback if the GLES backend exposes it. + rowBuf[col] = srcRow[col]; + } + const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend( + {rowBuf.data(), static_cast(width) * sizeof(Uint32)}, pboOffset + rowOffset); + } else if (pixels != nullptr) { + Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), + static_cast(width) * sizeof(Uint32)); } - pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); - } else if (pixels != nullptr && !packed.empty()) { - Memcpy(pixels, packed.data(), packed.size()); } return true; } @@ -3291,6 +3211,94 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + // Component-array read formats usable as (possibly narrow) wide-read sources. + static Int GetWideReadChannelCount(GLenum format) { + switch (format) { + case GL_RED: + case GL_RED_INTEGER: + return 1; + case GL_RG: + case GL_RG_INTEGER: + return 2; + case GL_RGB: + case GL_RGB_INTEGER: + return 3; + case GL_RGBA: + case GL_RGBA_INTEGER: + return 4; + default: + return 0; + } + } + + static Bool IsIntegerReadFormat(GLint format) { + return format == GL_RED_INTEGER || format == GL_RG_INTEGER || format == GL_RGB_INTEGER || + format == GL_RGBA_INTEGER; + } + + // Expands a tightly-packed narrow read (1-3 channels per texel) into the 4-channel wide RGBA + // layout ConvertWideReadbackRow expects. Missing G/B read zero; missing A reads one, encoded in + // the source component type. + static void ExpandNarrowWideRead(Vector& data, SizeT pixelCount, Int srcChannels, GLenum componentType) { + const SizeT componentSize = GetReadbackComponentSize(componentType); + if (componentSize == 0 || srcChannels <= 0 || srcChannels >= 4) { + return; + } + Uint8 zeroBits[4] = {0, 0, 0, 0}; + Uint8 oneBits[4] = {0, 0, 0, 0}; + switch (componentType) { + case GL_UNSIGNED_BYTE: + oneBits[0] = 0xFF; + break; + case GL_BYTE: + oneBits[0] = 0x7F; + break; + case GL_UNSIGNED_SHORT: { + const Uint16 one = 0xFFFF; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_SHORT: { + const Int16 one = 0x7FFF; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_HALF_FLOAT: { + const Uint16 one = 0x3C00; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_FLOAT: { + const Float one = 1.0f; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_UNSIGNED_INT: + case GL_INT: { + const Uint32 one = 1; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + default: + break; + } + + Vector expanded(pixelCount * 4 * componentSize); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* src = data.data() + i * static_cast(srcChannels) * componentSize; + Uint8* dst = expanded.data() + i * 4 * componentSize; + for (Int ch = 0; ch < 4; ++ch) { + if (ch < srcChannels) { + Memcpy(dst + static_cast(ch) * componentSize, src + static_cast(ch) * componentSize, + componentSize); + } else { + Memcpy(dst + static_cast(ch) * componentSize, ch == 3 ? oneBits : zeroBits, componentSize); + } + } + } + data = std::move(expanded); + } + static void DrainESErrors() { for (Int i = 0; i < 32 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { } @@ -3314,16 +3322,14 @@ namespace MobileGL::MG_Backend::DirectGLES { return componentType != 0 ? static_cast(componentType) : GL_UNSIGNED_NORMALIZED; } - // Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's - // (format, type) layout. Returns false when the combination is not convertible (the caller keeps its - // "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op. - static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, - GLenum type, void* pixels) { - ReadbackChannelMapping mapping{}; - if (!GetReadbackChannelMapping(format, mapping)) { - return false; - } - // Covers unknown types, packed field-count/format mismatches and float types on integer formats. + // Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the + // client-side PACK parameters and the bound pixel-pack buffer. `wide` holds `height` rows of + // `width` texels, 4 components x GetReadbackComponentSize(wideType) bytes each. + // honorPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply to GetTexImage of 3D + // images only; ReadPixels ignores them (GL 3.3 section 4.3.1). + static Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei height, + const ReadbackChannelMapping& mapping, GLenum type, void* pixels, + Bool honorPackImageParams) { const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); if (dstPixelBytes == 0) { return false; @@ -3332,91 +3338,20 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool isPackedType = ReadbackImpl::GetPackedReadbackLayout(type, packedLayout); const SizeT dstComponentSize = GetReadbackComponentSize(type); - if (width <= 0 || height <= 0) { - return true; - } const auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (!pixelPackBufferObject && pixels == nullptr) { - return true; - } - - const GLenum attachmentComponentType = QueryReadAttachmentComponentType(); - const Bool integerAttachment = - attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT; - if (mapping.isInteger != integerAttachment) { - MGLOG_E("Readback conversion: integer-ness of format %s does not match the read buffer, skipping", - MG_Util::ConvertGLEnumToString(format).c_str()); - return true; - } - - // Prefer the implementation-defined pair (full precision on e.g. norm16 buffers), then the - // spec-guaranteed pair for the attachment class. - GLint implFormat = 0; - GLint implType = 0; - g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); - g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); - - const GLenum wideFormat = mapping.isInteger ? GL_RGBA_INTEGER : GL_RGBA; - GLenum wideTypeCandidates[3]; - Int wideTypeCandidateCount = 0; - if (mapping.isInteger) { - if (implFormat == GL_RGBA_INTEGER && (implType == GL_INT || implType == GL_UNSIGNED_INT)) { - wideTypeCandidates[wideTypeCandidateCount++] = static_cast(implType); - } - wideTypeCandidates[wideTypeCandidateCount++] = - attachmentComponentType == GL_INT ? GL_INT : GL_UNSIGNED_INT; - } else { - if (implFormat == GL_RGBA && CanDecodeWideSourceType(static_cast(implType))) { - wideTypeCandidates[wideTypeCandidateCount++] = static_cast(implType); - } - if (attachmentComponentType == GL_FLOAT) { - wideTypeCandidates[wideTypeCandidateCount++] = GL_FLOAT; - } - wideTypeCandidates[wideTypeCandidateCount++] = GL_UNSIGNED_BYTE; - } - - GLint prevPixelPackBuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - - Vector wide; - GLenum wideType = GL_NONE; - DrainESErrors(); - for (Int i = 0; i < wideTypeCandidateCount; ++i) { - const GLenum candidate = wideTypeCandidates[i]; - Bool alreadyTried = false; - for (Int j = 0; j < i; ++j) { - alreadyTried = alreadyTried || wideTypeCandidates[j] == candidate; - } - if (alreadyTried) { - continue; - } - const SizeT candidateComponentSize = GetReadbackComponentSize(candidate); - wide.resize(static_cast(width) * static_cast(height) * 4 * candidateComponentSize); - g_GLESFuncs.glReadPixels(x, y, width, height, wideFormat, candidate, wide.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - wideType = candidate; - break; - } - } - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); - if (wideType == GL_NONE) { - MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback", - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); - return true; - } // Destination layout is computed from the client-side PACK parameters; only the actual pixel - // rows are written so skip regions of the destination stay untouched. + // rows are written so skip regions of the destination stay untouched. GL_PACK_SKIP_IMAGES + // skips whole 2D images of GL_PACK_IMAGE_HEIGHT (or `height`) rows for 3D readbacks. const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT dstSkipOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + + const SizeT imageRows = static_cast(packParams.ImageHeight > 0 ? packParams.ImageHeight : height); + const SizeT skipImages = + honorPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; + const SizeT dstSkipOffset = skipImages * imageRows * dstRowStride + + static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; @@ -3435,7 +3370,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector convertedRow(dstRowBytes); for (GLsizei row = 0; row < height; ++row) { - const Uint8* srcRow = wide.data() + static_cast(row) * static_cast(width) * srcPixelBytes; + const Uint8* srcRow = wide + static_cast(row) * static_cast(width) * srcPixelBytes; ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast(width), wideType, mapping, type); @@ -3456,13 +3391,207 @@ namespace MobileGL::MG_Backend::DirectGLES { Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); } } + return true; + } + + // Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's + // (format, type) layout. Returns false when the combination is not convertible (the caller keeps its + // "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op. + static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, + GLenum type, void* pixels, Bool honorPackImageParams = false) { + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) { + return false; + } + // Covers unknown types, packed field-count/format mismatches and float types on integer formats. + const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); + if (dstPixelBytes == 0) { + return false; + } + + if (width <= 0 || height <= 0) { + return true; + } + const auto& pixelPackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); + if (!pixelPackBufferObject && pixels == nullptr) { + return true; + } + + const GLenum attachmentComponentType = QueryReadAttachmentComponentType(); + const Bool integerAttachment = + attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT; + if (mapping.isInteger != integerAttachment) { + MGLOG_E("Readback conversion: integer-ness of format %s does not match the read buffer, skipping", + MG_Util::ConvertGLEnumToString(format).c_str()); + return true; + } + + // Prefer the implementation-defined pair (full precision on e.g. norm16 buffers, and possibly + // a narrow format like GL_RED/GL_UNSIGNED_SHORT), then the spec/extension-guaranteed pair for + // the attachment class. Narrow reads are expanded to RGBA on the CPU afterwards. + GLint implFormat = 0; + GLint implType = 0; + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); + + struct WideReadCandidate { + GLenum format; + GLenum type; + }; + WideReadCandidate candidates[4]; + Int candidateCount = 0; + if (mapping.isInteger) { + if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && IsIntegerReadFormat(implFormat) && + (implType == GL_INT || implType == GL_UNSIGNED_INT)) { + candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; + } + candidates[candidateCount++] = { + GL_RGBA_INTEGER, + attachmentComponentType == GL_INT ? static_cast(GL_INT) : static_cast(GL_UNSIGNED_INT)}; + } else { + if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && !IsIntegerReadFormat(implFormat) && + (CanDecodeWideSourceType(static_cast(implType)) || + (implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) { + candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; + } + if (attachmentComponentType == GL_FLOAT) { + candidates[candidateCount++] = {GL_RGBA, GL_FLOAT}; + } + if (attachmentComponentType == GL_SIGNED_NORMALIZED) { + // EXT_render_snorm attachments read back as RGBA/BYTE (8-bit) or RGBA/SHORT (16-bit). + candidates[candidateCount++] = {GL_RGBA, GL_SHORT}; + candidates[candidateCount++] = {GL_RGBA, GL_BYTE}; + } else { + candidates[candidateCount++] = {GL_RGBA, GL_UNSIGNED_BYTE}; + } + } + + GLint prevPixelPackBuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); + g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); + g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + + Vector wide; + GLenum wideType = GL_NONE; + GLenum readFormat = GL_NONE; + Int readChannels = 0; + DrainESErrors(); + for (Int i = 0; i < candidateCount; ++i) { + const WideReadCandidate candidate = candidates[i]; + Bool alreadyTried = false; + for (Int j = 0; j < i; ++j) { + alreadyTried = + alreadyTried || (candidates[j].format == candidate.format && candidates[j].type == candidate.type); + } + if (alreadyTried) { + continue; + } + const Int channels = GetWideReadChannelCount(candidate.format); + const SizeT candidateComponentSize = GetReadbackComponentSize(candidate.type); + wide.resize(static_cast(width) * static_cast(height) * + static_cast(channels) * candidateComponentSize); + g_GLESFuncs.glReadPixels(x, y, width, height, candidate.format, candidate.type, wide.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + wideType = candidate.type; + readFormat = candidate.format; + readChannels = channels; + break; + } + } + g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); + if (wideType == GL_NONE) { + MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); + return true; + } + + if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) { + // Unpack the packed words into a float wide buffer (full 10-bit precision on e.g. + // GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV). + const SizeT pixelCount = static_cast(width) * static_cast(height); + Vector floatWide(pixelCount * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(floatWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + Uint32 word; + Memcpy(&word, wide.data() + i * 4, sizeof(word)); + dst[i * 4 + 0] = static_cast(word & 0x3FFu) / 1023.0f; + dst[i * 4 + 1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; + dst[i * 4 + 2] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; + dst[i * 4 + 3] = static_cast((word >> 30) & 0x3u) / 3.0f; + } + wide = std::move(floatWide); + wideType = GL_FLOAT; + readChannels = 4; + } + if (readChannels < 4) { + ExpandNarrowWideRead(wide, static_cast(width) * static_cast(height), readChannels, wideType); + } + + if (!StoreWideRowsToClient(wide.data(), wideType, width, height, mapping, type, pixels, + honorPackImageParams)) { + return false; + } MGLOG_D("Readback conversion: converted %s/%s from wide %s/%s", MG_Util::ConvertGLEnumToString(format).c_str(), - MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(wideFormat).c_str(), + MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(readFormat).c_str(), MG_Util::ConvertGLEnumToString(wideType).c_str()); return true; } + // GetTexImage fallback for internal formats the ES driver cannot attach to a framebuffer + // (SNORM, RGB16, RGB9_E5, ...): decodes the canonical CPU shadow-mip storage into wide RGBA + // rows and repacks them into the client layout. Only valid while the shadow copy is + // authoritative, which holds for non-renderable formats (they can never be GPU-written). + static Bool GetTexImageViaShadowConversion(MG_State::GLState::TextureObjectMipmap* textureMipmapObject, + TextureUploadTarget uploadTarget, GLint level, GLsizei width, + GLsizei height, GLenum format, GLenum type, void* pixels) { + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) { + return false; + } + if (GetReadbackDstPixelSize(mapping, type) == 0) { + return false; + } + if (width <= 0 || height <= 0) { + return true; + } + const auto& pixelPackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); + if (!pixelPackBufferObject && pixels == nullptr) { + return true; + } + + const void* shadow = textureMipmapObject->MapMipmapData(uploadTarget, level); + if (!shadow) { + return false; + } + + Vector wide; + Bool isInteger = false; + Bool isSigned = false; + if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA( + textureMipmapObject->GetFormat(), shadow, static_cast(width) * static_cast(height), + wide, isInteger, isSigned)) { + return false; + } + if (mapping.isInteger != isInteger) { + // Spec-invalid combinations are rejected with GL errors at the state layer already. + return false; + } + const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT; + if (!StoreWideRowsToClient(wide.data(), wideType, width, height, mapping, type, pixels, + /*honorPackImageParams=*/true)) { + return false; + } + MGLOG_D("GetTexImage: converted %s/%s from the CPU shadow copy", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); + return true; + } + static Bool IsLegacyNativeReadPixelsFormat(GLenum format) { return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER || format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX; @@ -3509,7 +3638,16 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("ReadPixels: bound READ FBO is not complete"); return; } - if (!useNativeReadback) { + // ES only guarantees GL_RGBA/GL_UNSIGNED_BYTE and GL_RGBA_INTEGER/GL_(UNSIGNED_)INT for the + // matching attachment class; every other convertible color layout (including GL_RGBA/GL_FLOAT + // and legacy GL_RED reads) goes through the wide-format conversion, which picks a wide type + // the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so + // it always takes the conversion path (which swaps on the CPU). + const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes; + const Bool nativeFastPair = !packSwapBytes && + ((format == GL_RGBA && type == GL_UNSIGNED_BYTE) || + (format == GL_RGBA_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT))); + if (convertible && !nativeFastPair) { if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels)) { MGLOG_D("ReadPixels: finished via client-format conversion"); return; @@ -3528,10 +3666,6 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ReadPixels: finished via stencil GL_UNSIGNED_INT fallback"); return; } - if (type == GL_FLOAT && ReadPixelsFloatViaUnsignedByte(x, y, width, height, format, pixels)) { - MGLOG_D("ReadPixels: finished via GL_FLOAT fallback"); - return; - } // Handle PBO auto& pixelPackBufferObject = @@ -3667,18 +3801,29 @@ namespace MobileGL::MG_Backend::DirectGLES { TempFBOBinder tempFBOBinder(true); MGLOG_D("GetTexImage: glFramebufferTexture2D(level=%d)", level); - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, backendTexId, level); + // The temp FBO is reused across GetTexImage calls: detach the previous color attachment first + // so a failed attach below leaves the FBO incomplete instead of silently reading stale contents. + g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + const GLenum backendAttachTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( + MG_Util::ConvertGLEnumToTextureUploadTarget(target)); + if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) { + // ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads + // of deeper slices are served from the CPU shadow instead (see the shadow-first branch). + g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, backendTexId, level, 0); + } else { + g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, + backendTexId, level); + } MGLOG_D("GetTexImage: glReadBuffer(GL_COLOR_ATTACHMENT0)"); g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); MGLOG_D("GetTexImage: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str()); - if (fbStatus != GL_FRAMEBUFFER_COMPLETE) { - MGLOG_E("GetTexImage: READ FBO incomplete"); - MGLOG_E("GetTexImage: bound READ FBO is not complete"); - return; - } + // Non-renderable internal formats (SNORM, RGB16, RGB9_E5, ...) leave the temp FBO incomplete; + // those readbacks are served from the CPU shadow copy below instead of bailing out. + const Bool tempFBOComplete = fbStatus == GL_FRAMEBUFFER_COMPLETE; DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); @@ -3716,15 +3861,45 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y()); - if (!useNativeReadback) { - if (ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels)) { + // Prefer the client-format conversion for every convertible combination: the "native" ES pairs + // are only guaranteed for matching attachment classes (e.g. GL_RGBA/GL_UNSIGNED_INT is invalid + // for normalized attachments), while the conversion path reads a wide format that is always + // accepted and repacks on the CPU. + if (convertible) { + // 3D/array images read back every slice, but the FBO path can only read one layer: + // multi-slice reads are served from the CPU shadow (depth as extra rows, tight layout). + const GLsizei shadowRows = size.y() * std::max(size.z(), 1); + const Bool multiSlice = size.z() > 1; + if (multiSlice && + GetTexImageViaShadowConversion(textureMipmapObject, + MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(), + shadowRows, format, type, pixels)) { + MGLOG_D("GetTexImage: finished via shadow conversion"); + return; + } + if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels, + /*honorPackImageParams=*/true)) { MGLOG_D("GetTexImage: finished via client-format conversion"); return; } + if (GetTexImageViaShadowConversion(textureMipmapObject, + MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(), + shadowRows, format, type, pixels)) { + MGLOG_D("GetTexImage: finished via shadow conversion"); + return; + } + if (!tempFBOComplete) { + MGLOG_E("GetTexImage: READ FBO incomplete and no shadow copy available, skipping readback"); + return; + } MGLOG_E("GetTexImage: format %s with type %s is not implemented yet, skipping readback", MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); return; } + if (!tempFBOComplete) { + MGLOG_E("GetTexImage: bound READ FBO is not complete"); + return; + } // Handle PBO auto& pixelPackBufferObject = diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index bf3aaa7d..7c8035b3 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1476,10 +1476,13 @@ namespace MobileGL::MG_Backend::DirectGLES { return 2; case TextureInternalFormat::RGB8Snorm: case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow) + case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow) case TextureInternalFormat::RGB16Snorm: return 3; case TextureInternalFormat::RGBA8Snorm: case TextureInternalFormat::RGBA16: + case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow) case TextureInternalFormat::RGBA16Snorm: return 4; default: @@ -1574,7 +1577,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); - GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget()); + GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); auto targetInternal = stateTextureObject->GetTarget(); MGLOG_D(" Texture target for syncing is %s", MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); @@ -1695,7 +1698,7 @@ namespace MobileGL::MG_Backend::DirectGLES { auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level); bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level); - auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto* pData = (levelDirty && levelByteSize != 0) ? textureMipmapObject->MapMipmapData(uploadTarget, level) : nullptr; @@ -1706,19 +1709,22 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - switch (stateTextureObject->GetTarget()) { + const IntVec3 uploadSize = + GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize); + switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: g_GLESFuncs.glTexImage2D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(levelTexelSize.x()), static_cast(levelTexelSize.y()), + static_cast(uploadSize.x()), static_cast(uploadSize.y()), 0, glFormat, glType, uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexImage3D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(levelTexelSize.x()), static_cast(levelTexelSize.y()), - static_cast(levelTexelSize.z()), 0, glFormat, glType, uploadData); + static_cast(uploadSize.x()), static_cast(uploadSize.y()), + static_cast(uploadSize.z()), 0, glFormat, glType, uploadData); break; default: MGLOG_E("Unhandled texture target %s", @@ -1782,18 +1788,20 @@ namespace MobileGL::MG_Backend::DirectGLES { } else if (stateTextureObject->IsImmutable() || m_imageBindableStorageRequired) { DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - switch (targetInternal) { + const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize); + switch (MapToBackendTextureTarget(targetInternal)) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: g_GLESFuncs.glTexStorage2D(target, static_cast(mipmapCount), glInternalFormat, - static_cast(baseSize.x()), - static_cast(baseSize.y())); + static_cast(storageSize.x()), + static_cast(storageSize.y())); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexStorage3D(target, static_cast(mipmapCount), glInternalFormat, - static_cast(baseSize.x()), - static_cast(baseSize.y()), - static_cast(baseSize.z())); + static_cast(storageSize.x()), + static_cast(storageSize.y()), + static_cast(storageSize.z())); break; default: MGLOG_E("Unhandled immutable texture target %s", @@ -1817,7 +1825,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (levelDirty && levelByteSize != 0) { auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); - auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level); Vector convertedUploadData; const void* uploadData = PrepareNormFloatFallbackUpload( @@ -1826,20 +1834,23 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - switch (targetInternal) { + const IntVec3 uploadSize = + GetBackendUploadSize(targetInternal, levelTexelSize); + switch (MapToBackendTextureTarget(targetInternal)) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: g_GLESFuncs.glTexSubImage2D( glUploadTarget, static_cast(level), 0, 0, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), glFormat, glType, uploadData); + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), glFormat, glType, uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexSubImage3D( glUploadTarget, static_cast(level), 0, 0, 0, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), - static_cast(levelTexelSize.z()), glFormat, glType, uploadData); + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), glFormat, glType, uploadData); break; default: break; @@ -1866,7 +1877,7 @@ namespace MobileGL::MG_Backend::DirectGLES { auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level); bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level); - auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto* pData = (levelDirty && levelByteSize != 0) ? textureMipmapObject->MapMipmapData(uploadTarget, level) : nullptr; @@ -1883,22 +1894,23 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); auto textureTarget = stateTextureObject->GetTarget(); - // TODO: handle more texture types - switch (textureTarget) { + const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize); + switch (MapToBackendTextureTarget(textureTarget)) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: { g_GLESFuncs.glTexImage2D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), 0, glFormat, glType, uploadData); + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), 0, glFormat, glType, uploadData); break; } - case TextureTarget::Texture3D: { + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: { g_GLESFuncs.glTexImage3D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), - static_cast(levelTexelSize.z()), 0, glFormat, glType, uploadData); + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), 0, glFormat, glType, uploadData); break; } default: { @@ -1965,7 +1977,7 @@ namespace MobileGL::MG_Backend::DirectGLES { textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).x(), textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize); - auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); DebugImpl::ErrorLopper::Loop( [file = __FILE__, line = __LINE__, func = __func__](GLenum err) { @@ -1978,19 +1990,22 @@ namespace MobileGL::MG_Backend::DirectGLES { const void* uploadData = PrepareNormFloatFallbackUpload( textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType, convertedUploadData); - switch (stateTextureObject->GetTarget()) { + const IntVec3 uploadSize = + GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize); + switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast(level), 0, 0, - static_cast(texelSize.x()), - static_cast(texelSize.y()), glFormat, glType, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), glFormat, glType, uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast(level), 0, 0, 0, - static_cast(texelSize.x()), - static_cast(texelSize.y()), - static_cast(texelSize.z()), glFormat, glType, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), glFormat, glType, uploadData); break; default: @@ -2083,7 +2098,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); - GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget()); + GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); auto targetInternal = stateTextureObject->GetTarget(); MGLOG_D(" Texture target for syncing is %s", MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); @@ -2181,7 +2196,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); - GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget()); + GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); auto targetInternal = stateTextureObject->GetTarget(); MGLOG_D(" Texture target for syncing is %s", MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); @@ -2338,11 +2353,21 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment, backendTextureObject->GetBackendTextureId(), static_cast(attachmentObject.GetTextureLevel())); + } else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget(); + uploadTarget == TextureUploadTarget::Texture3D || + uploadTarget == TextureUploadTarget::Texture2DArray || + uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) { + // Single slice/layer of a 3D or array texture: ES has no + // glFramebufferTexture3D, layers attach via glFramebufferTextureLayer. + g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment, + backendTextureObject->GetBackendTextureId(), + static_cast(attachmentObject.GetTextureLevel()), + static_cast(attachmentObject.GetTextureLayer())); } else { - auto glTextureTarget = - MG_Util::ConvertTextureUploadTargetToGLEnum(attachmentObject.GetTextureUploadTarget()); + auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( + attachmentObject.GetTextureUploadTarget()); if (glTextureTarget == GL_UNKNOWN_MGL) { - glTextureTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()); + glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget()); } backendTextureObject->Bind(glTextureTarget); g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget, diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index c1a82b6e..3e1e9b0a 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -15,6 +15,7 @@ #include "MG_State/GLState/TextureState/TextureEnum.h" #include #include +#include namespace MobileGL::MG_Backend::DirectGLES { String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType); @@ -254,10 +255,48 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace TextureImpl { inline Bool IsSupportedTextureTarget(TextureTarget target) { - if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle || - target == TextureTarget::Texture1DArray || target == TextureTarget::Texture2DArray) - return false; - return true; + // Rectangle textures need non-normalized sampling ES cannot express; everything else is + // either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see + // MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and + // coordinate padding for 1D/1D-array shaders. + return target != TextureTarget::TextureRectangle; + } + + // ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays + // (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation. + inline TextureTarget MapToBackendTextureTarget(TextureTarget target) { + switch (target) { + case TextureTarget::Texture1D: + return TextureTarget::Texture2D; + case TextureTarget::Texture1DArray: + return TextureTarget::Texture2DArray; + default: + return target; + } + } + + inline GLenum ConvertTextureTargetToBackendGLEnum(TextureTarget target) { + return MG_Util::ConvertTextureTargetToGLEnum(MapToBackendTextureTarget(target)); + } + + inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) { + switch (uploadTarget) { + case TextureUploadTarget::Texture1D: + return GL_TEXTURE_2D; + case TextureUploadTarget::Texture1DArray: + return GL_TEXTURE_2D_ARRAY; + default: + return MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); + } + } + + // 1D arrays store layers in the state-side height; the ES 2D-array image keeps height 1 and + // moves the layer count into depth. + inline IntVec3 GetBackendUploadSize(TextureTarget stateTarget, const IntVec3& texelSize) { + if (stateTarget == TextureTarget::Texture1DArray) { + return {texelSize.x(), 1, texelSize.y()}; + } + return texelSize; } inline Bool IsMultisampleTextureTarget(TextureTarget target) { diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index f446e25a..17f33db9 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -557,38 +558,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } namespace { - // Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa - // bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero, - // +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value - // clamp to it. The mantissa is truncated (rounding mode is implementation-defined). - Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) { - const Uint32 bits = std::bit_cast(value); - const Bool negative = (bits & 0x80000000u) != 0; - const Uint32 exponent = (bits >> 23) & 0xFFu; - const Uint32 mantissa = bits & 0x7FFFFFu; - const Uint32 exponentMask = 0x1Fu << mantissaBits; - if (exponent == 0xFFu) { - if (mantissa != 0) { - return exponentMask | 1u; // NaN keeps NaN - } - return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf - } - if (negative) { - return 0u; - } - const Int32 smallExponent = static_cast(exponent) - 127 + 15; - if (smallExponent >= 31) { // above the largest finite value -> clamp to it - return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u); - } - if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero - const Uint32 fullMantissa = mantissa | 0x800000u; - const Int32 shift = (23 - mantissaBits) + 1 - smallExponent; - return shift > 23 ? 0u : fullMantissa >> shift; - } - return (static_cast(smallExponent) << mantissaBits) | - (mantissa >> (23u - static_cast(mantissaBits))); - } - void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) { switch (byteSize) { case 1: { @@ -608,43 +577,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } } // namespace - Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); } - Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); } - - // RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm - // (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31). - Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { - constexpr Int kMantissaBits = 9; - constexpr Int kExponentBias = 15; - constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B) - - Float clamped[3]; - for (Int i = 0; i < 3; ++i) { - const Float v = rgb[i]; - clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax); - } - const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2])); - - Int sharedExponent = 0; // all-zero input keeps the all-zero word - if (maxComponent > 0.0f) { - sharedExponent = std::max(-kExponentBias - 1, static_cast(std::floor(std::log2(maxComponent)))) + - 1 + kExponentBias; - const Float maxScaled = std::floor( - maxComponent / std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)) + - 0.5f); - if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead - ++sharedExponent; - } - } - - const Float scale = std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)); - Uint32 word = static_cast(sharedExponent) << 27; - for (Int i = 0; i < 3; ++i) { - const auto field = static_cast(std::floor(clamped[i] / scale + 0.5f)); - word |= std::min(field, 511u) << (i * kMantissaBits); - } - return word; - } + // Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion + // (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests. + Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); } + Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); } + Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); } void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType, const ReadbackChannelMapping& mapping, GLenum type) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index ef48452e..67ff8eed 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2150,8 +2150,8 @@ void main() { if (!supported) { // SetupDraw's pre-flight should have rejected this already; never upload a null payload. MGLOG_E("UploadAndBindVertexStreams skipped: unsupported current generic vertex attribute type: " - "program=%u location=%u type=0x%x", - program.GetExternalIndex(), location, glType); + "location=%u type=0x%x", + location, glType); return false; } diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index f987cdfa..8e00e400 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -60,6 +60,87 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } + // Whether the backend can actually attach this color format to a framebuffer. Preferred + // source of truth is the backend's probed format-capability cache (real glCheckFramebufferStatus + // probes, so extensions like EXT_render_snorm are respected). Formats a probe-less backend + // cannot answer for fall back to a conservative static list of formats no ES driver renders to: + // shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats. + // Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so + // reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal. + Bool IsColorInternalFormatRenderable(TextureInternalFormat format) { + const SizeT formatIndex = static_cast(format); + if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) { + const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities(); + const SizeT sentinelFormat = static_cast(TextureInternalFormat::RGBA8); + Bool cachePopulated = false; + for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount && !cachePopulated; + ++targetIndex) { + cachePopulated = MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][sentinelFormat], + MG_Backend::FormatCapability::Creatable); + } + if (cachePopulated) { + for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount; + ++targetIndex) { + if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex], + MG_Backend::FormatCapability::FramebufferRenderable) || + MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex], + MG_Backend::FormatCapability::FramebufferRenderable)) { + return true; + } + } + return false; + } + } + switch (format) { + case TextureInternalFormat::RGB9E5: + case TextureInternalFormat::R8Snorm: + case TextureInternalFormat::RG8Snorm: + case TextureInternalFormat::RGB8Snorm: + case TextureInternalFormat::RGBA8Snorm: + case TextureInternalFormat::R16Snorm: + case TextureInternalFormat::RG16Snorm: + case TextureInternalFormat::RGB16Snorm: + case TextureInternalFormat::RGBA16Snorm: + case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGB10: // stored as RGB16 + case TextureInternalFormat::RGB12: // stored as RGB16 + case TextureInternalFormat::RGB16F: + case TextureInternalFormat::RGB32F: + case TextureInternalFormat::RGB8I: + case TextureInternalFormat::RGB8UI: + case TextureInternalFormat::RGB16I: + case TextureInternalFormat::RGB16UI: + case TextureInternalFormat::RGB32I: + case TextureInternalFormat::RGB32UI: + case TextureInternalFormat::SRGB8: + return false; + default: + return true; + } + } + + Bool HasNonRenderableColorAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) { + const auto& attachments = framebufferObject.GetAllAttachmentObjects(); + for (SizeT i = 0; i < attachments.size(); ++i) { + const auto type = static_cast(i); + if (type < FramebufferAttachmentType::Color0 || type > FramebufferAttachmentType::Color31) { + continue; + } + const auto& attachment = attachments[i]; + if (!attachment.IsValid()) continue; + TextureInternalFormat format = TextureInternalFormat::Unknown; + if (attachment.IsTexture() && attachment.GetTexture()) { + format = attachment.GetTexture()->GetFormat(); + } else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) { + format = attachment.GetRenderbuffer()->GetInternalFormat(); + } + if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) { + return true; + } + } + return false; + } + void RecordUnsupportedFramebufferTextureAttachmentError(const char* functionName, const char* detail) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -556,6 +637,62 @@ namespace MobileGL::MG_Impl::GLImpl { return framebufferObject; } + // Attaches a single layer/slice of a 3D or array texture. The attachment model stores the layer + // index; the DirectGLES backend attaches it with glFramebufferTextureLayer. + static void AttachFramebufferTextureLayer(const char* functionName, GLenum target, GLenum attachment, + GLuint texture, GLint level, GLint layer, + TextureUploadTarget textureUploadTarget) { + if (target == GL_FRAMEBUFFER) { + target = GL_DRAW_FRAMEBUFFER; + } + if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) { + AttachFramebufferTextureLayer(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, layer, + textureUploadTarget); + AttachFramebufferTextureLayer(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, layer, + textureUploadTarget); + return; + } + + const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment); + const FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target); + if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return; + if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return; + if (!TextureImpl::ValidateTextureName(texture, true)) return; + + auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget); + auto& framebufferObject = bindingSlot.GetBoundObject(); + if (!framebufferObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", functionName, + "Framebuffer target is bound to no framebuffer object.")); + return; + } + + if (texture == 0) { + framebufferObject->Detach(attachmentType); + return; + } + + auto& textureObject = MG_State::pGLContext->GetTextureObject(texture); + if (!textureObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", functionName, + std::format("Texture object {} is not valid.", texture))); + return; + } + if (layer < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, "Layer must be non-negative.")); + return; + } + + framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer, + /*layered=*/false); + } + void FramebufferTextureLayer_State(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) { if (texture == 0) { const TextureUploadTarget detachTarget = TextureUploadTarget::Texture2D; @@ -563,10 +700,31 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - static_cast(layer); - RecordUnsupportedFramebufferTextureAttachmentError( - __func__, - "Layered framebuffer texture attachments are not represented by the current framebuffer attachment model."); + auto& textureObject = MG_State::pGLContext->GetTextureObject(texture); + if (!textureObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + std::format("Texture object {} is not valid.", texture))); + return; + } + TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown; + switch (textureObject->GetTarget()) { + case TextureTarget::Texture3D: + textureUploadTarget = TextureUploadTarget::Texture3D; + break; + case TextureTarget::Texture2DArray: + textureUploadTarget = TextureUploadTarget::Texture2DArray; + break; + case TextureTarget::Texture2DMultisampleArray: + textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray; + break; + default: + RecordUnsupportedFramebufferTextureAttachmentError( + __func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture."); + return; + } + AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget); } void FramebufferTexture3D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level, @@ -578,10 +736,15 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - static_cast(zoffset); - RecordUnsupportedFramebufferTextureAttachmentError( - __func__, - "3D framebuffer texture slice attachments are not represented by the current framebuffer attachment model."); + if (textarget != GL_TEXTURE_3D) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "FramebufferTexture3D requires GL_TEXTURE_3D.")); + return; + } + AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, zoffset, + TextureUploadTarget::Texture3D); } void FramebufferTexture2D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) { @@ -1252,6 +1415,9 @@ namespace MobileGL::MG_Impl::GLImpl { GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; } + if (HasNonRenderableColorAttachment(*framebufferObject)) { + return GL_FRAMEBUFFER_UNSUPPORTED; + } if (IsActiveBackendDirectVulkan() && IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) { return GL_FRAMEBUFFER_UNSUPPORTED; @@ -1277,6 +1443,9 @@ namespace MobileGL::MG_Impl::GLImpl { GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; } + if (HasNonRenderableColorAttachment(*framebufferObject)) { + return GL_FRAMEBUFFER_UNSUPPORTED; + } if (IsActiveBackendDirectVulkan() && IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) { return GL_FRAMEBUFFER_UNSUPPORTED; @@ -1626,8 +1795,8 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } - // Check framebuffer completeness - if (!framebufferObject->CheckCompleteness()) { + // Check framebuffer completeness (including formats the ES pipeline cannot attach) + if (!framebufferObject->CheckCompleteness() || HasNonRenderableColorAttachment(*framebufferObject)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidFramebufferOperation, MakeUnique("MG_Impl/GLImpl", "ReadPixels_State", "Framebuffer is incomplete")); @@ -1679,6 +1848,26 @@ namespace MobileGL::MG_Impl::GLImpl { "No color buffer for color format")); return false; } + + // GL 3.3 section 4.3.1: GL_INVALID_OPERATION if format is an integer format and the read + // buffer is not an integer format, or vice versa (GL CTS packed_pixels expects the error + // for every *_INTEGER readback from a normalized attachment). + const auto& readAttachment = framebufferObject->GetAttachment(readBuffer); + TextureInternalFormat attachmentFormat = TextureInternalFormat::Unknown; + if (readAttachment.IsTexture() && readAttachment.GetTexture()) { + attachmentFormat = readAttachment.GetTexture()->GetFormat(); + } else if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) { + attachmentFormat = readAttachment.GetRenderbuffer()->GetInternalFormat(); + } + if (attachmentFormat != TextureInternalFormat::Unknown && + TextureImpl::IsIntegerColorInputFormat(textureInputFormat) != + TextureImpl::IsIntegerColorInternalFormat(attachmentFormat)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "ReadPixels_State", + "Integer-ness of format does not match the read buffer")); + return false; + } } // Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp index 0ae604b6..360c8bc3 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp @@ -76,7 +76,8 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl { } Bool ValidateRenderbufferName(Uint index, Bool allowZero) { - if (index == 0 && !allowZero) { + if (index == 0) { + if (allowZero) return true; // unbind / detach never needs a live object MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName", diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 6ebe9a6a..816f6135 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -1894,7 +1894,11 @@ namespace MobileGL::MG_Impl::GLImpl { *params = dynamicParameters.MaxTextureSize; break; case GL_MAX_UNIFORM_BUFFER_BINDINGS: - *params = std::max(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings); + // Never advertise more bindings than the state layer's indexed-binding array can track + // (BufferState::BufferBindingPointCount): the GL CTS state reset calls glBindBufferBase + // on every advertised index and expects no error. + *params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings, + static_cast(MG_State::GLState::BufferBindingPointCount)); break; case GL_MAX_UNIFORM_BLOCK_SIZE: *params = dynamicParameters.MaxUniformBlockSize; diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index d0e958ea..8d74332b 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -404,13 +404,8 @@ namespace MobileGL::MG_Impl::GLImpl { "2D multisample textures must use depth 1.")); return false; } - if (textureTarget == TextureTarget::Texture2DMultisampleArray && depth == 0) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidValue, - MakeUnique("MG_Impl/GLImpl", caller, - "2D multisample array textures must have at least one layer.")); - return false; - } + // depth == 0 (like width/height == 0) deallocates the image and is not an error: + // the GL CTS state reset calls TexImage3DMultisample with all-zero sizes. const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat); if (samples > maxSamples) { @@ -557,6 +552,14 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_A: { auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname); auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param); + if (swizzleValue == TextureSwizzleParam::Unknown) { + // GL CTS texture_swizzle.api_errors: single-value TexParameter* with a value outside + // [RED, GREEN, BLUE, ALPHA, ZERO, ONE] must raise GL_INVALID_ENUM. + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", caller, "Invalid texture swizzle value.")); + return; + } textureObject->SetSwizzleParam(swizzleParam, swizzleValue); break; } @@ -734,6 +737,22 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture + // (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset + // sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay + // clean). Parameters set on default textures are accepted as a silent no-op. + const SharedPtr& GetTextureObjectByTargetForParameter( + TextureUploadTarget textureUploadTarget, TextureTarget textureTarget) { + if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) { + return TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget); + } + if (textureTarget == TextureTarget::Unknown) { + return nullTextureObject; + } + auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); + return activeUnit.GetBindingSlot(textureTarget).GetBoundObject(); + } + void GenerateMipmap_Backend(GLenum target) { MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target); } @@ -1041,7 +1060,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; switch (pname) { @@ -1074,6 +1093,12 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_A: { auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname); auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param); + if (swizzleValue == TextureSwizzleParam::Unknown) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, "Invalid texture swizzle value.")); + return; + } textureObject->SetSwizzleParam(swizzleParam, swizzleValue); break; } @@ -1120,7 +1145,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; TextureParameterObject_State(textureObject, pname, param, __func__); @@ -1136,7 +1161,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; SetTextureBorderColorFromFloats(textureObject, params); break; @@ -1144,7 +1169,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_RGBA: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; GLint signedParams[4] = {static_cast(params[0]), static_cast(params[1]), static_cast(params[2]), static_cast(params[3])}; @@ -1167,7 +1192,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; SetTextureBorderColorFromInts(textureObject, params); break; @@ -1175,7 +1200,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_RGBA: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) { return; @@ -1193,7 +1218,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_BORDER_COLOR: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; SetTextureBorderColorFromIntegerInts(textureObject, params); break; @@ -1201,7 +1226,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_SWIZZLE_RGBA: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; GLint signedParams[4] = {static_cast(params[0]), static_cast(params[1]), static_cast(params[2]), static_cast(params[3])}; @@ -1221,7 +1246,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_BORDER_COLOR: { TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; SetTextureBorderColorFromUnsignedInts(textureObject, params); break; @@ -1232,7 +1257,7 @@ namespace MobileGL::MG_Impl::GLImpl { TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // ======================= Processing ================================ - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); if (!textureObject) return; Vec4 swizzleParams; @@ -1283,6 +1308,9 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureObject = isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) : bindingSlot.GetBoundObject(); + // Respecifying the default texture (name 0) is legal GL (the GL CTS state reset resets it to + // zero size); accept it as a silent no-op since default textures carry no storage here. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (textureObject->GetStorageType() != TextureStorageType::Mipmap) { MG_State::pGLContext->RecordError( @@ -1324,6 +1352,9 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureObject = isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) : bindingSlot.GetBoundObject(); + // Respecifying the default texture (name 0) is legal GL (the GL CTS state reset resets it to + // zero size); accept it as a silent no-op since default textures carry no storage here. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (textureObject->GetStorageType() != TextureStorageType::Mipmap) { MG_State::pGLContext->RecordError( @@ -1402,6 +1433,9 @@ namespace MobileGL::MG_Impl::GLImpl { : bindingSlot.GetBoundObject(); // ===================== Error Checking ============================== + // Respecifying the default texture (name 0) is legal GL (the GL CTS state reset resets every + // default texture to zero size); accept it as a silent no-op. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!ValidateTextureMutable(textureObject, __func__)) return; @@ -1521,6 +1555,9 @@ namespace MobileGL::MG_Impl::GLImpl { : bindingSlot.GetBoundObject(); // ===================== Error Checking ============================== + // Respecifying the default texture (name 0) is legal GL (the GL CTS state reset resets every + // default texture to zero size); accept it as a silent no-op. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!ValidateTextureMutable(textureObject, __func__)) return; @@ -1625,6 +1662,8 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureObject = isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) : bindingSlot.GetBoundObject(); + // Respecifying the default texture (name 0) is legal GL; accept it as a silent no-op. + if (!isProxy && !textureObject) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!ValidateTextureMutable(textureObject, __func__)) return; @@ -1680,13 +1719,19 @@ namespace MobileGL::MG_Impl::GLImpl { if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return; // TODO: make sure `internalformat` is in one of supported format for TexBuffer - auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer); - if (!bufferObject) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", __func__, - "`buffer` is not zero and is not the name of an existing buffer object.")); - return; + // buffer == 0 is a legal detach (the GL CTS state reset calls glTexBuffer(..., 0) and + // expects no error); only a non-zero name that does not exist is GL_INVALID_OPERATION. + SharedPtr bufferObject; + if (buffer != 0) { + bufferObject = MG_State::pGLContext->GetBufferObject(buffer); + if (!bufferObject) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", __func__, + "`buffer` is not zero and is not the name of an existing buffer object.")); + return; + } } // ======================= Processing ================================ @@ -1695,6 +1740,8 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureObject = bindingSlot.GetBoundObject(); // ===================== Error Checking ============================== + // Detaching from the default texture (name 0) is a silent no-op. + if (!textureObject && buffer == 0) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (textureObject->GetStorageType() != TextureStorageType::Buffer) { MG_State::pGLContext->RecordError( @@ -2572,14 +2619,22 @@ namespace MobileGL::MG_Impl::GLImpl { void ActiveTexture_State(GLenum texture) { // ===================== Error Checking ============================== - if (texture < GL_TEXTURE0 || texture > GL_TEXTURE31) { + // The valid range is [GL_TEXTURE0, GL_TEXTURE0 + GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS): + // the GL CTS state reset iterates every advertised combined unit, so rejecting units the + // implementation itself reports would leave a sticky GL_INVALID_ENUM behind. + Int maxCombinedUnits = static_cast(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); + if (MG_Backend::pActiveBackendObject) { + maxCombinedUnits = std::min( + maxCombinedUnits, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits); + } + if (texture < GL_TEXTURE0 || static_cast(texture - GL_TEXTURE0) >= maxCombinedUnits) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique( "MG_Impl/GLImpl", "ActiveTexture_State", - std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to 31, but got " + std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, but got " "invalid enum: 0x{:X}, which may stand for unit {}.", - texture, texture - GL_TEXTURE0))); + maxCombinedUnits - 1, texture, texture - GL_TEXTURE0))); return; } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index f9713609..783b8e6c 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -175,7 +175,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } - static Bool IsIntegerColorInputFormat(TextureInputFormat format) { + Bool IsIntegerColorInputFormat(TextureInputFormat format) { return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger || format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger || format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger || @@ -183,7 +183,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { format == TextureInputFormat::AlphaInteger; } - static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) { + Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) { switch (internalFormat) { case TextureInternalFormat::R8I: case TextureInternalFormat::R8UI: diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index 32291304..e1152df6 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -23,6 +23,8 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { Bool ValidateTextureSizeRange(Int width, Int height, Int depth); Bool ValidateTextureInternalFormat(TextureInternalFormat format); Bool ValidateTextureBorderNumber(Int border); + Bool IsIntegerColorInputFormat(TextureInputFormat format); + Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat); Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type); Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format, TextureInternalFormat internalFormat, diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp index 9e40c3b0..c72e7442 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp @@ -78,15 +78,10 @@ namespace MobileGL::MG_Impl::GLImpl { } static bool ValidateCurrentVertexAttribIndex(GLuint index, const char* funcName) { - if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return false; - if (index == 0) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", funcName, - "Generic vertex attribute 0 current value cannot be modified.")); - return false; - } - return true; + // Core GL allows setting the current value of every generic attribute, including 0 + // (the GL CTS state reset calls glVertexAttrib4f(0, ...) and expects no error). + static_cast(funcName); + return VertexArrayImpl::ValidateVertexAttributeIndex(index); } static bool TryGetVertexAttribute(GLuint index, const MG_State::GLState::VertexAttribute** outAttr) { diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index c3b24c5e..c5d2c062 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -674,3 +674,86 @@ TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) { EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_5_9_9_9_REV), 0u); EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_10F_11F_11F_REV), 0u); } + +// ---- GL CTS packed_pixels readback root-cause regressions -------------------------------------- + +TEST_F(FramebufferTest, ReadPixelsRejectsIntegerFormatMismatchWithReadBuffer) { + GLuint framebuffer = 0; + GLuint texture = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); + MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8UI, 4, 4); + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0); + MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + + MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels; + Uint8 pixelStorage[4 * 4 * 4] = {}; + + // GL 3.3 section 4.3.1: normalized format on an integer read buffer -> GL_INVALID_OPERATION + // (GL CTS packed_pixels expects the error for every mismatched combination). + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // The matching integer readback stays valid. + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // And the inverse mismatch: integer format on a normalized attachment. + GLuint normalizedTexture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &normalizedTexture); + MG_Impl::GLImpl::TextureStorage2D(normalizedTexture, 1, GL_RGBA8, 4, 4); + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, normalizedTexture, 0); + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage); + EXPECT_EQ(g_readPixelsCallCount, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); +} + +TEST_F(FramebufferTest, BindRenderbufferZeroUnbindsWithoutError) { + // The GL CTS state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) and expects no error; + // name 0 used to be reported as an invalid renderbuffer name. + MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(FramebufferTest, FramebufferTexture3DAttachesSliceWithLayerTracking) { + // glFramebufferTexture3D with zoffset used to be rejected outright, leaving a sticky + // GL_INVALID_OPERATION behind (GL CTS packed_pixels varied_rectangle runs on GL_TEXTURE_3D). + GLuint framebuffer = 0; + GLuint texture = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture); + MG_Impl::GLImpl::TextureStorage3D(texture, 1, GL_RGBA8, 4, 4, 2); + + MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer); + MG_Impl::GLImpl::FramebufferTexture3D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, 0, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer); + ASSERT_NE(framebufferObject, nullptr); + const auto& attachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Color0); + ASSERT_TRUE(attachment.IsTexture()); + EXPECT_EQ(attachment.GetTextureLayer(), 1); + EXPECT_FALSE(attachment.IsLayered()); +} + +TEST_F(FramebufferTest, NonRenderableColorFormatsReportUnsupportedFramebuffer) { + // Without a probing backend the conservative list applies: RGB9_E5 is texture-only, so + // attaching it must not report GL_FRAMEBUFFER_COMPLETE (GL CTS packed_pixels rgb9_e5 expects + // read errors instead of silent unwritten readbacks). + GLuint framebuffer = 0; + GLuint texture = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); + MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGB9_E5, 4, 4); + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0); + MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + + EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_READ_FRAMEBUFFER), + static_cast(GL_FRAMEBUFFER_UNSUPPORTED)); + + Uint8 pixelStorage[4 * 4 * 4] = {}; + MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_FRAMEBUFFER_OPERATION); +} diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index ab950d29..3cdb734c 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -17,7 +17,11 @@ #include #include #include +#include +#include +#include #include +#include using namespace MobileGL; @@ -1174,3 +1178,221 @@ TEST_F(TextureTest, NormalizeDepth24Stencil8UsesPackedDepthStencilType) { EXPECT_EQ(format, GL_DEPTH_STENCIL); EXPECT_EQ(type, GL_UNSIGNED_INT_24_8); } + +// ---- GL CTS packed_pixels / texture_swizzle readback root-cause regressions -------------------- + +TEST_F(TextureTest, NormalizeLegacySizedFormatsMapToCanonicalShadowLayouts) { + struct Case { + GLenum requested; + GLenum internalFormat; + GLenum format; + GLenum type; + }; + const Case cases[] = { + // Legacy <=8-bit-per-channel formats store as UNorm8 component arrays. + {GL_R3_G3_B2, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE}, + {GL_RGB4, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE}, + {GL_RGB5, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE}, + {GL_RGBA2, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE}, + {GL_RGBA4, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE}, + {GL_RGB5_A1, GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_BYTE}, + // 10/12-bit channels store as UNorm16 component arrays. + {GL_RGB10, GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT}, + {GL_RGB12, GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT}, + {GL_RGBA12, GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT}, + // RGB10_A2UI keeps its native packed layout (was previously unhandled -> broken uploads). + {GL_RGB10_A2UI, GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV}, + }; + for (const auto& testCase : cases) { + GLenum internalFormat = 0; + GLenum format = 0; + GLenum type = 0; + MG_Util::TextureFormatProcessor::NormalizePixelFormat(testCase.requested, + PixelFormatNormalizeOptionBit::None, + &internalFormat, &format, &type); + EXPECT_EQ(internalFormat, testCase.internalFormat) << "requested 0x" << std::hex << testCase.requested; + EXPECT_EQ(format, testCase.format) << "requested 0x" << std::hex << testCase.requested; + EXPECT_EQ(type, testCase.type) << "requested 0x" << std::hex << testCase.requested; + } +} + +TEST_F(TextureTest, ConvertsUnsignedInt1010102PixelDataType) { + // GL CTS packed_pixels uploads/reads GL_UNSIGNED_INT_10_10_10_2; the GL->MG mapping was missing, + // rejecting every valid combination as GL_INVALID_ENUM. + EXPECT_EQ(MG_Util::ConvertGLEnumToTexturePixelDataType(GL_UNSIGNED_INT_10_10_10_2), + TexturePixelDataType::UnsignedInt1010102); +} + +TEST_F(TextureTest, TexParameteriRejectsInvalidSwizzleValue) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + // GL CTS texture_swizzle.api_errors: values outside [RED, GREEN, BLUE, ALPHA, ZERO, ONE] + // must raise GL_INVALID_ENUM through the single-value TexParameteri path. + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RGB); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, -1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM); + + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_ALPHA); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, DefaultTextureOperationsAreSilentNoOps) { + // The GL CTS state reset drives texture name 0 through TexParameter*/TexImage*/TexBuffer for + // every unit and target and expects glGetError() to stay clean. + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, BoundTexImage2DEncodesPackedInternalShadowWords) { + // RGB10_A2 / RGB9_E5 / R11F_G11F_B10F shadow bytes hold the ES upload word; uploads from + // component client data must encode instead of raw-copying (GL CTS packed_pixels rgb10_a2, + // rgb9_e5, r11f_g11f_b10f data comparisons). + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 rgba8[] = {255, 0, 0, 255}; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba8); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + { + const auto* stored = GetBoundTexture2DLevelBytes(texture); + Uint32 word = 0; + std::memcpy(&word, stored, sizeof(word)); + EXPECT_EQ(word & 0x3FFu, 1023u); // red = 1.0 + EXPECT_EQ((word >> 10) & 0x3FFu, 0u); // green = 0 + EXPECT_EQ((word >> 20) & 0x3FFu, 0u); // blue = 0 + EXPECT_EQ((word >> 30) & 0x3u, 3u); // alpha = 1.0 + } + + const Float rgb[] = {1.0f, 0.5f, 0.25f}; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 1, 1, 0, GL_RGB, GL_FLOAT, rgb); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + { + const auto* stored = GetBoundTexture2DLevelBytes(texture); + Uint32 word = 0; + std::memcpy(&word, stored, sizeof(word)); + EXPECT_EQ(word, MG_Util::EncodeSharedExponentRGB9E5(rgb)); + } + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R11F_G11F_B10F, 1, 1, 0, GL_RGB, GL_FLOAT, rgb); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + { + const auto* stored = GetBoundTexture2DLevelBytes(texture); + Uint32 word = 0; + std::memcpy(&word, stored, sizeof(word)); + const Uint32 expected = MG_Util::EncodeFloatToUnsignedF11(rgb[0]) | + (MG_Util::EncodeFloatToUnsignedF11(rgb[1]) << 11) | + (MG_Util::EncodeFloatToUnsignedF10(rgb[2]) << 22); + EXPECT_EQ(word, expected); + } + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, BoundTexImage2DDecodesPackedFloatSourceTypes) { + // 5_9_9_9_REV / 10F_11F_11F_REV client data uploaded into a component internal format must be + // decoded per texel (GL CTS packed_pixels uploads every RGB internal format with these types). + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Float rgb[] = {1.0f, 0.5f, 0.25f}; + const Uint32 word = MG_Util::EncodeSharedExponentRGB9E5(rgb); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, &word); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + EXPECT_EQ(stored[0], 255); // 1.0 + EXPECT_EQ(stored[1], 128); // 0.5 + EXPECT_EQ(stored[2], 64); // 0.25 + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, UnpackSwapBytesSwapsComponentsNotWholePixels) { + // GL_UNPACK_SWAP_BYTES on the identity-layout copy path used to reverse the whole pixel + // (4 bytes for GL_RG16), garbling multi-component rows (GL CTS packed_pixels varied_rectangle + // GL_UNPACK_SWAP_BYTES cases). + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint16 swapped[] = {0x3412, 0x7856}; // byte-swapped {0x1234, 0x5678} + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SWAP_BYTES, GL_TRUE); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RG16, 1, 1, 0, GL_RG, GL_UNSIGNED_SHORT, swapped); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto* stored = GetBoundTexture2DLevelBytes(texture); + Uint16 red = 0; + Uint16 green = 0; + std::memcpy(&red, stored, sizeof(red)); + std::memcpy(&green, stored + 2, sizeof(green)); + EXPECT_EQ(red, 0x1234); + EXPECT_EQ(green, 0x5678); + + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SWAP_BYTES, GL_FALSE); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, DecodeShadowDataToWideRGBACoversComponentAndPackedLayouts) { + // GetTexImage of non-renderable formats reads the CPU shadow; the decode must cover both + // component-array and packed internal layouts. + Vector wide; + Bool isInteger = false; + Bool isSigned = false; + + const Uint8 r8[] = {128}; + ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::R8, r8, 1, wide, + isInteger, isSigned)); + EXPECT_FALSE(isInteger); + { + Float rgba[4]; + std::memcpy(rgba, wide.data(), sizeof(rgba)); + EXPECT_NEAR(rgba[0], 128.0f / 255.0f, 1e-6f); + EXPECT_EQ(rgba[1], 0.0f); + EXPECT_EQ(rgba[2], 0.0f); + EXPECT_EQ(rgba[3], 1.0f); + } + + const Float rgb[] = {1.0f, 0.5f, 0.25f}; + const Uint32 e5Word = MG_Util::EncodeSharedExponentRGB9E5(rgb); + ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::RGB9E5, &e5Word, 1, + wide, isInteger, isSigned)); + EXPECT_FALSE(isInteger); + { + Float rgba[4]; + std::memcpy(rgba, wide.data(), sizeof(rgba)); + EXPECT_NEAR(rgba[0], 1.0f, 1.0f / 256.0f); + EXPECT_NEAR(rgba[1], 0.5f, 1.0f / 256.0f); + EXPECT_NEAR(rgba[2], 0.25f, 1.0f / 256.0f); + EXPECT_EQ(rgba[3], 1.0f); + } + + const Uint32 uiWord = 1023u | (511u << 10) | (255u << 20) | (2u << 30); // RGB10_A2UI + ASSERT_TRUE(MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(TextureInternalFormat::RGB10A2UI, &uiWord, 1, + wide, isInteger, isSigned)); + EXPECT_TRUE(isInteger); + EXPECT_FALSE(isSigned); + { + Uint32 rgba[4]; + std::memcpy(rgba, wide.data(), sizeof(rgba)); + EXPECT_EQ(rgba[0], 1023u); + EXPECT_EQ(rgba[1], 511u); + EXPECT_EQ(rgba[2], 255u); + EXPECT_EQ(rgba[3], 2u); + } +} diff --git a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp index 0ce7ba85..dc9736a8 100644 --- a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp +++ b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp @@ -1107,9 +1107,10 @@ TEST_F(GeneralVertexArrayTest, CurrentAttrib_PackedValidation) { GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out); EXPECT_FLOAT_EQ(out[0], 42.0f); // unchanged by the failed call - // Attribute 0 is rejected by MobileGL policy (GL_INVALID_OPERATION). + // Attribute 0's current value is settable in core GL (the GL CTS state reset calls + // glVertexAttrib4f(0, ...) on every attribute and expects no error). VertexAttribP4ui(0, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u); - EXPECT_EQ(GetError(), GL_INVALID_OPERATION); + EXPECT_EQ(GetError(), GL_NO_ERROR); // Out-of-range index -> GL_INVALID_VALUE. VertexAttribP4ui(VertexArrayImpl::GetMaxVertexAttribs(), GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u); diff --git a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp index 42ad434f..1b1dc8f5 100644 --- a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp @@ -308,6 +308,8 @@ namespace MobileGL { return TexturePixelDataType::UnsignedInt8888; case GL_UNSIGNED_INT_8_8_8_8_REV: return TexturePixelDataType::UnsignedInt8888Rev; + case GL_UNSIGNED_INT_10_10_10_2: + return TexturePixelDataType::UnsignedInt1010102; case GL_UNSIGNED_INT_10F_11F_11F_REV: return TexturePixelDataType::UnsignedInt101111Rev; case GL_UNSIGNED_INT_2_10_10_10_REV: diff --git a/MobileGL/MG_Util/Math/SmallFloat.h b/MobileGL/MG_Util/Math/SmallFloat.h new file mode 100644 index 00000000..e63c2f20 --- /dev/null +++ b/MobileGL/MG_Util/Math/SmallFloat.h @@ -0,0 +1,116 @@ +// MobileGL - MobileGL/MG_Util/Math/SmallFloat.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include + +#include +#include +#include + +namespace MobileGL::MG_Util { + // Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa + // bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero, + // +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value + // clamp to it. The mantissa is truncated (rounding mode is implementation-defined). + inline Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) { + const Uint32 bits = std::bit_cast(value); + const Bool negative = (bits & 0x80000000u) != 0; + const Uint32 exponent = (bits >> 23) & 0xFFu; + const Uint32 mantissa = bits & 0x7FFFFFu; + const Uint32 exponentMask = 0x1Fu << mantissaBits; + if (exponent == 0xFFu) { + if (mantissa != 0) { + return exponentMask | 1u; // NaN keeps NaN + } + return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf + } + if (negative) { + return 0u; + } + const Int32 smallExponent = static_cast(exponent) - 127 + 15; + if (smallExponent >= 31) { // above the largest finite value -> clamp to it + return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u); + } + if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero + const Uint32 fullMantissa = mantissa | 0x800000u; + const Int32 shift = (23 - mantissaBits) + 1 - smallExponent; + return shift > 23 ? 0u : fullMantissa >> shift; + } + return (static_cast(smallExponent) << mantissaBits) | + (mantissa >> (23u - static_cast(mantissaBits))); + } + + inline Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); } + inline Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); } + + // Decodes an unsigned small float (5-bit exponent, bias 15, mantissaBits mantissa bits). + inline Float DecodeUnsignedSmallFloatToFloat(Uint32 field, Int mantissaBits) { + const Uint32 exponent = (field >> mantissaBits) & 0x1Fu; + const Uint32 mantissa = field & ((1u << mantissaBits) - 1u); + const Float mantissaScale = 1.0f / static_cast(1u << mantissaBits); + if (exponent == 0) { + return std::exp2(-14.0f) * static_cast(mantissa) * mantissaScale; + } + if (exponent == 31) { + return mantissa == 0 ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + } + return std::exp2(static_cast(exponent) - 15.0f) * + (1.0f + static_cast(mantissa) * mantissaScale); + } + + inline Float DecodeUnsignedF11ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 6); } + inline Float DecodeUnsignedF10ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 5); } + + // RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm + // (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31). + inline Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { + constexpr Int kMantissaBits = 9; + constexpr Int kExponentBias = 15; + constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B) + + Float clamped[3]; + for (Int i = 0; i < 3; ++i) { + const Float v = rgb[i]; + clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax); + } + const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2])); + + Int sharedExponent = 0; // all-zero input keeps the all-zero word + if (maxComponent > 0.0f) { + sharedExponent = std::max(-kExponentBias - 1, static_cast(std::floor(std::log2(maxComponent)))) + + 1 + kExponentBias; + const Float maxScaled = std::floor( + maxComponent / std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)) + + 0.5f); + if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead + ++sharedExponent; + } + } + + const Float scale = std::exp2(static_cast(sharedExponent - kExponentBias - kMantissaBits)); + Uint32 word = static_cast(sharedExponent) << 27; + for (Int i = 0; i < 3; ++i) { + const auto field = static_cast(std::floor(clamped[i] / scale + 0.5f)); + word |= std::min(field, 511u) << (i * kMantissaBits); + } + return word; + } + + // RGB9E5 shared-exponent decode. + inline void DecodeSharedExponentRGB9E5(Uint32 word, Float outRgb[3]) { + constexpr Int kMantissaBits = 9; + constexpr Int kExponentBias = 15; + const Int exponent = static_cast(word >> 27) - kExponentBias - kMantissaBits; + const Float scale = std::exp2(static_cast(exponent)); + for (Int i = 0; i < 3; ++i) { + outRgb[i] = static_cast((word >> (i * kMantissaBits)) & 0x1FFu) * scale; + } + } +} // namespace MobileGL::MG_Util diff --git a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp index c345ec3d..cbeb8b92 100644 --- a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp +++ b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp @@ -15,10 +15,10 @@ namespace MobileGL { SizeT GetSizedInternalFormatSizeInBytes(TextureInternalFormat internal) { switch (internal) { case TextureInternalFormat::R8: + case TextureInternalFormat::Red: // UNorm8 shadow layout case TextureInternalFormat::R8Snorm: case TextureInternalFormat::R8I: case TextureInternalFormat::R8UI: - case TextureInternalFormat::R3G3B2: return 1; case TextureInternalFormat::R16: @@ -27,14 +27,17 @@ namespace MobileGL { case TextureInternalFormat::R16UI: case TextureInternalFormat::R16F: case TextureInternalFormat::RG8: + case TextureInternalFormat::RG: // UNorm8x2 shadow layout case TextureInternalFormat::RG8Snorm: case TextureInternalFormat::RG8I: case TextureInternalFormat::RG8UI: case TextureInternalFormat::DepthComponent16: return 2; + case TextureInternalFormat::R3G3B2: // UNorm8x3 shadow layout case TextureInternalFormat::RGB4: case TextureInternalFormat::RGB5: + case TextureInternalFormat::RGB: // UNorm8x3 shadow layout case TextureInternalFormat::RGB8: case TextureInternalFormat::RGB8Snorm: case TextureInternalFormat::SRGB8: @@ -43,11 +46,10 @@ namespace MobileGL { case TextureInternalFormat::DepthComponent24: return 3; - case TextureInternalFormat::RGB10: - case TextureInternalFormat::RGB12: case TextureInternalFormat::RGBA2: case TextureInternalFormat::RGBA4: case TextureInternalFormat::RGB5A1: + case TextureInternalFormat::RGBA: // UNorm8x4 shadow layout case TextureInternalFormat::RGBA8: case TextureInternalFormat::RGBA8Snorm: case TextureInternalFormat::RGBA8I: @@ -72,6 +74,8 @@ namespace MobileGL { return 4; case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGB10: // UNorm16x3 shadow layout + case TextureInternalFormat::RGB12: // UNorm16x3 shadow layout case TextureInternalFormat::RGB16Snorm: case TextureInternalFormat::RGB16F: case TextureInternalFormat::RGB16I: diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp index a667765c..0da4b2c6 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp @@ -8,6 +8,7 @@ #include "PixelStoreProcessor.h" #include "MG_Util/Math/HalfFloat.h" +#include "MG_Util/Math/SmallFloat.h" #include namespace MobileGL::MG_Util::PixelStoreProcessor { @@ -122,13 +123,30 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) { switch (internal) { - case TextureInternalFormat::R8: out = {1, ShadowComponent::UNorm8, false}; return true; - case TextureInternalFormat::RG8: out = {2, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::R8: + case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::RG8: + case TextureInternalFormat::RG: out = {2, ShadowComponent::UNorm8, false}; return true; case TextureInternalFormat::RGB8: + case TextureInternalFormat::RGB: case TextureInternalFormat::SRGB8: out = {3, ShadowComponent::UNorm8, false}; return true; case TextureInternalFormat::RGBA8: + case TextureInternalFormat::RGBA: case TextureInternalFormat::SRGB8Alpha8: out = {4, ShadowComponent::UNorm8, false}; return true; + // Legacy desktop-GL sized normalized formats are stored in the closest ES-legal layout + // (see TextureFormatProcessor::NormalizePixelFormat): 8-bit unorm for <=8-bit channels, + // 16-bit unorm for 10/12-bit channels. + case TextureInternalFormat::R3G3B2: + case TextureInternalFormat::RGB4: + case TextureInternalFormat::RGB5: out = {3, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::RGBA2: + case TextureInternalFormat::RGBA4: + case TextureInternalFormat::RGB5A1: out = {4, ShadowComponent::UNorm8, false}; return true; + case TextureInternalFormat::RGB10: + case TextureInternalFormat::RGB12: out = {3, ShadowComponent::UNorm16, false}; return true; + case TextureInternalFormat::RGBA12: out = {4, ShadowComponent::UNorm16, false}; return true; + case TextureInternalFormat::R8Snorm: out = {1, ShadowComponent::SNorm8, false}; return true; case TextureInternalFormat::RG8Snorm: out = {2, ShadowComponent::SNorm8, false}; return true; case TextureInternalFormat::RGB8Snorm: out = {3, ShadowComponent::SNorm8, false}; return true; @@ -185,12 +203,76 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { case TextureInternalFormat::RGBA32I: out = {4, ShadowComponent::Int32, true}; return true; default: - // Packed internal layouts (RGB5A1, RGB10A2, RGB9E5, ...), depth/stencil and unsized formats - // keep the legacy copy path. + // Packed internal layouts (RGB10A2, RGB9E5, ...), depth/stencil and unsized formats + // have no component-array shadow layout (packed ones are handled below). return false; } } + // Packed internal formats whose shadow bytes hold the ES upload word directly + // (GL_UNSIGNED_INT_2_10_10_10_REV / 5_9_9_9_REV / 10F_11F_11F_REV encoding, 4 bytes/texel). + enum class PackedInternalKind { + UNorm2101010Rev, // GL_RGB10_A2 + UInt2101010Rev, // GL_RGB10_A2UI + FloatR11G11B10, // GL_R11F_G11F_B10F + FloatRGB9E5, // GL_RGB9_E5 + }; + + struct InternalPackedLayout { + PackedInternalKind kind; + Int channelCount; + Bool isInteger; + }; + + Bool GetInternalPackedLayout(TextureInternalFormat internal, InternalPackedLayout& out) { + switch (internal) { + case TextureInternalFormat::RGB10A2: + out = {PackedInternalKind::UNorm2101010Rev, 4, false}; + return true; + case TextureInternalFormat::RGB10A2UI: + out = {PackedInternalKind::UInt2101010Rev, 4, true}; + return true; + case TextureInternalFormat::R11FG11FB10F: + out = {PackedInternalKind::FloatR11G11B10, 3, false}; + return true; + case TextureInternalFormat::RGB9E5: + out = {PackedInternalKind::FloatRGB9E5, 3, false}; + return true; + default: + return false; + } + } + + Uint32 EncodePackedInternalWordFloat(PackedInternalKind kind, const Float rgba[4]) { + switch (kind) { + case PackedInternalKind::UNorm2101010Rev: { + const auto field = [](Float v, Uint32 maxValue) { + return static_cast(std::llround(std::clamp(v, 0.0f, 1.0f) * static_cast(maxValue))); + }; + return field(rgba[0], 1023u) | (field(rgba[1], 1023u) << 10) | (field(rgba[2], 1023u) << 20) | + (field(rgba[3], 3u) << 30); + } + case PackedInternalKind::FloatR11G11B10: + return EncodeFloatToUnsignedF11(rgba[0]) | (EncodeFloatToUnsignedF11(rgba[1]) << 11) | + (EncodeFloatToUnsignedF10(rgba[2]) << 22); + case PackedInternalKind::FloatRGB9E5: + return EncodeSharedExponentRGB9E5(rgba); + default: + return 0; + } + } + + Uint32 EncodePackedInternalWordInt(PackedInternalKind kind, const Int64 rgba[4]) { + if (kind != PackedInternalKind::UInt2101010Rev) { + return 0; + } + const auto field = [](Int64 v, Int64 maxValue) { + return static_cast(std::clamp(v, 0, maxValue)); + }; + return field(rgba[0], 1023) | (field(rgba[1], 1023) << 10) | (field(rgba[2], 1023) << 20) | + (field(rgba[3], 3) << 30); + } + struct UnpackChannelMapping { Int formatPosition[4]; // position of R,G,B,A within the input format's component list; -1 = missing Int channelCount; @@ -301,6 +383,8 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { SizeT inputPixelSize; SizeT swapGroupSize; // UNPACK_SWAP_BYTES group: packed word size, or the component size SizeT internalPixelSize; + Bool internalIsPacked; + InternalPackedLayout internalPacked; }; // Returns true when the (format, type) -> internal-format upload needs a per-texel conversion; @@ -309,10 +393,16 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { Bool GetUnpackConversionSpec(TextureInternalFormat internal, TextureInputFormat format, TexturePixelDataType type, UnpackConversionSpec& out) { InternalShadowLayout layout{}; - if (!GetInternalShadowLayout(internal, layout)) return false; + InternalPackedLayout packedInternal{}; + const Bool hasComponentLayout = GetInternalShadowLayout(internal, layout); + const Bool hasPackedInternal = !hasComponentLayout && GetInternalPackedLayout(internal, packedInternal); + if (!hasComponentLayout && !hasPackedInternal) return false; + const Bool internalIsInteger = hasComponentLayout ? layout.isInteger : packedInternal.isInteger; + const Int internalChannelCount = hasComponentLayout ? layout.channelCount : packedInternal.channelCount; + UnpackChannelMapping mapping{}; if (!GetUnpackChannelMapping(format, mapping)) return false; - if (mapping.isInteger != layout.isInteger) return false; // rejected upstream; stay safe + if (mapping.isInteger != internalIsInteger) return false; // rejected upstream; stay safe PackedTypeLayout packed{}; const Bool isPacked = GetPackedTypeLayout(type, packed); @@ -323,6 +413,13 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { type == TexturePixelDataType::UnsignedInt8888Rev) { return false; } + // The client word already equals the packed internal word (memcpy fast path). + if (hasPackedInternal && type == TexturePixelDataType::UnsignedInt2101010Rev && + (format == TextureInputFormat::RGBA || format == TextureInputFormat::RGBAInteger) && + (packedInternal.kind == PackedInternalKind::UNorm2101010Rev || + packedInternal.kind == PackedInternalKind::UInt2101010Rev)) { + return false; + } } else { ShadowComponent direct{}; const Bool hasDirect = GetDirectShadowComponentForType(type, mapping.isInteger, direct); @@ -338,25 +435,46 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { case TexturePixelDataType::HalfFloat: if (mapping.isInteger) return false; // rejected upstream break; + case TexturePixelDataType::UnsignedInt5999Rev: + case TexturePixelDataType::UnsignedInt101111Rev: + // Packed-float RGB source words (decoded in ConvertUnpackRow); only pair with + // GL_RGB, which the state layer already enforces. + if (mapping.isInteger || mapping.channelCount != 3) return false; + // The client word already equals the packed internal word. + if (hasPackedInternal && + ((packedInternal.kind == PackedInternalKind::FloatRGB9E5 && + type == TexturePixelDataType::UnsignedInt5999Rev) || + (packedInternal.kind == PackedInternalKind::FloatR11G11B10 && + type == TexturePixelDataType::UnsignedInt101111Rev))) { + return false; + } + break; default: return false; } - if (hasDirect && direct == layout.component && mapping.channelCount == layout.channelCount && - IsIdentityChannelOrder(mapping)) { + if (hasComponentLayout && hasDirect && direct == layout.component && + mapping.channelCount == layout.channelCount && IsIdentityChannelOrder(mapping)) { return false; // input already matches the shadow layout } } out.mapping = mapping; - out.internal = layout; + out.internal = hasComponentLayout + ? layout + : InternalShadowLayout{internalChannelCount, ShadowComponent::UNorm8, internalIsInteger}; out.packed = packed; out.isPacked = isPacked; out.type = type; out.inputPixelSize = GetInputBytesPerPixel(format, type); + const Bool isPackedFloatWord = type == TexturePixelDataType::UnsignedInt5999Rev || + type == TexturePixelDataType::UnsignedInt101111Rev; out.swapGroupSize = isPacked ? static_cast(packed.totalBits / 8) - : GetBaseTexturePixelDataTypeSize(type); + : (isPackedFloatWord ? 4 : GetBaseTexturePixelDataTypeSize(type)); + out.internalIsPacked = hasPackedInternal; + out.internalPacked = packedInternal; out.internalPixelSize = - static_cast(layout.channelCount) * GetShadowComponentSize(layout.component); + hasPackedInternal ? 4 + : static_cast(layout.channelCount) * GetShadowComponentSize(layout.component); return true; } @@ -564,13 +682,36 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { rgba[ch] = DecodeComponentToInt(s + static_cast(pos) * srcComponentSize, conv.type); } } + if (conv.internalIsPacked) { + const Uint32 word = EncodePackedInternalWordInt(conv.internalPacked.kind, rgba); + Memcpy(d, &word, sizeof(word)); + continue; + } for (Int ch = 0; ch < conv.internal.channelCount; ++ch) { EncodeShadowComponentInt(d + static_cast(ch) * dstComponentSize, conv.internal.component, rgba[ch]); } } else { Float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f}; - if (conv.isPacked) { + if (conv.type == TexturePixelDataType::UnsignedInt5999Rev || + conv.type == TexturePixelDataType::UnsignedInt101111Rev) { + // Packed-float RGB source word: decode the shared-exponent / small-float fields. + Uint32 word; + Memcpy(&word, s, sizeof(word)); + Float comps[3]; + if (conv.type == TexturePixelDataType::UnsignedInt5999Rev) { + DecodeSharedExponentRGB9E5(word, comps); + } else { + comps[0] = DecodeUnsignedF11ToFloat(word & 0x7FFu); + comps[1] = DecodeUnsignedF11ToFloat((word >> 11) & 0x7FFu); + comps[2] = DecodeUnsignedF10ToFloat((word >> 22) & 0x3FFu); + } + for (Int ch = 0; ch < 4; ++ch) { + const Int pos = conv.mapping.formatPosition[ch]; + if (pos < 0 || pos >= 3) continue; + rgba[ch] = comps[pos]; + } + } else if (conv.isPacked) { const Uint32 word = ReadPackedWord(s, conv.packed.totalBits); for (Int ch = 0; ch < 4; ++ch) { const Int pos = conv.mapping.formatPosition[ch]; @@ -587,6 +728,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { DecodeComponentToFloat(s + static_cast(pos) * srcComponentSize, conv.type); } } + if (conv.internalIsPacked) { + const Uint32 word = EncodePackedInternalWordFloat(conv.internalPacked.kind, rgba); + Memcpy(d, &word, sizeof(word)); + continue; + } for (Int ch = 0; ch < conv.internal.channelCount; ++ch) { EncodeShadowComponentFloat(d + static_cast(ch) * dstComponentSize, conv.internal.component, rgba[ch]); @@ -687,9 +833,16 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { } else { Memcpy(layerDst, layerSrc, static_cast(copyWidth) * pixelSize); - if (params.SwapBytes && pixelSize > 1 && !isByteType) { - MGLOG_D("%s: SwapBytes", __func__); - SwapBytes(layerDst, pixelSize, static_cast(copyWidth)); + if (params.SwapBytes && !isByteType) { + // GL_UNPACK_SWAP_BYTES swaps within each element (component or packed + // word), never across a whole multi-component pixel. + SizeT swapGroup = GetSizedTexturePixelDataTypeSize(inputDataType); + if (swapGroup == 0) swapGroup = GetBaseTexturePixelDataTypeSize(inputDataType); + if (swapGroup > 1) { + MGLOG_D("%s: SwapBytes (group %d)", __func__, static_cast(swapGroup)); + SwapBytes(layerDst, swapGroup, + static_cast(copyWidth) * pixelSize / swapGroup); + } } if (params.LSBFirst && isBitmap) { @@ -788,4 +941,177 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { return outputPixels; } + + namespace { + Float DecodeShadowComponentToFloat(const Uint8* p, ShadowComponent component) { + switch (component) { + case ShadowComponent::UNorm8: + return static_cast(*p) / 255.0f; + case ShadowComponent::SNorm8: { + Int8 v; + Memcpy(&v, p, sizeof(v)); + return std::max(static_cast(v) / 127.0f, -1.0f); + } + case ShadowComponent::UNorm16: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return static_cast(v) / 65535.0f; + } + case ShadowComponent::SNorm16: { + Int16 v; + Memcpy(&v, p, sizeof(v)); + return std::max(static_cast(v) / 32767.0f, -1.0f); + } + case ShadowComponent::Half: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return DecodeHalfBitsToFloat(v); + } + case ShadowComponent::Float32: { + Float v; + Memcpy(&v, p, sizeof(v)); + return v; + } + default: + return 0.0f; + } + } + + Int64 DecodeShadowComponentToInt(const Uint8* p, ShadowComponent component) { + switch (component) { + case ShadowComponent::UInt8: + return *p; + case ShadowComponent::Int8: { + Int8 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case ShadowComponent::UInt16: { + Uint16 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case ShadowComponent::Int16: { + Int16 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case ShadowComponent::UInt32: { + Uint32 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + case ShadowComponent::Int32: { + Int32 v; + Memcpy(&v, p, sizeof(v)); + return v; + } + default: + return 0; + } + } + } // namespace + + Bool DecodeShadowDataToWideRGBA(TextureInternalFormat internalFormat, const void* src, SizeT pixelCount, + Vector& outWide, Bool& outIsInteger, Bool& outIsSigned) { + if (!src) return false; + const Uint8* srcBytes = static_cast(src); + + InternalShadowLayout layout{}; + if (GetInternalShadowLayout(internalFormat, layout)) { + const SizeT componentSize = GetShadowComponentSize(layout.component); + const SizeT srcPixelSize = static_cast(layout.channelCount) * componentSize; + outIsInteger = layout.isInteger; + outIsSigned = layout.component == ShadowComponent::Int8 || layout.component == ShadowComponent::Int16 || + layout.component == ShadowComponent::Int32; + outWide.resize(pixelCount * 16); + if (layout.isInteger) { + auto* dst = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* s = srcBytes + i * srcPixelSize; + for (Int ch = 0; ch < 4; ++ch) { + Int64 v = ch == 3 ? 1 : 0; + if (ch < layout.channelCount) { + v = DecodeShadowComponentToInt(s + static_cast(ch) * componentSize, + layout.component); + } + if (outIsSigned) { + const auto out = static_cast(v); + Memcpy(&dst[i * 4 + ch], &out, sizeof(out)); + } else { + dst[i * 4 + ch] = static_cast(v); + } + } + } + } else { + auto* dst = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* s = srcBytes + i * srcPixelSize; + for (Int ch = 0; ch < 4; ++ch) { + Float v = ch == 3 ? 1.0f : 0.0f; + if (ch < layout.channelCount) { + v = DecodeShadowComponentToFloat(s + static_cast(ch) * componentSize, + layout.component); + } + dst[i * 4 + ch] = v; + } + } + } + return true; + } + + InternalPackedLayout packedInternal{}; + if (GetInternalPackedLayout(internalFormat, packedInternal)) { + outIsInteger = packedInternal.isInteger; + outIsSigned = false; + outWide.resize(pixelCount * 16); + if (packedInternal.isInteger) { + auto* dst = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + Uint32 word; + Memcpy(&word, srcBytes + i * 4, sizeof(word)); + dst[i * 4 + 0] = word & 0x3FFu; + dst[i * 4 + 1] = (word >> 10) & 0x3FFu; + dst[i * 4 + 2] = (word >> 20) & 0x3FFu; + dst[i * 4 + 3] = (word >> 30) & 0x3u; + } + } else { + auto* dst = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + Uint32 word; + Memcpy(&word, srcBytes + i * 4, sizeof(word)); + switch (packedInternal.kind) { + case PackedInternalKind::UNorm2101010Rev: + dst[i * 4 + 0] = static_cast(word & 0x3FFu) / 1023.0f; + dst[i * 4 + 1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; + dst[i * 4 + 2] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; + dst[i * 4 + 3] = static_cast((word >> 30) & 0x3u) / 3.0f; + break; + case PackedInternalKind::FloatR11G11B10: + dst[i * 4 + 0] = DecodeUnsignedF11ToFloat(word & 0x7FFu); + dst[i * 4 + 1] = DecodeUnsignedF11ToFloat((word >> 11) & 0x7FFu); + dst[i * 4 + 2] = DecodeUnsignedF10ToFloat((word >> 22) & 0x3FFu); + dst[i * 4 + 3] = 1.0f; + break; + case PackedInternalKind::FloatRGB9E5: { + Float rgb[3]; + DecodeSharedExponentRGB9E5(word, rgb); + dst[i * 4 + 0] = rgb[0]; + dst[i * 4 + 1] = rgb[1]; + dst[i * 4 + 2] = rgb[2]; + dst[i * 4 + 3] = 1.0f; + break; + } + default: + dst[i * 4 + 0] = dst[i * 4 + 1] = dst[i * 4 + 2] = 0.0f; + dst[i * 4 + 3] = 1.0f; + break; + } + } + } + return true; + } + + return false; + } } // namespace MobileGL::MG_Util::PixelStoreProcessor diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.h b/MobileGL/MG_Util/Texture/PixelStoreProcessor.h index 32f4ba0d..04dcf437 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.h +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.h @@ -22,4 +22,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { TextureInputFormat dstInputFormat, TexturePixelDataType dstDataType, IntVec3 dimension, Bool isBitmap, SizeT& outSize); void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector& swizzle); + + // Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU + // readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with + // 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set + // outIsInteger (outIsSigned tells signed from unsigned). Missing channels read 0 (G/B) and + // 1 / 1.0f (A). Returns false when the format has no canonical shadow layout. + Bool DecodeShadowDataToWideRGBA(TextureInternalFormat internalFormat, const void* src, SizeT pixelCount, + Vector& outWide, Bool& outIsInteger, Bool& outIsSigned); } // namespace MobileGL::MG_Util::PixelStoreProcessor diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index aa5fad0f..2b4e6857 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -19,11 +19,14 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { applicableOptions |= options & PixelFormatNormalizeOptionBit::NoDepthComponent32; break; case GL_RGBA16: + case GL_RGBA12: // stored as RGBA16 (see NormalizePixelFormat) case GL_RG16: case GL_R16: applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16; break; case GL_RGB16: + case GL_RGB10: // stored as RGB16 (see NormalizePixelFormat) + case GL_RGB12: // stored as RGB16 (see NormalizePixelFormat) applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16; applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16; break; @@ -159,6 +162,30 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { } *outInternalFormat = internalFormat; break; + // Legacy desktop-GL sized normalized formats (GL CTS packed_pixels): ES drivers reject them + // as internal formats, so store them in the closest ES-legal format with at least the same + // per-channel precision (extra precision stays inside the CTS comparison epsilon, which is + // derived from the requested format's bit widths). The upload (format, type) below matches + // the canonical shadow layout in PixelStoreProcessor (UNorm8 / UNorm16 component arrays). + case GL_R3_G3_B2: + case GL_RGB4: + case GL_RGB5: + *outInternalFormat = GL_RGB565; + break; + case GL_RGB10: + case GL_RGB12: + *outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoNorm16) || + (options & PixelFormatNormalizeOptionBit::NoRgb16) + ? GL_RGB32F + : GL_RGB16; + break; + case GL_RGBA2: + *outInternalFormat = GL_RGBA4; + break; + case GL_RGBA12: + *outInternalFormat = + (options & PixelFormatNormalizeOptionBit::NoNorm16) ? GL_RGBA32F : GL_RGBA16; + break; default: *outInternalFormat = internalFormat; break; @@ -276,6 +303,7 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { *outFormat = GL_RGB; break; case GL_SRGB8_ALPHA8: + case GL_SRGB_ALPHA: *outFormat = GL_RGBA; break; @@ -288,6 +316,24 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { case GL_RGB5_A1: *outFormat = GL_RGBA; break; + case GL_RGB10_A2UI: + *outFormat = GL_RGBA_INTEGER; + break; + + // Legacy desktop-GL sized normalized formats + case GL_R3_G3_B2: + case GL_RGB4: + case GL_RGB5: + case GL_RGB565: + case GL_RGB10: + case GL_RGB12: + *outFormat = GL_RGB; + break; + case GL_RGBA2: + case GL_RGBA4: + case GL_RGBA12: + *outFormat = GL_RGBA; + break; // Depth case GL_DEPTH_COMPONENT16: @@ -459,10 +505,44 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { *outType = GL_UNSIGNED_INT_10F_11F_11F_REV; break; case GL_RGB10_A2: + case GL_RGB10_A2UI: *outType = GL_UNSIGNED_INT_2_10_10_10_REV; break; case GL_RGB5_A1: - *outType = GL_UNSIGNED_SHORT_5_5_5_1; + // The shadow stores RGB5_A1 as UNorm8x4 (see PixelStoreProcessor); ES accepts + // GL_RGBA/GL_UNSIGNED_BYTE uploads for this internal format. + *outType = GL_UNSIGNED_BYTE; + break; + + // Legacy desktop-GL sized normalized formats: the upload type matches the canonical + // shadow layout (UNorm8 for <=8-bit channels, UNorm16 for 10/12-bit channels). + case GL_R3_G3_B2: + case GL_RGB4: + case GL_RGB5: + case GL_RGB565: + case GL_RGBA2: + case GL_RGBA4: + *outType = GL_UNSIGNED_BYTE; + break; + case GL_RGB10: + case GL_RGB12: + *outType = (options & PixelFormatNormalizeOptionBit::NoNorm16) || + (options & PixelFormatNormalizeOptionBit::NoRgb16) + ? GL_FLOAT + : GL_UNSIGNED_SHORT; + break; + case GL_RGBA12: + *outType = (options & PixelFormatNormalizeOptionBit::NoNorm16) ? GL_FLOAT : GL_UNSIGNED_SHORT; + break; + + // Unsized color formats keep their byte-per-channel client layout. + case GL_RGBA: + case GL_RGB: + case GL_RG: + case GL_RED: + case GL_SRGB: + case GL_SRGB_ALPHA: + *outType = GL_UNSIGNED_BYTE; break; // Depth From d4922cb0fb88c6dadc44ce6417fac47d6474e656 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 16 Jul 2026 22:59:11 -0400 Subject: [PATCH 31/31] [Fix] (MG_Backend, MG_Impl/GLImpl, MG_Util): make anisotropic filtering actually reachable - advertise GL_EXT/ARB_texture_filter_anisotropic only where the host driver or the samplerAnisotropy device feature supports it, answer GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT from the backend limit, and honor the sampler state on DirectVulkan (feature enable, limit clamp, LINEAR-only gate, resolved value in the sampler cache key) --- MobileGL/MG_Backend/BackendObject.h | 3 + .../DirectGLES/BackendObject_DirectGLES.cpp | 32 ++++++---- .../DirectGLES/BackendObject_DirectGLES.h | 6 +- .../BackendObject_DirectVulkan.cpp | 20 ++++++- .../DirectVulkan/BackendObject_DirectVulkan.h | 3 +- .../Renderer/VkSamplerManager.cpp | 30 +++++++--- .../DirectVulkan/Renderer/VkSamplerManager.h | 11 ++++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 7 ++- .../DirectVulkan/Renderer/VulkanRenderer.h | 4 ++ MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 11 ++++ .../BackendLoader/BackendLoaderTest.cpp | 58 +++++++++++++++++++ MobileGL/MG_Test/Texture/TextureTest.cpp | 28 +++++++++ .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 7 +++ .../MG_Util/BackendLoaders/OpenGL/Loader.h | 3 + .../MG_Util/BackendLoaders/Vulkan/Loader.cpp | 2 + .../MG_Util/BackendLoaders/Vulkan/Loader.h | 3 + MobileGL/MG_Util/SelfTest/DriverPost.cpp | 8 ++- 17 files changed, 207 insertions(+), 29 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 5032c5dc..8432f312 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -232,6 +232,9 @@ namespace MobileGL { struct DynamicBackendParameters { SizeT UniformBufferOffsetAlignment = 256; + // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically, + // which is also why the extension is not advertised in that case. + Float MaxTextureMaxAnisotropy = 1.0f; Float AliasedLineWidthRangeMin = 1.0f; Float AliasedLineWidthRangeMax = 1.0f; Float SmoothLineWidthRangeMin = 1.0f; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 0c5b62be..1ebbfdf3 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -605,9 +605,9 @@ namespace MobileGL::MG_Backend::DirectGLES { { .TargetGLVersion = {3, 3, 0}, // Target OpenGL Version .TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version - // Baseline advertisement (no timer queries yet); reconciled once - // the ES capabilities exist, see UpdateAdvertisedTimerQueryExtension. - .Extensions = BuildAdvertisedExtensions(false), + // Baseline advertisement (no timer queries / anisotropy yet); reconciled + // once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions. + .Extensions = BuildAdvertisedExtensions(false, false), .IsCompatibilityProfile = false // Is Compatibility Profile }, .StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability @@ -627,8 +627,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // thread can only observe the extension string after the // advertisement for its context has settled; rebuilding the whole // list keeps the re-run after a context recreation idempotent. - void UpdateAdvertisedTimerQueryExtension() { - MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(AreTimerQueriesSupported()); + void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) { + MutableRendererInfo().RendererGLInfo.Extensions = + BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported); } } // namespace @@ -672,11 +673,11 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } DirectGLES::SetGLESCapabilities(m_GLESCapabilities); - // Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query, - // reconcile the E_GL_ARB_timer_query advertisement (see the comment on - // UpdateAdvertisedTimerQueryExtension for why it cannot happen when - // the extension list is first built). - UpdateAdvertisedTimerQueryExtension(); + // Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and + // GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on + // UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension + // list is first built). + UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy); UpdateDynamicBackendParameters(); PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities()); PrintFormatCapabilities(GetFormatCapabilities()); @@ -818,7 +819,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return MutableRendererInfo(); } - Vector BuildAdvertisedExtensions(Bool timerQueriesSupported) { + Vector BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) { Vector extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store, @@ -836,6 +837,14 @@ namespace MobileGL::MG_Backend::DirectGLES { if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) { extensions.push_back(E_GL_ARB_timer_query); } + // Only advertised when the host ES driver actually filters anisotropically: the sampler + // state is accepted regardless, but forwarding it would be a no-op without the extension, + // and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently + // get plain trilinear. + if (anisotropicFilteringSupported) { + extensions.push_back(E_GL_EXT_texture_filter_anisotropic); + extensions.push_back(E_GL_ARB_texture_filter_anisotropic); + } return extensions; } @@ -940,6 +949,7 @@ namespace MobileGL::MG_Backend::DirectGLES { void BackendObject_DirectGLES::UpdateDynamicBackendParameters() { m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment; + m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy; m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin; m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax; m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index b13f2344..00bddb9b 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -66,9 +66,9 @@ namespace MobileGL::MG_Backend::DirectGLES { const RendererInfo& GetRendererIdentity(); // The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS)) - // for a device whose timer queries are (or are not) usable. The - // MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside. - Vector BuildAdvertisedExtensions(Bool timerQueriesSupported); + // for a device whose timer queries / anisotropic filtering are (or are not) usable. + // The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside. + Vector BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported); // Format: , OpenGL ES . — the exact string an // initialized backend returns from GetBackendAPIVersionString (and that ends up diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 4f9f4788..2a4b416f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -488,14 +488,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { .TargetGLSLVersion = {4, 6, 0}, // Baseline advertisement (no shader subgroup, no timer queries); a // live backend reconciles its copy in UpdateAdvertisedExtensions. - .Extensions = BuildAdvertisedExtensions(false, false), + .Extensions = BuildAdvertisedExtensions(false, false, false), .IsCompatibilityProfile = false }, .StaticBackendCapability = {.AllowVSOnlyPrograms = false}}; return rendererInfo; } - Vector BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported) { + Vector BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, + Bool anisotropicFilteringSupported) { Vector extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store, @@ -516,6 +517,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) { extensions.push_back(E_GL_ARB_timer_query); } + // Only advertised when the samplerAnisotropy device feature was granted: without it the + // sampler state is accepted but never applied, and an app trusting the string (LWJGL builds + // GLCapabilities from it) would think it enabled anisotropic filtering. + if (anisotropicFilteringSupported) { + extensions.push_back(E_GL_EXT_texture_filter_anisotropic); + extensions.push_back(E_GL_ARB_texture_filter_anisotropic); + } return extensions; } @@ -630,7 +638,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // run without a renderer; no timer query is advertised then. Rebuilding // the whole list keeps re-runs idempotent. m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions( - m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported()); + m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(), + pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()); } void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() { @@ -680,6 +689,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment; m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin; m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax; + // Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy) + // rather than a maximum the sampler manager will never apply. + m_dynamicParameters.MaxTextureMaxAnisotropy = + (pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy + : 1.0f; m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin; m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax; m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity; diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index f3a19816..8f81f6bc 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -73,7 +73,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and // MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass // the detected device support (passing an already-gated value is harmless). - Vector BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported); + Vector BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, + Bool anisotropicFilteringSupported); // Format: , Vulkan , Driver — the exact // string an initialized backend returns from GetBackendAPIVersionString (and that diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index 2d837a92..9b80a961 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -58,11 +58,24 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_device = initInfo.device; m_config = initInfo.config; + m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported; + m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f); MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr, "VkSamplerManager::Initialize failed: invalid initialization info"); return true; } + Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const { + if (!m_samplerAnisotropySupported) return 1.0f; + // VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to + // be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy]. + if (sampler.GetMinFilter() != SamplerFilterMode::Linear || + sampler.GetMagFilter() != SamplerFilterMode::Linear) { + return 1.0f; + } + return std::clamp(sampler.GetMaxAnisotropy(), 1.0f, m_maxSamplerAnisotropy); + } + void VkSamplerManager::Shutdown() { for (auto& [_, sampler] : m_samplers) { if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) { @@ -99,9 +112,11 @@ 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. + // The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan + // will not apply (NEAREST filtering, or requests past the device limit) must still share one + // VkSampler, while two samplers that really do differ must not collide onto the first one's. + const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler); + XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy))); const auto compareMode = sampler.GetCompareMode(); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); const auto compareFunc = ResolveCompareFunc(sampler, texture); @@ -128,10 +143,11 @@ 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; + // Must use the same resolver as BuildSamplerKey - a divergence would either collide two + // different samplers or silently create duplicates. + const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler); + samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE; + samplerInfo.maxAnisotropy = maxAnisotropy; samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE; samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture)); samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h index edd62f3c..001b11a5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h @@ -24,6 +24,10 @@ public: struct InitInfo { VkDevice device = VK_NULL_HANDLE; const VulkanRendererConfig* config = nullptr; + // The samplerAnisotropy device feature was requested and granted at vkCreateDevice. + Bool samplerAnisotropySupported = false; + // VkPhysicalDeviceLimits::maxSamplerAnisotropy. + Float maxSamplerAnisotropy = 1.0f; }; Bool Initialize(const InitInfo& initInfo); @@ -49,9 +53,16 @@ private: const MG_State::GLState::ITextureObject& texture); static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture); + // The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and + // the sampler filters linearly both ways, otherwise the GL request clamped to the device limit. + // GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly + // that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw. + Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const; VkDevice m_device = VK_NULL_HANDLE; const VulkanRendererConfig* m_config = nullptr; + Bool m_samplerAnisotropySupported = false; + Float m_maxSamplerAnisotropy = 1.0f; UnorderedMap m_samplers; static inline XXH64_state_t* m_hashState = XXH64_createState(); }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 84af9d4d..851ac679 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -1890,7 +1890,8 @@ void main() { m_samplerManager = MakeUnique(); MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed."); - succeeded = m_samplerManager->Initialize({m_device, &m_config}); + succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled, + m_physicalDevice.properties.limits.maxSamplerAnisotropy}); MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed."); succeeded = InitializeBlitResources(); MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed."); @@ -6508,6 +6509,10 @@ void main() { deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect; m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE; m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE; + // Backs GL_TEXTURE_MAX_ANISOTROPY_EXT; optional in Vulkan, so the sampler manager falls back + // to isotropic filtering (and the extension goes unadvertised) when the device lacks it. + deviceFeatures.samplerAnisotropy = supportedDeviceFeatures.samplerAnisotropy; + m_samplerAnisotropyFeatureEnabled = deviceFeatures.samplerAnisotropy == VK_TRUE; VkDeviceCreateInfo deviceCreateInfo{}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index b64de3b2..b5c7a57e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -227,6 +227,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // frontend. Timestamp support (queue timestampValidBits > 0 and a // non-zero timestampPeriod) is cached at device creation. Bool IsTimerQuerySupported() const; + // The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is + // honored rather than accepted-and-ignored. + Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; } // Ensures the frame command buffer is recording (same lazy pattern as // SetupDraw) and writes a bottom-of-pipe timestamp into the current // frame's pool. Null when unsupported or the pool is exhausted. @@ -359,6 +362,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool m_indexTypeUint8ExtensionEnabled = false; Bool m_logicOpFeatureEnabled = false; Bool m_multiDrawIndirectFeatureEnabled = false; + Bool m_samplerAnisotropyFeatureEnabled = false; Bool m_shaderDrawParametersExtensionEnabled = false; Bool m_shaderDrawParametersFeatureEnabled = false; // fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 6ebe9a6a..606ed54c 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -529,6 +529,13 @@ namespace MobileGL::MG_Impl::GLImpl { params[1] = dynamicParameters.AliasedLineWidthRangeMax; return; } + case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: { + // EXT_texture_filter_anisotropic queries this as a float; the integer path below widens + // from here, so this case is the authoritative one. + const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); + params[0] = dynamicParameters.MaxTextureMaxAnisotropy; + return; + } case GL_ALIASED_POINT_SIZE_RANGE: case GL_POINT_SIZE_RANGE: { const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); @@ -1955,6 +1962,10 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_MAX_SAMPLES: *params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples); break; + case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: + // Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2. + *params = static_cast(std::lround(dynamicParameters.MaxTextureMaxAnisotropy)); + break; default: MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname); MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum, diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index 8ee37032..87354376 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -12,6 +12,10 @@ #include #include +#include + +#include +#include #include // ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver: @@ -31,6 +35,11 @@ namespace { GLenum pendingError = GL_NO_ERROR; std::vector extensions; + // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT the fake reports, and whether it was ever asked: + // querying it on a driver without the extension would raise GL_INVALID_ENUM. + GLfloat maxTextureMaxAnisotropy = 16.0f; + bool maxTextureMaxAnisotropyQueried = false; + GLuint nextBufferId = 1; GLuint nextShaderId = 1; GLuint nextProgramId = 1; @@ -122,6 +131,10 @@ namespace { }; funcs.glGetFloatv = [](GLenum pname, GLfloat* data) { switch (pname) { + case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: + g_fake.maxTextureMaxAnisotropyQueried = true; + data[0] = g_fake.maxTextureMaxAnisotropy; + break; // Two-component range queries. case GL_ALIASED_LINE_WIDTH_RANGE: case GL_SMOOTH_LINE_WIDTH_RANGE: @@ -404,6 +417,51 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) { ExpectProbeReleasedAllObjects(); } +// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising +// it on a driver that cannot filter anisotropically would leave them silently on trilinear. +TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) { + const auto contains = [](const MobileGL::Vector& extensions, + MobileGL::GLExtension wanted) { + return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end(); + }; + + const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false); + EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic)); + EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic)); + + const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true); + EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic)); + EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic)); + + // Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature. + const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false); + EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic)); + const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true); + EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic)); + EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic)); +} + +TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) { + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + const auto funcs = MakeFakeGLESFunctions(); + + MobileGL::MG_External::GLESCapabilities absentCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs)); + // Never probed (it would be GL_INVALID_ENUM), and reported as "no anisotropy". + EXPECT_FALSE(g_fake.maxTextureMaxAnisotropyQueried); + EXPECT_FLOAT_EQ(absentCaps.MaxTextureMaxAnisotropy, 1.0f); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.maxTextureMaxAnisotropy = 16.0f; + 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(g_fake.maxTextureMaxAnisotropyQueried); + EXPECT_FLOAT_EQ(presentCaps.MaxTextureMaxAnisotropy, 16.0f); +} + TEST(TextureAnisotropyCapabilities, ExtensionPresenceIsDetectedExactly) { ResetFakeDriver(); g_fake.maxVertexSsboBlocks = 0; diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 1e6cd5b2..9ffa6561 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -122,6 +122,10 @@ namespace { return table; } const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override { + return MutableDynamicParameters(); + } + // Lets a test stand in a backend limit (e.g. GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT). + static MobileGL::MG_Backend::DynamicBackendParameters& MutableDynamicParameters() { static MobileGL::MG_Backend::DynamicBackendParameters params = {}; return params; } @@ -162,6 +166,30 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv +// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default. +TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) { + auto backend = MakeUnique(); + FormatCapabilityBackend::MutableDynamicParameters().MaxTextureMaxAnisotropy = 16.0f; + ScopedBackendOverride override(Move(backend)); + + GLfloat floatValue = 0.0f; + MG_Impl::GLImpl::GetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue); + EXPECT_FLOAT_EQ(floatValue, 16.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLint integerValue = 0; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &integerValue); + EXPECT_EQ(integerValue, 16); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // A backend without anisotropy reports the no-anisotropy floor rather than erroring. + FormatCapabilityBackend::MutableDynamicParameters().MaxTextureMaxAnisotropy = 1.0f; + MG_Impl::GLImpl::GetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &floatValue); + EXPECT_FLOAT_EQ(floatValue, 1.0f); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, TextureMaxAnisotropyDefaultsToOneAndRoundTripsWithoutRedundantVersionBumps) { 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 9b9aa97c..87caa900 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -911,6 +911,13 @@ namespace MobileGL::MG_Util::BackendLoader { glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports); glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims); glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits); + // Only legal to query once the extension has been seen in the loop above, hence not batched + // with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM. + if (caps.SupportsTextureFilterAnisotropy) { + GLfloat maxTextureMaxAnisotropy = 1.0f; + glesFuncs.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxTextureMaxAnisotropy); + caps.MaxTextureMaxAnisotropy = std::max(maxTextureMaxAnisotropy, 1.0f); + } caps.AliasedLineWidthRangeMin = aliasedLineWidthRange[0]; caps.AliasedLineWidthRangeMax = aliasedLineWidthRange[1]; caps.SmoothLineWidthRangeMin = smoothLineWidthRange[0]; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index f07f4b00..c87066e3 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1034,6 +1034,9 @@ namespace MobileGL { // GL_EXT_texture_filter_anisotropic is present, so sampler/texture // anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES. Bool SupportsTextureFilterAnisotropy = false; + // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT of the host driver; only queried when the + // extension above is present, and left at 1.0 (no anisotropy) otherwise. + Float MaxTextureMaxAnisotropy = 1.0f; Bool SupportsBaseInstance = false; // GL_EXT_disjoint_timer_query is present in the extension string. Bool SupportsDisjointTimerQuery = false; diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp index 1323087c..3105abb6 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp @@ -124,6 +124,7 @@ namespace MobileGL::MG_Util::BackendLoader { caps.UniformBufferOffsetAlignment = static_cast(p.limits.minUniformBufferOffsetAlignment); caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0]; caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1]; + caps.MaxSamplerAnisotropy = p.limits.maxSamplerAnisotropy; caps.SmoothLineWidthRangeMin = p.limits.lineWidthRange[0]; caps.SmoothLineWidthRangeMax = p.limits.lineWidthRange[1]; caps.SmoothLineWidthGranularity = p.limits.lineWidthGranularity; @@ -208,6 +209,7 @@ namespace MobileGL::MG_Util::BackendLoader { caps.UniformBufferOffsetAlignment = static_cast(properties.limits.minUniformBufferOffsetAlignment); caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0]; caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1]; + caps.MaxSamplerAnisotropy = properties.limits.maxSamplerAnisotropy; caps.SmoothLineWidthRangeMin = properties.limits.lineWidthRange[0]; caps.SmoothLineWidthRangeMax = properties.limits.lineWidthRange[1]; caps.SmoothLineWidthGranularity = properties.limits.lineWidthGranularity; diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h index 9819ea33..887d8541 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h @@ -18,6 +18,9 @@ namespace MobileGL { Int UniformBufferOffsetAlignment = 256; Float AliasedLineWidthRangeMin = 1.0f; Float AliasedLineWidthRangeMax = 1.0f; + // VkPhysicalDeviceLimits::maxSamplerAnisotropy. Whether it can be used at all depends on + // the samplerAnisotropy feature, which the renderer decides at device creation. + Float MaxSamplerAnisotropy = 1.0f; Float SmoothLineWidthRangeMin = 1.0f; Float SmoothLineWidthRangeMax = 1.0f; Float SmoothLineWidthGranularity = 1.0f; diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 82d93ee3..ddba5082 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -548,8 +548,8 @@ namespace MobileGL::MG_Util::SelfTest { if (summary.capsValid) { backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString( summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor); - advertisedExtensions = JoinAdvertisedExtensions( - MG_Backend::DirectGLES::BuildAdvertisedExtensions(summary.caps.SupportsDisjointTimerQuery)); + advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions( + summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy)); } AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString, advertisedExtensions); @@ -841,6 +841,7 @@ namespace MobileGL::MG_Util::SelfTest { String driverVersionString; // raw hex, vendor-encoded (see RunVulkanDriverPost) Bool shaderSubgroupUsable = false; Bool timerQueriesSupported = false; + Bool samplerAnisotropySupported = false; }; } // namespace @@ -1107,6 +1108,7 @@ namespace MobileGL::MG_Util::SelfTest { VkPhysicalDeviceFeatures features{}; vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features); + summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE; if (features.multiDrawIndirect == VK_TRUE) { builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands"); } else { @@ -1279,7 +1281,7 @@ namespace MobileGL::MG_Util::SelfTest { backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString( summary.deviceName, summary.apiVersionString, summary.driverVersionString); advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions( - summary.shaderSubgroupUsable, summary.timerQueriesSupported)); + summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported)); } AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString, advertisedExtensions);