From 0dbb4ceba889dedaecd021593d295846dbf0cf44 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 27 Aug 2026 05:11:16 -0400 Subject: [PATCH] [Fix] (DirectVulkan): resolve arbitrary and integer border colours through VK_EXT_custom_border_color, clamped to the sampled format --- .../Renderer/VkSamplerManager.cpp | 234 ++++++++++++++++-- .../DirectVulkan/Renderer/VkSamplerManager.h | 39 ++- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 53 +++- .../DirectVulkan/Renderer/VulkanRenderer.h | 10 + 4 files changed, 312 insertions(+), 24 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index f94f0918..d537ba56 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -21,6 +21,87 @@ namespace MobileGL::MG_Backend::DirectVulkan { sampler.GetWrapR() == SamplerWrapMode::ClampToBorder; } + // The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float + // family and an integer family and requires the sampler's choice to match the image view's + // format (a float border on an integer view, or the reverse, is undefined) - so the domain + // comes from the TEXTURE, while the value comes from whichever GL entry point wrote it. + enum class BorderColorDomain { + Float, + SignedInteger, + UnsignedInteger + }; + + BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R8I: + case TextureInternalFormat::R16I: + case TextureInternalFormat::R32I: + case TextureInternalFormat::RG8I: + case TextureInternalFormat::RG16I: + case TextureInternalFormat::RG32I: + case TextureInternalFormat::RGB8I: + case TextureInternalFormat::RGB16I: + case TextureInternalFormat::RGB32I: + case TextureInternalFormat::RGBA8I: + case TextureInternalFormat::RGBA16I: + case TextureInternalFormat::RGBA32I: + return BorderColorDomain::SignedInteger; + case TextureInternalFormat::R8UI: + case TextureInternalFormat::R16UI: + case TextureInternalFormat::R32UI: + case TextureInternalFormat::RG8UI: + case TextureInternalFormat::RG16UI: + case TextureInternalFormat::RG32UI: + case TextureInternalFormat::RGB8UI: + case TextureInternalFormat::RGB16UI: + case TextureInternalFormat::RGB32UI: + case TextureInternalFormat::RGBA8UI: + case TextureInternalFormat::RGBA16UI: + case TextureInternalFormat::RGBA32UI: + case TextureInternalFormat::RGB10A2UI: + return BorderColorDomain::UnsignedInteger; + default: + return BorderColorDomain::Float; + } + } + + Bool IsSignedNormalizedFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R8Snorm: + case TextureInternalFormat::R16Snorm: + case TextureInternalFormat::RG8Snorm: + case TextureInternalFormat::RG16Snorm: + case TextureInternalFormat::RGB8Snorm: + case TextureInternalFormat::RGB16Snorm: + case TextureInternalFormat::RGBA8Snorm: + case TextureInternalFormat::RGBA16Snorm: + return true; + default: + return false; + } + } + + // Float and half-float colour formats hold values outside [0,1] perfectly well, so their + // border colour must NOT be clamped - the RGBA32F conformance rows use a border of 1.0 and + // would be unaffected either way, but an HDR border of 4.0 must survive. + Bool IsUnclampedColorFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R16F: + case TextureInternalFormat::RG16F: + case TextureInternalFormat::RGB16F: + case TextureInternalFormat::RGBA16F: + case TextureInternalFormat::R32F: + case TextureInternalFormat::RG32F: + case TextureInternalFormat::RGB32F: + case TextureInternalFormat::RGBA32F: + case TextureInternalFormat::R11FG11FB10F: + case TextureInternalFormat::RGB9E5: + return true; + default: + return false; + } + } + Bool IsDepthTextureFormat(TextureInternalFormat format) { switch (format) { case TextureInternalFormat::DepthComponent: @@ -72,6 +153,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_config = initInfo.config; m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported; m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f); + m_customBorderColorSupported = initInfo.customBorderColorSupported; + m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers; + m_customBorderColorSamplerCount = 0; MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr, "VkSamplerManager::Initialize failed: invalid initialization info"); return true; @@ -102,6 +186,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_device = VK_NULL_HANDLE; m_config = nullptr; m_frameBoundaryCounter = 0; + m_customBorderColorSupported = false; + m_maxCustomBorderColorSamplers = 0; + m_customBorderColorSamplerCount = 0; } void VkSamplerManager::OnFrameBoundary() { @@ -123,6 +210,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) { vkDestroySampler(m_device, entry.handle, nullptr); } + if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) { + --m_customBorderColorSamplerCount; + } it = m_samplers.erase(it); } else { ++it; @@ -131,8 +221,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, - const MG_State::GLState::ITextureObject& texture, - Bool forceNearestFiltering, Bool singleLevelView) const { + Bool forceNearestFiltering, Bool singleLevelView, + const ResolvedBorderColor& borderColor) const { MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); @@ -166,8 +256,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); const auto compareFunc = sampler.GetSamplerCompareFunc(); XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc))); - const auto borderColor = ResolveVkBorderColor(sampler, texture); - XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor))); + // The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the + // colour itself: two samplers that differ only in a custom border colour carry the same enum + // and would otherwise collide onto whichever one was created first. + XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color))); + if (borderColor.isCustom) { + XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue))); + } return XXH64_digest(m_hashState); } @@ -183,7 +278,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // allocation for a genuinely single-level image) and faults the GPU - the same failure // the default-framebuffer blit shader had to work around with an explicit-LOD sample. const Bool singleLevelView = viewLevelCount == 1; - const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView); + // Resolved once and used for both the key and the create-info; see ResolvedBorderColor. + const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture); + const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor); auto it = m_samplers.find(key); if (it != m_samplers.end()) { it->second.lastUsedFrameBoundary = m_frameBoundaryCounter; @@ -211,9 +308,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Must match BuildSamplerKey's resolution exactly. samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView); samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod); - samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture); + samplerInfo.borderColor = borderColor.color; samplerInfo.unnormalizedCoordinates = VK_FALSE; + // VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because + // customBorderColorWithoutFormat was required alongside customBorderColors at device + // creation - a GL sampler object has no idea which texture it will be paired with. + VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{}; + if (borderColor.isCustom) { + customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT; + customBorderColorInfo.customBorderColor = borderColor.customValue; + customBorderColorInfo.format = VK_FORMAT_UNDEFINED; + customBorderColorInfo.pNext = samplerInfo.pNext; + samplerInfo.pNext = &customBorderColorInfo; + } + VkSampler vkSampler = VK_NULL_HANDLE; VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)"); @@ -222,6 +331,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.externalIndex = sampler.GetExternalIndex(); entry.version = sampler.GetVersion(); entry.lastUsedFrameBoundary = m_frameBoundaryCounter; + entry.usesCustomBorderColor = borderColor.isCustom; + if (entry.usesCustomBorderColor) { + ++m_customBorderColorSamplerCount; + } m_samplers[key] = entry; return vkSampler; } @@ -281,39 +394,124 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler, - const MG_State::GLState::ITextureObject& texture) { + VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor( + const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const { + ResolvedBorderColor resolved{}; if (!UsesBorderColor(sampler)) { - return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; + return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled } // Border colour is sampler state: a bound sampler object supplies its own, and a texture // with none reaches the very same value through the sampler object it owns. - const auto& borderColor = sampler.GetBorderColor(); - const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat()); + const auto format = texture.GetFormat(); + const auto domain = ResolveBorderColorDomain(format); + const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 && + m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers; - if (isDepthTexture) { + if (domain != BorderColorDomain::Float) { + // An integer image view REQUIRES an integer border colour, whatever the value is - even + // (0,0,0,1). The value itself is whichever integer form the application wrote; a float + // border on an integer texture is nonsense GL leaves undefined, so the derived integer + // representation (a plain cast) is as good an answer as any. + const auto& borderColorI = sampler.GetBorderColorI(); + const Bool allZeroRgb = borderColorI.x() == 0 && borderColorI.y() == 0 && borderColorI.z() == 0; + if (allZeroRgb && borderColorI.w() == 0) { + resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK; + return resolved; + } + if (allZeroRgb && borderColorI.w() == 1) { + resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK; + return resolved; + } + if (borderColorI.x() == 1 && borderColorI.y() == 1 && borderColorI.z() == 1 && borderColorI.w() == 1) { + resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE; + return resolved; + } + if (canUseCustom) { + resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT; + resolved.isCustom = true; + if (domain == BorderColorDomain::UnsignedInteger) { + const auto& borderColorUI = sampler.GetBorderColorUI(); + resolved.customValue.uint32[0] = borderColorUI.x(); + resolved.customValue.uint32[1] = borderColorUI.y(); + resolved.customValue.uint32[2] = borderColorUI.z(); + resolved.customValue.uint32[3] = borderColorUI.w(); + } else { + resolved.customValue.int32[0] = borderColorI.x(); + resolved.customValue.int32[1] = borderColorI.y(); + resolved.customValue.int32[2] = borderColorI.z(); + resolved.customValue.int32[3] = borderColorI.w(); + } + return resolved; + } + // No custom colour available: pick the nearest of the three integer palette entries + // rather than always answering transparent black, which is what turned an integer border + // of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright. + const Bool opaque = borderColorI.w() != 0; + const Bool bright = borderColorI.x() != 0 || borderColorI.y() != 0 || borderColorI.z() != 0; + resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK + : (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK); + return resolved; + } + + // Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's + // format, so a fixed-point or depth texture clamps it to that format's representable range + // first. Without the clamp the CTS's border of (255,255,255,255) on a GL_RGBA8 texture + // matched none of the palette entries and fell through to transparent black - every border + // texel sampled 0 where the test wanted 255. + FloatVec4 borderColor = sampler.GetBorderColor(); + if (!IsUnclampedColorFormat(format)) { + const Float lowerBound = IsSignedNormalizedFormat(format) ? -1.0f : 0.0f; + borderColor = FloatVec4(std::clamp(borderColor.x(), lowerBound, 1.0f), + std::clamp(borderColor.y(), lowerBound, 1.0f), + std::clamp(borderColor.z(), lowerBound, 1.0f), + std::clamp(borderColor.w(), lowerBound, 1.0f)); + } + + // A depth texture samples one component, so only x decides - and its alpha reads as 1. + if (IsDepthTextureFormat(format)) { if (NearlyEqual(borderColor.x(), 1.0f)) { - return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + return resolved; } if (NearlyEqual(borderColor.x(), 0.0f)) { - return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; + resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; + return resolved; } } const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) && NearlyEqual(borderColor.z(), 0.0f); if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) { - return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; + resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; + return resolved; } if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) { - return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; + resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; + return resolved; } if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) && NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) { - return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + return resolved; } - return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; + if (canUseCustom) { + resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT; + resolved.isCustom = true; + resolved.customValue.float32[0] = borderColor.x(); + resolved.customValue.float32[1] = borderColor.y(); + resolved.customValue.float32[2] = borderColor.z(); + resolved.customValue.float32[3] = borderColor.w(); + return resolved; + } + + // Nearest of the three float palette entries. Transparent black stays the answer for a + // transparent border, which is what the old unconditional fallback got right by accident. + const Bool opaque = borderColor.w() >= 0.5f; + const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f; + resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK + : (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK); + return resolved; } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h index c00484d0..2c24aee0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h @@ -28,6 +28,13 @@ public: Bool samplerAnisotropySupported = false; // VkPhysicalDeviceLimits::maxSamplerAnisotropy. Float maxSamplerAnisotropy = 1.0f; + // VK_EXT_custom_border_color was enabled with BOTH customBorderColors and + // customBorderColorWithoutFormat; see VulkanRenderer::m_customBorderColorFeatureEnabled. + Bool customBorderColorSupported = false; + // VkPhysicalDeviceCustomBorderColorPropertiesEXT::maxCustomBorderColorSamplers. A hard device + // limit on how many LIVE samplers may carry a custom border colour, so the cache counts them + // and falls back to the snapped predefined value once it is reached. + Uint32 maxCustomBorderColorSamplers = 0; }; Bool Initialize(const InitInfo& initInfo); @@ -52,6 +59,21 @@ public: // boundaries. void OnFrameBoundary(); + // What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a + // legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue` + // carries the actual components in a VkSamplerCustomBorderColorCreateInfoEXT. + // + // Resolved ONCE per GetOrCreateSampler call and threaded into both the cache key and the + // create-info, so the two cannot disagree - the same discipline the resolved anisotropy needs, + // and here it also makes the maxCustomBorderColorSamplers fallback deterministic: whether a + // custom colour was affordable is decided before the key is built, not twice with a budget + // change in between. + struct ResolvedBorderColor { + VkBorderColor color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; + VkClearColorValue customValue{}; + Bool isCustom = false; + }; + private: struct SamplerCacheEntry { VkSampler handle = VK_NULL_HANDLE; @@ -60,17 +82,18 @@ private: // Frame boundary of the last cache hit; entries idle past the // OnFrameBoundary retirement age have their VkSampler destroyed. Uint64 lastUsedFrameBoundary = 0; + // Counted against maxCustomBorderColorSamplers for as long as this entry lives. + Bool usesCustomBorderColor = false; }; - Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, - const MG_State::GLState::ITextureObject& texture, - Bool forceNearestFiltering, Bool singleLevelView) const; + Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering, + Bool singleLevelView, const ResolvedBorderColor& borderColor) const; static VkFilter ToVkFilter(SamplerFilterMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); static VkCompareOp ToVkCompareOp(SamplerCompareFunc func); - static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler, - const MG_State::GLState::ITextureObject& texture); + ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler, + const MG_State::GLState::ITextureObject& texture) const; // 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 @@ -82,6 +105,12 @@ private: const VulkanRendererConfig* m_config = nullptr; Bool m_samplerAnisotropySupported = false; Float m_maxSamplerAnisotropy = 1.0f; + Bool m_customBorderColorSupported = false; + Uint32 m_maxCustomBorderColorSamplers = 0; + // Live cache entries carrying a custom border colour. Kept in step with the entries themselves + // in exactly the three places one can appear or disappear: creation, the OnFrameBoundary sweep, + // and Shutdown. + Uint32 m_customBorderColorSamplerCount = 0; UnorderedMap m_samplers; // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. Uint64 m_frameBoundaryCounter = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index f6ec09e3..1b16e137 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3173,7 +3173,9 @@ void main() { m_samplerManager = MakeUnique(); MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed."); succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled, - m_physicalDevice.properties.limits.maxSamplerAnisotropy}); + m_physicalDevice.properties.limits.maxSamplerAnisotropy, + m_customBorderColorFeatureEnabled, + m_maxCustomBorderColorSamplers}); MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed."); succeeded = InitializeBlitResources(); MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed."); @@ -13179,6 +13181,55 @@ void main() { } } + // VK_EXT_custom_border_color: an arbitrary GL_TEXTURE_BORDER_COLOR, in float or integer form, + // instead of the four predefined VkBorderColor values. Without it a border outside + // transparent black / opaque black / opaque white has to be snapped, which is what made every + // border texel of a GL_RGBA8 texture with border (255,255,255,255) sample as 0 and what made + // an integer border of -1 come back as 0. + // + // customBorderColorWithoutFormat is required alongside customBorderColors, not merely + // preferred: a GL sampler object carries a border colour with no idea which texture it will + // be paired with, so the VkSamplerCustomBorderColorCreateInfoEXT this backend builds has to + // leave `format` VK_FORMAT_UNDEFINED. + m_customBorderColorFeatureEnabled = false; + m_maxCustomBorderColorSamplers = 0; + VkPhysicalDeviceCustomBorderColorFeaturesEXT customBorderColorFeatures{}; + customBorderColorFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT; + if (IsExtensionSupported(availableExtensions, VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME) && + getPhysicalDeviceFeatures2 != nullptr) { + VkPhysicalDeviceFeatures2 featureQuery{}; + featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + featureQuery.pNext = &customBorderColorFeatures; + getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); + if (customBorderColorFeatures.customBorderColors == VK_TRUE && + customBorderColorFeatures.customBorderColorWithoutFormat == VK_TRUE) { + if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, + VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME)) { + enabledDeviceExtensions.push_back(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME); + } + customBorderColorFeatures.pNext = const_cast(deviceCreateInfo.pNext); + deviceCreateInfo.pNext = &customBorderColorFeatures; + m_customBorderColorFeatureEnabled = true; + + if (getPhysicalDeviceProperties2 != nullptr) { + VkPhysicalDeviceCustomBorderColorPropertiesEXT customBorderColorProperties{}; + customBorderColorProperties.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_PROPERTIES_EXT; + VkPhysicalDeviceProperties2 propertyQuery{}; + propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + propertyQuery.pNext = &customBorderColorProperties; + getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery); + m_maxCustomBorderColorSamplers = customBorderColorProperties.maxCustomBorderColorSamplers; + } + MGLOG_I("Enabled optional device extension: %s (maxCustomBorderColorSamplers=%u)", + VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, m_maxCustomBorderColorSamplers); + } + } + if (!m_customBorderColorFeatureEnabled) { + MGLOG_I("%s unavailable; GL_TEXTURE_BORDER_COLOR snaps to the nearest predefined VkBorderColor", + VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME); + } + // Native subgroup topology, and VK_EXT_subgroup_size_control's // computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what // turns the derived gl_NumSubgroups (DeriveNumSubgroupsPass) from diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 67d5943d..1b75e615 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -584,6 +584,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { // needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent. Bool m_dualSrcBlendFeatureEnabled = false; Bool m_primitiveTopologyListRestartFeatureEnabled = false; + // VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only + // transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or + // an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border + // colour outside the palette has to be snapped to the nearest predefined one. Both features + // are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom + // colour without naming the image format it will be paired with, which GL's sampler objects + // cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has + // to be able to fall back to the snapped value once it is reached. + Bool m_customBorderColorFeatureEnabled = false; + Uint32 m_maxCustomBorderColorSamplers = 0; // sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e. // glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT // hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate