From e5388c0e7e04512678b289d35ee133b7a10c5e1e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 19 Jul 2026 07:57:46 -0400 Subject: [PATCH] [Fix] (MG_Backend, MG_Impl, ShaderTranspiler, MG_Test): support iterationRP custom images and storage format reinterpretation --- MobileGL/MG_Backend/BackendObject.h | 3 + .../DirectGLES/BackendObject_DirectGLES.cpp | 24 +- .../DirectGLES/BackendObject_DirectGLES.h | 1 + .../BackendObject_DirectVulkan.cpp | 23 +- .../DirectVulkan/Renderer/ProgramFactory.cpp | 184 +++++++++- .../DirectVulkan/Renderer/ProgramFactory.h | 30 +- .../DirectVulkan/Renderer/UniformManager.cpp | 124 ++++++- .../DirectVulkan/Renderer/UniformManager.h | 11 +- .../Renderer/VertexInputStateFactory.cpp | 100 +++++- .../Renderer/VertexInputStateFactory.h | 15 +- .../Renderer/VkSamplerManager.cpp | 25 +- .../DirectVulkan/Renderer/VkSamplerManager.h | 9 +- .../Renderer/VkTextureManager.cpp | 340 +++++++++++++++++- .../DirectVulkan/Renderer/VkTextureManager.h | 85 ++++- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 330 +++++++++++++++-- .../DirectVulkan/Renderer/VulkanRenderer.h | 51 ++- .../MG_Impl/GLImpl/Exporting/Definitions.cpp | 6 +- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 13 +- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 212 +++++++++++ MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h | 5 + .../BackendLoader/BackendLoaderTest.cpp | 49 ++- MobileGL/MG_Test/Program/ProgramTest.cpp | 83 +++++ MobileGL/MG_Test/Program/ProgramUtilTest.cpp | 114 ++++++ MobileGL/MG_Test/SanityTest.cpp | 271 ++++++++++++++ MobileGL/MG_Test/Texture/TextureTest.cpp | 140 ++++++++ .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 18 + .../MG_Util/BackendLoaders/OpenGL/Loader.h | 3 + .../MG_Util/BackendLoaders/Vulkan/Loader.cpp | 9 + .../MG_Util/BackendLoaders/Vulkan/Loader.h | 6 + .../ShaderTranspiler/ShaderCompiler.cpp | 158 +++++++- .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 8 + .../glslang/TMglGlslIoResolver.cpp | 19 +- .../glslang/TMglGlslIoResolver.h | 1 + 33 files changed, 2382 insertions(+), 88 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 8432f312..8c4abda9 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -272,6 +272,9 @@ namespace MobileGL { Int MaxUniformBlockSize = 16384; Int MaxImageUnits = 8; Int MaxCombinedImageUniforms = 8; + Int MaxVertexImageUniforms = 0; + Int MaxGeometryImageUniforms = 0; + Int MaxFragmentImageUniforms = 8; Int MaxComputeImageUniforms = 8; Int MaxDrawBuffers = 8; Int MaxColorAttachments = 8; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 1ebbfdf3..d44b177d 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -947,6 +947,12 @@ namespace MobileGL::MG_Backend::DirectGLES { return m_dynamicParameters; } + void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting( + const MG_External::GLESCapabilities& capabilities) { + m_GLESCapabilities = capabilities; + UpdateDynamicBackendParameters(); + } + void BackendObject_DirectGLES::UpdateDynamicBackendParameters() { m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment; m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy; @@ -1003,9 +1009,21 @@ namespace MobileGL::MG_Backend::DirectGLES { m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize; const Int maxSupportedTextureUnits = static_cast(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); - m_dynamicParameters.MaxImageUnits = std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits); - m_dynamicParameters.MaxCombinedImageUniforms = m_GLESCapabilities.MaxCombinedImageUniforms; - m_dynamicParameters.MaxComputeImageUniforms = m_GLESCapabilities.MaxComputeImageUniforms; + m_dynamicParameters.MaxImageUnits = + std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0); + m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0); + const auto clampStageImageUniforms = [this](Int stageLimit) { + return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits, + m_dynamicParameters.MaxCombinedImageUniforms}); + }; + m_dynamicParameters.MaxVertexImageUniforms = + clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms); + m_dynamicParameters.MaxGeometryImageUniforms = + clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms); + m_dynamicParameters.MaxFragmentImageUniforms = + clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms); + m_dynamicParameters.MaxComputeImageUniforms = + clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms); m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index 00bddb9b..e107ef72 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -41,6 +41,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const MG_External::GLESFunctionsTable& GetGLESFunctions() const; const MG_External::EGLFunctionsTable& GetEGLFunctions() const; + void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities); private: void UpdateDynamicBackendParameters(); diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 2a4b416f..e6a009a5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -504,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { 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_texture_storage_multisample, - E_GL_ARB_direct_state_access, + E_GL_ARB_clear_texture, 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}; @@ -746,9 +746,24 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize; m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings; m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize; - m_dynamicParameters.MaxImageUnits = std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits); - m_dynamicParameters.MaxCombinedImageUniforms = m_vulkanCaps.MaxCombinedImageUniforms; - m_dynamicParameters.MaxComputeImageUniforms = m_vulkanCaps.MaxComputeImageUniforms; + m_dynamicParameters.MaxImageUnits = + std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0); + m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0); + const Int maxPerStageImageUniforms = + std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms); + // Vulkan uses one descriptor limit for every stage, but non-compute stores/atomics are + // optional device features. VulkanRenderer enables each feature whenever the physical + // device reports it, so these are the exact limits the logical device can compile and run. + m_dynamicParameters.MaxVertexImageUniforms = + m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0; + m_dynamicParameters.MaxGeometryImageUniforms = + m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader + ? maxPerStageImageUniforms + : 0; + m_dynamicParameters.MaxFragmentImageUniforms = + m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0; + m_dynamicParameters.MaxComputeImageUniforms = + std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms); const Int maxSupportedDrawBuffers = static_cast(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS); m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index cef9b576..3786b61f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -1197,6 +1197,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { return TextureTarget::Unknown; } } + + Bool IsFloatStorageImageUniformType(GLenum uniformType) { + switch (uniformType) { + case GL_IMAGE_1D: + case GL_IMAGE_2D: + case GL_IMAGE_3D: + case GL_IMAGE_2D_RECT: + case GL_IMAGE_CUBE: + case GL_IMAGE_BUFFER: + case GL_IMAGE_1D_ARRAY: + case GL_IMAGE_2D_ARRAY: + case GL_IMAGE_CUBE_MAP_ARRAY: + case GL_IMAGE_2D_MULTISAMPLE: + case GL_IMAGE_2D_MULTISAMPLE_ARRAY: + return true; + default: + return false; + } + } } // namespace VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) { @@ -1218,6 +1237,105 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + VkFormat ProgramFactory::ConvertSpirvImageFormatToVkFormat(SpvImageFormat format) { + switch (format) { + case SpvImageFormatUnknown: return VK_FORMAT_UNDEFINED; + case SpvImageFormatRgba32f: return VK_FORMAT_R32G32B32A32_SFLOAT; + case SpvImageFormatRgba16f: return VK_FORMAT_R16G16B16A16_SFLOAT; + case SpvImageFormatR32f: return VK_FORMAT_R32_SFLOAT; + case SpvImageFormatRgba8: return VK_FORMAT_R8G8B8A8_UNORM; + case SpvImageFormatRgba8Snorm: return VK_FORMAT_R8G8B8A8_SNORM; + case SpvImageFormatRg32f: return VK_FORMAT_R32G32_SFLOAT; + case SpvImageFormatRg16f: return VK_FORMAT_R16G16_SFLOAT; + case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32; + case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT; + case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM; + case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; + case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM; + case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM; + case SpvImageFormatR16: return VK_FORMAT_R16_UNORM; + case SpvImageFormatR8: return VK_FORMAT_R8_UNORM; + case SpvImageFormatRgba16Snorm: return VK_FORMAT_R16G16B16A16_SNORM; + case SpvImageFormatRg16Snorm: return VK_FORMAT_R16G16_SNORM; + case SpvImageFormatRg8Snorm: return VK_FORMAT_R8G8_SNORM; + case SpvImageFormatR16Snorm: return VK_FORMAT_R16_SNORM; + case SpvImageFormatR8Snorm: return VK_FORMAT_R8_SNORM; + case SpvImageFormatRgba32i: return VK_FORMAT_R32G32B32A32_SINT; + case SpvImageFormatRgba16i: return VK_FORMAT_R16G16B16A16_SINT; + case SpvImageFormatRgba8i: return VK_FORMAT_R8G8B8A8_SINT; + case SpvImageFormatR32i: return VK_FORMAT_R32_SINT; + case SpvImageFormatRg32i: return VK_FORMAT_R32G32_SINT; + case SpvImageFormatRg16i: return VK_FORMAT_R16G16_SINT; + case SpvImageFormatRg8i: return VK_FORMAT_R8G8_SINT; + case SpvImageFormatR16i: return VK_FORMAT_R16_SINT; + case SpvImageFormatR8i: return VK_FORMAT_R8_SINT; + case SpvImageFormatRgba32ui: return VK_FORMAT_R32G32B32A32_UINT; + case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT; + case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT; + case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT; + case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32; + case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT; + case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT; + case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT; + case SpvImageFormatR16ui: return VK_FORMAT_R16_UINT; + case SpvImageFormatR8ui: return VK_FORMAT_R8_UINT; + case SpvImageFormatR64ui: return VK_FORMAT_R64_UINT; + case SpvImageFormatR64i: return VK_FORMAT_R64_SINT; + case SpvImageFormatMax: return VK_FORMAT_UNDEFINED; + } + return VK_FORMAT_UNDEFINED; + } + + SamplerNumericDomain ProgramFactory::UniformTypeToSamplerNumericDomain(GLenum glType) { + switch (glType) { + case GL_INT_SAMPLER_1D: + case GL_INT_SAMPLER_2D: + case GL_INT_SAMPLER_3D: + case GL_INT_SAMPLER_CUBE: + case GL_INT_SAMPLER_2D_RECT: + case GL_INT_SAMPLER_1D_ARRAY: + case GL_INT_SAMPLER_2D_ARRAY: + case GL_INT_SAMPLER_BUFFER: + case GL_INT_SAMPLER_2D_MULTISAMPLE: + case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: + case GL_INT_SAMPLER_CUBE_MAP_ARRAY: + return SamplerNumericDomain::SignedInteger; + case GL_UNSIGNED_INT_SAMPLER_1D: + case GL_UNSIGNED_INT_SAMPLER_2D: + case GL_UNSIGNED_INT_SAMPLER_3D: + case GL_UNSIGNED_INT_SAMPLER_CUBE: + case GL_UNSIGNED_INT_SAMPLER_2D_RECT: + case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: + case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: + case GL_UNSIGNED_INT_SAMPLER_BUFFER: + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: + case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY: + return SamplerNumericDomain::UnsignedInteger; + case GL_SAMPLER_1D: + case GL_SAMPLER_2D: + case GL_SAMPLER_3D: + case GL_SAMPLER_CUBE: + case GL_SAMPLER_2D_RECT: + case GL_SAMPLER_1D_ARRAY: + case GL_SAMPLER_2D_ARRAY: + case GL_SAMPLER_BUFFER: + case GL_SAMPLER_2D_MULTISAMPLE: + case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: + case GL_SAMPLER_CUBE_MAP_ARRAY: + case GL_SAMPLER_1D_SHADOW: + case GL_SAMPLER_2D_SHADOW: + case GL_SAMPLER_CUBE_SHADOW: + case GL_SAMPLER_2D_RECT_SHADOW: + case GL_SAMPLER_1D_ARRAY_SHADOW: + case GL_SAMPLER_2D_ARRAY_SHADOW: + case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW: + return SamplerNumericDomain::Float; + default: + return SamplerNumericDomain::Unknown; + } + } + ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const { XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); @@ -1465,6 +1583,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.samplerNameByBinding.assign(m_maxBindings, String()); entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1); entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D); + entry.samplerNumericDomainByBinding.assign(m_maxBindings, SamplerNumericDomain::Unknown); + entry.storageImageFormatByBinding.assign(m_maxBindings, VK_FORMAT_UNDEFINED); + entry.storageImageUsesBindingFormatByBinding.assign(m_maxBindings, false); entry.storageBlockNameByBinding.assign(m_maxBindings, String()); entry.storageBlockIndexByBinding.assign(m_maxBindings, -1); entry.globalUboBinding = -1; @@ -1592,10 +1713,54 @@ namespace MobileGL::MG_Backend::DirectVulkan { continue; } - const TextureTarget target = UniformTypeToTextureTarget(program.GetUniformType(static_cast(location))); + const GLenum uniformType = program.GetUniformType(static_cast(location)); + + if (descriptorKind == DescriptorBindingKind::StorageImage) { + const VkFormat reflectedFormat = + ConvertSpirvImageFormatToVkFormat(sampler->image.image_format); + VkFormat& existingFormat = entry.storageImageFormatByBinding[binding]; + MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED || + reflectedFormat == VK_FORMAT_UNDEFINED || + existingFormat == reflectedFormat, + "ProgramFactory::ReflectLayout: storage image binding %u ('%s') has " + "conflicting reflected formats (%d vs %d)", + binding, uniformName.c_str(), static_cast(existingFormat), + static_cast(reflectedFormat)); + if (existingFormat == VK_FORMAT_UNDEFINED) { + existingFormat = reflectedFormat; + } + if (m_unformattedFloatStorageImagesEnabled && + existingFormat == VK_FORMAT_UNDEFINED && + IsFloatStorageImageUniformType(uniformType)) { + entry.storageImageUsesBindingFormatByBinding[binding] = true; + } else if (reflectedFormat != VK_FORMAT_UNDEFINED) { + // A typed declaration in any stage wins for the entire binding. This is + // required when another stage reaches the same image through an atomic + // path and therefore could not be made formatless. + entry.storageImageUsesBindingFormatByBinding[binding] = false; + } + } + + const TextureTarget target = UniformTypeToTextureTarget(uniformType); MOBILEGL_ASSERT(target != TextureTarget::Unknown, "ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'", uniformName.c_str()); + if (descriptorKind == DescriptorBindingKind::CombinedImageSampler) { + const SamplerNumericDomain numericDomain = UniformTypeToSamplerNumericDomain(uniformType); + MOBILEGL_ASSERT(numericDomain != SamplerNumericDomain::Unknown, + "ProgramFactory::ReflectLayout: failed to resolve sampler numeric domain " + "for '%s' (uniformType=0x%x)", + uniformName.c_str(), uniformType); + MOBILEGL_ASSERT(entry.samplerNumericDomainByBinding[binding] == + SamplerNumericDomain::Unknown || + entry.samplerNumericDomainByBinding[binding] == numericDomain, + "ProgramFactory::ReflectLayout: sampler binding %u ('%s') has conflicting " + "numeric domains (%d vs %d)", + binding, uniformName.c_str(), + static_cast(entry.samplerNumericDomainByBinding[binding]), + static_cast(numericDomain)); + entry.samplerNumericDomainByBinding[binding] = numericDomain; + } MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 || entry.samplerUniformLocationByBinding[binding] == location, "ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)", @@ -1737,6 +1902,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } } + + // When Vulkan can legally access storage images without a statically declared + // format, let GL's glBindImageTexture format select the runtime image view. This + // provides desktop-driver-compatible behavior for packs such as iterationRP, whose + // float image qualifier can disagree with the bound render-target format. Integer + // storage images remain formatted so r32ui/r32i bit-reinterpretation paths keep the + // exact descriptor format required by their shader operations. + if (m_unformattedFloatStorageImagesEnabled) { + Vector unformattedSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan( + moduleSpirvs[i], unformattedSpirv)) { + moduleSpirvs[i] = std::move(unformattedSpirv); + } else { + MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u", + program.GetExternalIndex()); + } + } } const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 12e53f92..6dba8852 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -14,8 +14,16 @@ #include "MG_State/GLState/TextureState/TextureEnum.h" #include +#include namespace MobileGL::MG_Backend::DirectVulkan { + enum class SamplerNumericDomain : Uint8 { + Unknown = 0, + Float, + SignedInteger, + UnsignedInteger, + }; + class ProgramFactory { public: enum class DescriptorBindingKind : Uint8 { @@ -54,6 +62,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector samplerNameByBinding; Vector samplerUniformLocationByBinding; Vector samplerTextureTargetByBinding; + Vector samplerNumericDomainByBinding; + Vector storageImageFormatByBinding; + Vector storageImageUsesBindingFormatByBinding; Vector storageBlockNameByBinding; Vector storageBlockIndexByBinding; Int globalUboBinding = -1; @@ -82,6 +93,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { samplerNameByBinding = std::move(other.samplerNameByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); + samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding); + storageImageFormatByBinding = std::move(other.storageImageFormatByBinding); + storageImageUsesBindingFormatByBinding = + std::move(other.storageImageUsesBindingFormatByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); globalUboBinding = other.globalUboBinding; @@ -118,6 +133,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { samplerNameByBinding = std::move(other.samplerNameByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); + samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding); + storageImageFormatByBinding = std::move(other.storageImageFormatByBinding); + storageImageUsesBindingFormatByBinding = + std::move(other.storageImageUsesBindingFormatByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); globalUboBinding = other.globalUboBinding; @@ -167,9 +186,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16, - Bool shaderDrawParametersEnabled = false) + Bool shaderDrawParametersEnabled = false, + Bool unformattedFloatStorageImagesEnabled = false) : m_device(device), m_maxBindings(maxBindings), m_config(config), - m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) { + m_shaderDrawParametersEnabled(shaderDrawParametersEnabled), + m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) { VkProgramObject::s_device = device; } ~ProgramFactory() = default; @@ -180,6 +201,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags); static VkShaderStageFlagBits ToVkStage(ShaderStage stage); + static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format); + static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType); private: struct ProgramLookupCache { @@ -206,6 +229,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass // (which needs the DrawParameters capability / gl_BaseInstance builtin). Bool m_shaderDrawParametersEnabled = false; + // True only when the logical device enabled both + // shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat. + Bool m_unformattedFloatStorageImagesEnabled = false; mutable ProgramLookupCache m_lastLookup; static inline XXH64_state_t* m_hashState = XXH64_createState(); }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index d3af72bb..fa356319 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -13,6 +13,7 @@ #include "MG_State/GLState/ProgramState/ProgramObject.h" #include "MG_State/GLState/TextureState/TextureObject2D.h" #include "MG_State/GLState/TextureState/TextureObjectBuffer.h" +#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Metrics/TextureMetrics.h" @@ -78,6 +79,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { return uniformUnit >= 0 ? uniformUnit : 0; } + VkFormat UniformManager::ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat, + VkFormat resourceFormat, Bool useBindingFormat) { + if (useBindingFormat) { + const TextureInternalFormat bindingInternalFormat = + MG_Util::ConvertGLEnumToTextureInternalFormat(bindingFormat); + return MG_Util::ConvertTextureInternalFormatToVkEnum(bindingInternalFormat); + } + return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat; + } + Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager, ProgramFactory* programFactory, VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount, @@ -276,6 +287,36 @@ namespace MobileGL::MG_Backend::DirectVulkan { "ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u", static_cast(resource->layout), texture->GetExternalIndex(), binding); } + + MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(), + "ResolveSamplerDescriptor: sampler numeric-domain binding %u out of range", binding); + const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding]; + // Vulkan forbids linear filtering and anisotropy for integer sampled-image formats. + // Some desktop GL shader packs deliberately bit-read a mutable float texture through a + // usampler and still leave the texture's ordinary linear parameters in place; texelFetch + // ignores filtering, so a nearest VkSampler preserves the operation while keeping the + // descriptor valid. + const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger || + numericDomain == SamplerNumericDomain::UnsignedInteger; + const VkFormat sampledViewFormat = + VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain); + if (sampledViewFormat == VK_FORMAT_UNDEFINED) { + MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') " + "textureId=%d imageFormat=%d numericDomain=%d", + binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), + static_cast(resource->format), static_cast(numericDomain)); + return false; + } + const VkImageView sampledImageView = + m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat); + if (sampledImageView == VK_NULL_HANDLE) { + MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') " + "textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d", + binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), + static_cast(resource->format), static_cast(sampledViewFormat), + static_cast(numericDomain)); + return false; + } // Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's // sampler object and texture (both by lifetime id + version) are unchanged from the // last draw that resolved it: the resulting sampler key, and therefore the VkSampler @@ -291,23 +332,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Uint64 textureLifetimeId = texture->GetLifetimeId(); const Uint16 textureParamsVersion = texture->GetTextureParamsVersion(); if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion && - memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion) { + memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion && + memo.forceNearestFiltering == forceNearestFiltering) { resolvedSampler = memo.sampler; } else { - resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture); + resolvedSampler = + m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering); memo.samplerLifetimeId = samplerLifetimeId; memo.samplerVersion = samplerVersion; memo.textureLifetimeId = textureLifetimeId; memo.textureParamsVersion = textureParamsVersion; + memo.forceNearestFiltering = forceNearestFiltering; memo.sampler = resolvedSampler; memo.valid = true; } } else { - resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture); + resolvedSampler = + m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering); } outImageInfo = { .sampler = resolvedSampler, - .imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView, + .imageView = sampledImageView, .imageLayout = resource->layout, }; return outImageInfo.sampler != VK_NULL_HANDLE; @@ -572,9 +617,32 @@ namespace MobileGL::MG_Backend::DirectVulkan { } const Uint32 mipLevel = static_cast(std::max(0, imageBinding.Level)); - VkImageView view = m_textureManager->GetOrCreateViewAtMipLevel(*imageBinding.Texture, mipLevel); + MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(), + "ResolveStorageImageDescriptor: storage image format binding %u out of range", binding); + MOBILEGL_ASSERT(binding < programObj.storageImageUsesBindingFormatByBinding.size(), + "ResolveStorageImageDescriptor: storage image format policy binding %u out of range", + binding); + const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding]; + const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding]; + const VkFormat viewFormat = ResolveStorageImageViewFormat( + reflectedFormat, imageBinding.Format, resource->format, useBindingFormat); + if (viewFormat == VK_FORMAT_UNDEFINED) { + MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x " + "for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s", + imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(), + useBindingFormat ? "true" : "false"); + return false; + } + const VkImageView view = m_textureManager->GetOrCreateStorageImageView( + *imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer); if (view == VK_NULL_HANDLE) { - view = resource->fullView; + MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u " + "bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s", + imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format, + static_cast(resource->format), static_cast(reflectedFormat), + static_cast(viewFormat), + useBindingFormat ? "true" : "false"); + return false; } outImageInfo.sampler = VK_NULL_HANDLE; outImageInfo.imageView = view; @@ -632,6 +700,50 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + Bool UniformManager::CollectStorageImageTextures( + const MG_State::GLState::ProgramObject& program, + const ProgramFactory::VkProgramObject& programObj, + Vector& outTextures) const { + outTextures.clear(); + MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, + "CollectStorageImageTextures: GL context is null"); + + const Uint32 bindingCount = + std::min(m_maxBindings, static_cast(programObj.bindingKinds.size())); + for (Uint32 binding = 0; binding < bindingCount; ++binding) { + if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::StorageImage) { + continue; + } + if (binding >= programObj.samplerUniformLocationByBinding.size()) { + MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding); + return false; + } + + const Int location = programObj.samplerUniformLocationByBinding[binding]; + if (location < 0) { + MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding); + return false; + } + const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); + if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) { + MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u", + imageUnit, binding); + return false; + } + + auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get(); + if (texture == nullptr) { + MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u", + imageUnit, binding); + return false; + } + if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) { + outTextures.push_back(texture); + } + } + return true; + } + Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, UboBindResult& out) const { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 0879e7e8..b3d5c503 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -42,6 +42,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Vector& outTextures); + Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program, + const ProgramFactory::VkProgramObject& programObj, + Vector& outTextures) const; Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, @@ -49,6 +52,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, const SamplerBindingOverride* samplerBindingOverride = nullptr); + // Pure format-policy helper kept public for host regression tests. Formatted storage + // images use their shader qualifier; transformed float images use glBindImageTexture's + // format and never silently fall back to the backing image format. + static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat, + VkFormat resourceFormat, Bool useBindingFormat); + private: struct DescriptorPoolBucket { VkDescriptorPool handle = VK_NULL_HANDLE; @@ -165,9 +174,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkSampler sampler = VK_NULL_HANDLE; Uint16 samplerVersion = 0; Uint16 textureParamsVersion = 0; + Bool forceNearestFiltering = false; Bool valid = false; }; mutable Vector m_samplerResolveMemo; }; } // namespace MobileGL::MG_Backend::DirectVulkan - diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index 1e8802a2..a7ec84cb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -66,6 +66,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector bindingBaseOffsets; Vector bindingAttributeLocations; Vector bindingUsesClientMemory; + Vector bindingConversions; Uint32 unsupportedAttribMask = 0; for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) { @@ -74,8 +75,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { continue; } - const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra); - if (vkFormat == VK_FORMAT_UNDEFINED) { + const VkFormat sourceVkFormat = + ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra); + if (sourceVkFormat == VK_FORMAT_UNDEFINED) { MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is " "enabled but cannot be mapped to a VkFormat", location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); @@ -83,6 +85,33 @@ namespace MobileGL::MG_Backend::DirectVulkan { continue; } + VkFormat vkFormat = sourceVkFormat; + VertexStreamConversion conversion = VertexStreamConversion::None; + if (!SupportsVertexBufferFormat(vkFormat)) { + if (IsScaledIntegerVertexFormat(vkFormat)) { + const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size); + if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) { + vkFormat = fallbackFormat; + conversion = VertexStreamConversion::ScaledIntegerToFloat32; + MGLOG_W("Vertex attribute location=%u format=%d lacks " + "VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d " + "(type=%s size=%d normalized=%s integer=%s)", + location, static_cast(sourceVkFormat), static_cast(vkFormat), + MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size, + attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false"); + } + } + + if (conversion == VertexStreamConversion::None) { + MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): " + "VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists", + location, static_cast(sourceVkFormat), + MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); + unsupportedAttribMask |= (1u << location); + continue; + } + } + const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); if (attribByteSize == 0) { MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is " @@ -92,8 +121,28 @@ namespace MobileGL::MG_Backend::DirectVulkan { continue; } - const Uint32 stride = + const Uint32 sourceStride = attr.Stride > 0 ? static_cast(attr.Stride) : static_cast(attribByteSize); + const Bool packedAttribute = attr.Type == DataType::Int2101010Rev || + attr.Type == DataType::Uint2101010Rev; + const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type); + if (conversion == VertexStreamConversion::None && requiredAlignment > 1 && + ((sourceStride % requiredAlignment) != 0 || (attr.Offset % requiredAlignment) != 0)) { + // GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not + // unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one + // attribute into a tightly packed transient stream without changing its format. + conversion = VertexStreamConversion::Repack; + MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment " + "(offset=%zu stride=%u required=%zu); using a tightly packed stream", + location, attr.Offset, sourceStride, requiredAlignment); + } + + Uint32 stride = sourceStride; + if (conversion == VertexStreamConversion::Repack) { + stride = static_cast(attribByteSize); + } else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) { + stride = static_cast(attr.Size * static_cast(sizeof(Float))); + } const VkVertexInputRate inputRate = (attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; @@ -103,6 +152,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0); bindingAttributeLocations.push_back(location); bindingUsesClientMemory.push_back(attr.Buffer == nullptr); + bindingConversions.push_back(conversion); builder.AddBinding(binding, stride, inputRate); builder.AddAttribute(location, binding, vkFormat, 0); } @@ -117,6 +167,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.bindingBaseOffsets = std::move(bindingBaseOffsets); entry.bindingAttributeLocations = std::move(bindingAttributeLocations); entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory); + entry.bindingConversions = std::move(bindingConversions); entry.unsupportedAttribMask = unsupportedAttribMask; entry.state = state; entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data(); @@ -282,4 +333,47 @@ namespace MobileGL::MG_Backend::DirectVulkan { const SizeT componentSize = GetComponentSize(type); return componentSize == 0 ? 0 : componentSize * static_cast(size); } + + Bool VertexInputStateFactory::IsScaledIntegerVertexFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_USCALED: + case VK_FORMAT_R8_SSCALED: + case VK_FORMAT_R8G8_USCALED: + case VK_FORMAT_R8G8_SSCALED: + case VK_FORMAT_R8G8B8_USCALED: + case VK_FORMAT_R8G8B8_SSCALED: + case VK_FORMAT_R8G8B8A8_USCALED: + case VK_FORMAT_R8G8B8A8_SSCALED: + case VK_FORMAT_R16_USCALED: + case VK_FORMAT_R16_SSCALED: + case VK_FORMAT_R16G16_USCALED: + case VK_FORMAT_R16G16_SSCALED: + case VK_FORMAT_R16G16B16_USCALED: + case VK_FORMAT_R16G16B16_SSCALED: + case VK_FORMAT_R16G16B16A16_USCALED: + case VK_FORMAT_R16G16B16A16_SSCALED: + return true; + default: + return false; + } + } + + VkFormat VertexInputStateFactory::ToFloat32VertexFormat(Int componentCount) { + switch (componentCount) { + case 1: return VK_FORMAT_R32_SFLOAT; + case 2: return VK_FORMAT_R32G32_SFLOAT; + case 3: return VK_FORMAT_R32G32B32_SFLOAT; + case 4: return VK_FORMAT_R32G32B32A32_SFLOAT; + default: return VK_FORMAT_UNDEFINED; + } + } + + Bool VertexInputStateFactory::SupportsVertexBufferFormat(VkFormat format) const { + if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) { + return false; + } + VkFormatProperties properties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties); + return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0; + } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h index afd65b37..3f82180e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h @@ -19,6 +19,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { public: using HashType = Uint64; + enum class VertexStreamConversion : Uint8 { + None = 0, + Repack, + ScaledIntegerToFloat32, + }; + struct BackendVertexInputState { HashType hash = 0; Vector bindings; @@ -27,6 +33,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector bindingBaseOffsets; Vector bindingAttributeLocations; Vector bindingUsesClientMemory; + Vector bindingConversions; // Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are // absent from `attributes`, so without this mask the draw path cannot tell them apart from // a genuinely disabled array and would silently feed the shader the current attribute value. @@ -36,8 +43,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; }; - explicit VertexInputStateFactory(const VulkanRendererConfig& config): - m_config(config) {} + VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice): + m_config(config), m_physicalDevice(physicalDevice) {} ~VertexInputStateFactory() = default; VertexInputStateFactory(const VertexInputStateFactory&) = delete; @@ -56,8 +63,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { private: static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false); + static Bool IsScaledIntegerVertexFormat(VkFormat format); + static VkFormat ToFloat32VertexFormat(Int componentCount); + Bool SupportsVertexBufferFormat(VkFormat format) const; const VulkanRendererConfig& m_config; + VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; UnorderedMap m_cache; static inline XXH64_state_t* m_hashState = XXH64_createState(); }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index 9b80a961..241267fd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -65,8 +65,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const { + Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler, + Bool forceNearestFiltering) const { if (!m_samplerAnisotropySupported) return 1.0f; + if (forceNearestFiltering) 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 || @@ -90,10 +92,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, - const MG_State::GLState::ITextureObject& texture) const { + const MG_State::GLState::ITextureObject& texture, + Bool forceNearestFiltering) const { MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); + XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering))); + const auto minFilter = sampler.GetMinFilter(); XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter))); const auto magFilter = sampler.GetMagFilter(); @@ -115,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // 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); + const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering); XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy))); const auto compareMode = sampler.GetCompareMode(); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); @@ -127,8 +132,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { } VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, - const MG_State::GLState::ITextureObject& texture) { - const Uint64 key = BuildSamplerKey(sampler, texture); + const MG_State::GLState::ITextureObject& texture, + Bool forceNearestFiltering) { + const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering); auto it = m_samplers.find(key); if (it != m_samplers.end()) { return it->second.handle; @@ -136,16 +142,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkSamplerCreateInfo samplerInfo{}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; - samplerInfo.magFilter = ToVkFilter(sampler.GetMagFilter()); - samplerInfo.minFilter = ToVkFilter(sampler.GetMinFilter()); - samplerInfo.mipmapMode = ToVkMipmapMode(sampler.GetMipmapMode()); + samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter()); + samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter()); + samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST + : ToVkMipmapMode(sampler.GetMipmapMode()); samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS()); samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT()); samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR()); samplerInfo.mipLodBias = sampler.GetLodBias(); // Must use the same resolver as BuildSamplerKey - a divergence would either collide two // different samplers or silently create duplicates. - const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler); + const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering); samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE; samplerInfo.maxAnisotropy = maxAnisotropy; samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h index 001b11a5..0e72f60a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h @@ -34,7 +34,8 @@ public: void Shutdown(); VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, - const MG_State::GLState::ITextureObject& texture); + const MG_State::GLState::ITextureObject& texture, + Bool forceNearestFiltering = false); private: struct SamplerCacheEntry { @@ -44,7 +45,8 @@ private: }; Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, - const MG_State::GLState::ITextureObject& texture) const; + const MG_State::GLState::ITextureObject& texture, + Bool forceNearestFiltering) const; static VkFilter ToVkFilter(SamplerFilterMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); @@ -57,7 +59,8 @@ private: // 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; + Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler, + Bool forceNearestFiltering) const; VkDevice m_device = VK_NULL_HANDLE; const VulkanRendererConfig* m_config = nullptr; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index da38a978..d737772c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -8,6 +8,8 @@ #include "VkTextureManager.h" +#include "ProgramFactory.h" + #include "MG_State/GLState/Core.h" #include "MG_Util/Converters/MGToStr/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" @@ -17,6 +19,7 @@ #include #include #include +#include namespace MobileGL::MG_Backend::DirectVulkan { // Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule @@ -63,6 +66,59 @@ namespace MobileGL::MG_Backend::DirectVulkan { target == TextureUploadTarget::ProxyTexture2DMultisampleArray; } + static Bool IsMutableStorageImageFormat(VkFormat format) { + if (!vkuFormatIsColor(format) || vkuFormatIsCompressed(format)) { + return false; + } + + // These are the uncompressed color compatibility classes covered by the core GLSL/SPIR-V + // storage-image formats. OpenGL mutable texture storage uses image-format compatibility by + // size, so a shader may legally reinterpret (for example) RGBA16_UNORM storage as rgba16f. Vulkan + // requires the image to be mutable and the view formats to share this exact compatibility + // class for the equivalent operation. + switch (vkuFormatCompatibilityClass(format)) { + case VKU_FORMAT_COMPATIBILITY_CLASS_8BIT: + case VKU_FORMAT_COMPATIBILITY_CLASS_16BIT: + case VKU_FORMAT_COMPATIBILITY_CLASS_32BIT: + case VKU_FORMAT_COMPATIBILITY_CLASS_64BIT: + case VKU_FORMAT_COMPATIBILITY_CLASS_128BIT: + return true; + default: + return false; + } + } + + static Bool HasMatchingColorComponentLayout(VkFormat lhs, VkFormat rhs) { + const VKU_FORMAT_INFO lhsInfo = vkuGetFormatInfo(lhs); + const VKU_FORMAT_INFO rhsInfo = vkuGetFormatInfo(rhs); + if (lhsInfo.component_count == 0 || lhsInfo.component_count != rhsInfo.component_count || + lhsInfo.texel_block_size != rhsInfo.texel_block_size || + lhsInfo.texels_per_block != 1 || rhsInfo.texels_per_block != 1) { + return false; + } + for (Uint32 component = 0; component < lhsInfo.component_count; ++component) { + if (lhsInfo.components[component].type != rhsInfo.components[component].type || + lhsInfo.components[component].size != rhsInfo.components[component].size) { + return false; + } + } + return true; + } + + static Bool FormatMatchesSamplerNumericDomain(VkFormat format, SamplerNumericDomain numericDomain) { + switch (numericDomain) { + case SamplerNumericDomain::Float: + return vkuFormatIsSampledFloat(format); + case SamplerNumericDomain::SignedInteger: + return vkuFormatIsSINT(format); + case SamplerNumericDomain::UnsignedInteger: + return vkuFormatIsUINT(format); + case SamplerNumericDomain::Unknown: + return true; + } + return false; + } + static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) { switch (requestedSamples) { case 1: @@ -767,6 +823,180 @@ namespace MobileGL::MG_Backend::DirectVulkan { return perMipSampledView; } + VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, + VkFormat format) { + TextureResource* resource = SyncTextureAndGetDescriptor(texture); + if (resource == nullptr || resource->image == VK_NULL_HANDLE || + resource->sampledView == VK_NULL_HANDLE) { + return VK_NULL_HANDLE; + } + + if (format == VK_FORMAT_UNDEFINED || format == resource->format) { + return resource->sampledView; + } + if (!AreSampledImageViewFormatsCompatible(resource->format, format)) { + MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d", + __func__, static_cast(format), texture.GetExternalIndex(), + static_cast(resource->format)); + return VK_NULL_HANDLE; + } + if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) { + MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d", + __func__, texture.GetExternalIndex(), static_cast(resource->format), + static_cast(format)); + return VK_NULL_HANDLE; + } + + const TextureResource::SampledImageViewKey key{ + .baseMipLevel = resource->sampledBaseMipLevel, + .levelCount = resource->sampledLevelCount, + .viewType = resource->viewType, + .format = format, + }; + const auto existing = resource->alternateSampledViews.find(key); + if (existing != resource->alternateSampledViews.end()) { + return existing->second; + } + + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); + if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) { + MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT " + "for textureId=%d (available=0x%x)", + __func__, static_cast(format), texture.GetExternalIndex(), + static_cast(formatProperties.optimalTilingFeatures)); + return VK_NULL_HANDLE; + } + + const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); + const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); + const VkImageView view = CreateImageView( + resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, resource->viewType, + resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers, + &sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT); + if (view == VK_NULL_HANDLE) { + MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d", + __func__, texture.GetExternalIndex(), static_cast(resource->format), + static_cast(format)); + return VK_NULL_HANDLE; + } + + resource->alternateSampledViews.emplace(key, view); + MGLOG_D("%s: created sampled image view textureId=%d imageFormat=%d viewFormat=%d mip=[%u,%u)", + __func__, texture.GetExternalIndex(), static_cast(resource->format), + static_cast(format), resource->sampledBaseMipLevel, + resource->sampledBaseMipLevel + resource->sampledLevelCount); + return view; + } + + VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, + Uint32 mipLevel, VkFormat format, + Bool layered, Int32 layer) { + TextureResource* resource = SyncTextureAndGetDescriptor(texture); + if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels || + resource->sampleCount != VK_SAMPLE_COUNT_1_BIT || + (resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { + return VK_NULL_HANDLE; + } + + if (format == VK_FORMAT_UNDEFINED) { + format = resource->format; + } + if (!AreStorageImageViewFormatsCompatible(resource->format, format)) { + MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d", + __func__, static_cast(format), texture.GetExternalIndex(), + static_cast(resource->format)); + return VK_NULL_HANDLE; + } + if (format != resource->format && + (resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) { + MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d", + __func__, texture.GetExternalIndex(), static_cast(resource->format), + static_cast(format)); + return VK_NULL_HANDLE; + } + + Uint32 baseArrayLayer = 0; + Uint32 layerCount = resource->arrayLayers; + VkImageViewType viewType = resource->viewType; + if (!layered) { + switch (resource->viewType) { + case VK_IMAGE_VIEW_TYPE_1D_ARRAY: + viewType = VK_IMAGE_VIEW_TYPE_1D; + break; + case VK_IMAGE_VIEW_TYPE_2D_ARRAY: + case VK_IMAGE_VIEW_TYPE_CUBE: + case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY: + viewType = VK_IMAGE_VIEW_TYPE_2D; + break; + case VK_IMAGE_VIEW_TYPE_3D: + MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d", + __func__, texture.GetExternalIndex()); + return VK_NULL_HANDLE; + default: + break; + } + + if (viewType != resource->viewType) { + if (layer < 0 || static_cast(layer) >= resource->arrayLayers) { + MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u", + __func__, layer, texture.GetExternalIndex(), resource->arrayLayers); + return VK_NULL_HANDLE; + } + baseArrayLayer = static_cast(layer); + layerCount = 1; + } + } + + const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers && + viewType == resource->viewType; + if (format == resource->format && isFullResourceView) { + return GetOrCreateViewAtMipLevel(texture, mipLevel); + } + + const TextureResource::StorageImageViewKey key{ + .mipLevel = mipLevel, + .baseArrayLayer = baseArrayLayer, + .layerCount = layerCount, + .viewType = viewType, + .format = format, + }; + auto it = resource->storageImageViews.find(key); + if (it != resource->storageImageViews.end()) { + return it->second; + } + + VkFormatFeatureFlags requiredFormatFeatures = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; + if (format != resource->format && + (format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT)) { + requiredFormatFeatures |= VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT; + } + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); + if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) { + MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d " + "(available=0x%x)", + __func__, static_cast(format), static_cast(requiredFormatFeatures), + texture.GetExternalIndex(), static_cast(formatProperties.optimalTilingFeatures)); + return VK_NULL_HANDLE; + } + + const VkImageView view = CreateImageView(resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, viewType, + mipLevel, 1, baseArrayLayer, layerCount, nullptr, + VK_IMAGE_USAGE_STORAGE_BIT); + if (view == VK_NULL_HANDLE) { + MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d", + __func__, texture.GetExternalIndex(), mipLevel, static_cast(resource->format), + static_cast(format)); + return VK_NULL_HANDLE; + } + resource->storageImageViews.emplace(key, view); + MGLOG_D("%s: created storage image view textureId=%d mip=%u imageFormat=%d viewFormat=%d", + __func__, texture.GetExternalIndex(), mipLevel, static_cast(resource->format), + static_cast(format)); + return view; + } + void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) { MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null"); auto it = m_textureResources.find(MakeTextureIdentity(texture)); @@ -1085,6 +1315,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } + const VkImageAspectFlags aspect = GetAspectMaskForFormat(format); + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); + const Bool supportsStorageImage = + !isMultisampleTexture && + (aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 && + (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; + VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags; + if (supportsStorageImage && IsMutableStorageImageFormat(format)) { + imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } + const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format && resource.extent.width == static_cast(texelSize.x()) && resource.extent.height == static_cast(texelSize.y()) && @@ -1092,6 +1334,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.arrayLayers == shapeInfo.arrayLayers && resource.viewType == shapeInfo.viewType && resource.sampleCount == resolvedSampleCount && + resource.imageCreateFlags == imageCreateFlags && resource.mipLevels == backingMipLevels; if (compatible) { if (resource.perMipViews.size() != backingMipLevels) { @@ -1112,6 +1355,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.arrayLayers == shapeInfo.arrayLayers && resource.viewType == shapeInfo.viewType && resource.sampleCount == resolvedSampleCount && + resource.imageCreateFlags == imageCreateFlags && resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT && resource.mipLevels < backingMipLevels && resource.layout != VK_IMAGE_LAYOUT_UNDEFINED; @@ -1123,26 +1367,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { DeferResourceRelease(Move(resource)); } - auto aspect = GetAspectMaskForFormat(format); - VkImageCreateInfo imageInfo{}; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - imageInfo.flags = shapeInfo.imageFlags; - imageInfo.imageType = shapeInfo.imageType; + imageInfo.flags = imageCreateFlags; + imageInfo.imageType = shapeInfo.imageType; imageInfo.extent.width = static_cast(texelSize.x()); imageInfo.extent.height = static_cast(texelSize.y()); - imageInfo.extent.depth = shapeInfo.depth; + imageInfo.extent.depth = shapeInfo.depth; imageInfo.mipLevels = backingMipLevels; - imageInfo.arrayLayers = shapeInfo.arrayLayers; + imageInfo.arrayLayers = shapeInfo.arrayLayers; imageInfo.format = format; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - VkFormatProperties formatProperties{}; - vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); - const Bool supportsStorageImage = - !isMultisampleTexture && - (aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 && - (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | @@ -1153,15 +1389,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; } imageInfo.samples = resolvedSampleCount; - if (isMultisampleTexture) { + if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) { VkImageFormatProperties imageFormatProperties{}; const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags, &imageFormatProperties); if (imageFormatResult != VK_SUCCESS || - (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0) { - MGLOG_D("%s: sampleCount=%d is unsupported for textureId=%d target=%s format=%d usage=0x%x", - __func__, texture.GetSamples(), texture.GetExternalIndex(), + (isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) { + MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s " + "format=%d usage=0x%x", + __func__, static_cast(imageInfo.flags), texture.GetSamples(), + texture.GetExternalIndex(), MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(), static_cast(format), static_cast(imageInfo.usage)); return false; @@ -1188,6 +1426,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.aspect = aspect; resource.viewType = shapeInfo.viewType; resource.sampleCount = resolvedSampleCount; + resource.imageCreateFlags = imageCreateFlags; resource.syncedTextureParamsVersion = 0; if (preservedResource) { @@ -1203,7 +1442,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { void VkTextureManager::DeferResourceRelease(TextureResource&& resource) { if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE && resource.sampledView == VK_NULL_HANDLE && - resource.perMipViews.empty() && resource.perMipSampledViews.empty()) { + resource.perMipViews.empty() && resource.perMipSampledViews.empty() && + resource.attachmentViews.empty() && resource.alternateSampledViews.empty() && + resource.storageImageViews.empty()) { return; } @@ -1299,6 +1540,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { sampledView = VK_NULL_HANDLE; } } + for (const auto& [_, sampledView] : resource.alternateSampledViews) { + DeferViewRelease(sampledView); + } + resource.alternateSampledViews.clear(); const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); @@ -1324,7 +1569,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount, Uint32 baseArrayLayer, Uint32 layerCount, - const VkComponentMapping* components) const { + const VkComponentMapping* components, + VkImageUsageFlags viewUsage) const { VkImageViewCreateInfo viewInfo{}; viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; viewInfo.image = image; @@ -1340,6 +1586,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer; viewInfo.subresourceRange.layerCount = layerCount; + VkImageViewUsageCreateInfo usageInfo{}; + if (viewUsage != 0) { + usageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO; + usageInfo.usage = viewUsage; + viewInfo.pNext = &usageInfo; + } + VkImageView view = VK_NULL_HANDLE; VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)"); return view; @@ -1665,4 +1918,55 @@ namespace MobileGL::MG_Backend::DirectVulkan { } return imageAspect; } + + VkFormat VkTextureManager::ResolveSampledImageViewFormat(VkFormat imageFormat, + SamplerNumericDomain numericDomain) { + if (imageFormat == VK_FORMAT_UNDEFINED || numericDomain == SamplerNumericDomain::Unknown || + FormatMatchesSamplerNumericDomain(imageFormat, numericDomain)) { + return imageFormat; + } + if (!IsMutableStorageImageFormat(imageFormat)) { + return VK_FORMAT_UNDEFINED; + } + + // Preserve component ordering and bit widths. This selects R32_UINT for an R32_SFLOAT + // texture sampled by a usampler rather than an arbitrary member (such as + // R8G8B8A8_UINT) of Vulkan's broad 32-bit compatibility class. + for (Int candidateValue = static_cast(VK_FORMAT_R4G4_UNORM_PACK8); + candidateValue <= static_cast(VK_FORMAT_ASTC_12x12_SRGB_BLOCK); + ++candidateValue) { + const VkFormat candidate = static_cast(candidateValue); + if (!IsMutableStorageImageFormat(candidate) || + !FormatMatchesSamplerNumericDomain(candidate, numericDomain) || + !HasMatchingColorComponentLayout(imageFormat, candidate) || + !AreSampledImageViewFormatsCompatible(imageFormat, candidate)) { + continue; + } + + // If an integer backing is intentionally bit-read through a float sampler, require + // a true floating-point view. Normalized/scaled views satisfy OpTypeFloat but apply + // an unrelated numeric conversion to those bits. + if (numericDomain == SamplerNumericDomain::Float && !vkuFormatIsSFLOAT(candidate)) { + continue; + } + return candidate; + } + return VK_FORMAT_UNDEFINED; + } + + Bool VkTextureManager::AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) { + if (imageFormat == viewFormat) { + return true; + } + return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) && + vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat); + } + + Bool VkTextureManager::AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) { + if (imageFormat == viewFormat) { + return true; + } + return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) && + vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat); + } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index e2683936..9e3a3567 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -19,6 +19,8 @@ class ITextureObject; } namespace MobileGL::MG_Backend::DirectVulkan { +enum class SamplerNumericDomain : Uint8; + class VkTextureManager { public: // Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass @@ -78,6 +80,61 @@ public: } }; + struct StorageImageViewKey { + Uint32 mipLevel = 0; + Uint32 baseArrayLayer = 0; + Uint32 layerCount = 1; + VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; + VkFormat format = VK_FORMAT_UNDEFINED; + + Bool operator==(const StorageImageViewKey& other) const { + return mipLevel == other.mipLevel && + baseArrayLayer == other.baseArrayLayer && + layerCount == other.layerCount && + viewType == other.viewType && + format == other.format; + } + }; + + struct SampledImageViewKey { + Uint32 baseMipLevel = 0; + Uint32 levelCount = 1; + VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; + VkFormat format = VK_FORMAT_UNDEFINED; + + Bool operator==(const SampledImageViewKey& other) const { + return baseMipLevel == other.baseMipLevel && + levelCount == other.levelCount && + viewType == other.viewType && + format == other.format; + } + }; + + struct SampledImageViewKeyHash { + SizeT operator()(const SampledImageViewKey& key) const { + SizeT hash = std::hash{}(key.baseMipLevel); + hash ^= std::hash{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(static_cast(key.viewType)) + + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(static_cast(key.format)) + + 0x9e3779b9u + (hash << 6) + (hash >> 2); + return hash; + } + }; + + struct StorageImageViewKeyHash { + SizeT operator()(const StorageImageViewKey& key) const { + SizeT hash = std::hash{}(key.mipLevel); + hash ^= std::hash{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(static_cast(key.viewType)) + + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(static_cast(key.format)) + + 0x9e3779b9u + (hash << 6) + (hash >> 2); + return hash; + } + }; + VkImage image = VK_NULL_HANDLE; VmaAllocation allocation = nullptr; VkImageView fullView = VK_NULL_HANDLE; @@ -85,6 +142,8 @@ public: Vector perMipViews; Vector perMipSampledViews; UnorderedMap attachmentViews; + UnorderedMap alternateSampledViews; + UnorderedMap storageImageViews; VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; VkExtent2D extent = {0, 0}; Uint32 depth = 1; @@ -96,6 +155,7 @@ public: VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; + VkImageCreateFlags imageCreateFlags = 0; Uint16 syncedTextureParamsVersion = 0; // Snapshot of ITextureObject::GetContentVersion() at the last successful sync; // lets SyncTexture skip the whole re-check/re-upload when content is unchanged. @@ -115,6 +175,8 @@ public: std::swap(this->perMipViews, that.perMipViews); std::swap(this->perMipSampledViews, that.perMipSampledViews); std::swap(this->attachmentViews, that.attachmentViews); + std::swap(this->alternateSampledViews, that.alternateSampledViews); + std::swap(this->storageImageViews, that.storageImageViews); std::swap(this->layout, that.layout); std::swap(this->extent, that.extent); std::swap(this->depth, that.depth); @@ -126,6 +188,7 @@ public: std::swap(this->aspect, that.aspect); std::swap(this->viewType, that.viewType); std::swap(this->sampleCount, that.sampleCount); + std::swap(this->imageCreateFlags, that.imageCreateFlags); std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); @@ -153,6 +216,16 @@ public: vkDestroyImageView(s_device, attachmentView, nullptr); } } + for (const auto& [_, sampledView] : alternateSampledViews) { + if (sampledView != VK_NULL_HANDLE) { + vkDestroyImageView(s_device, sampledView, nullptr); + } + } + for (const auto& [_, storageImageView] : storageImageViews) { + if (storageImageView != VK_NULL_HANDLE) { + vkDestroyImageView(s_device, storageImageView, nullptr); + } + } if (image != VK_NULL_HANDLE && allocation != nullptr) { vmaDestroyImage(s_allocator, image, allocation); } @@ -161,6 +234,8 @@ public: perMipViews.clear(); perMipSampledViews.clear(); attachmentViews.clear(); + alternateSampledViews.clear(); + storageImageViews.clear(); image = VK_NULL_HANDLE; allocation = nullptr; layout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -174,6 +249,7 @@ public: aspect = VK_IMAGE_ASPECT_NONE; viewType = VK_IMAGE_VIEW_TYPE_2D; sampleCount = VK_SAMPLE_COUNT_1_BIT; + imageCreateFlags = 0; syncedTextureParamsVersion = 0; syncedContentVersion = 0; syncedMipLevelCount = 0; @@ -198,6 +274,9 @@ public: Uint32 baseArrayLayer, Uint32 layerCount, VkImageViewType viewType); VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel); + VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format); + VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, + VkFormat format, Bool layered, Int32 layer); void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout); void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject* texture, @@ -207,6 +286,9 @@ public: Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect); + static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain); + static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat); + static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat); static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout, VkImageLayout newLayout, VkPipelineStageFlags srcStageMask, @@ -255,7 +337,8 @@ private: VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount, Uint32 baseArrayLayer, Uint32 layerCount, - const VkComponentMapping* components = nullptr) const; + const VkComponentMapping* components = nullptr, + VkImageUsageFlags viewUsage = 0) const; Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture, TextureUploadTarget uploadTarget, TextureResource &outResource); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e34fbc38..7f14c7ae 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -579,6 +579,88 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + template + static Float ConvertIntegerVertexComponentToFloat(ComponentT value, Bool normalized) { + if (!normalized) { + return static_cast(value); + } + if constexpr (std::is_signed_v) { + const Float scaled = static_cast(value) / + static_cast(std::numeric_limits::max()); + return std::max(-1.0f, scaled); + } else { + return static_cast(value) / + static_cast(std::numeric_limits::max()); + } + } + + template + static Bool ConvertIntegerVertexStreamToFloat32( + const MG_State::GLState::VertexAttribute& attribute, + const Uint8* sourceData, + SizeT sourceStride, + SizeT elementCount, + Vector& outData) { + if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) { + return false; + } + + const SizeT componentCount = static_cast(attribute.Size); + outData.resize(elementCount * componentCount); + for (SizeT element = 0; element < elementCount; ++element) { + const Uint8* sourceElement = sourceData + element * sourceStride; + Float* destinationElement = outData.data() + element * componentCount; + for (SizeT component = 0; component < componentCount; ++component) { + ComponentT value{}; + Memcpy(&value, sourceElement + component * sizeof(ComponentT), sizeof(ComponentT)); + destinationElement[component] = + ConvertIntegerVertexComponentToFloat(value, attribute.Normalized); + } + } + return true; + } + + static Bool ConvertScaledIntegerVertexStreamToFloat32( + const MG_State::GLState::VertexAttribute& attribute, + const Uint8* sourceData, + SizeT sourceStride, + SizeT elementCount, + Vector& outData) { + switch (attribute.Type) { + case DataType::Int8: + return ConvertIntegerVertexStreamToFloat32( + attribute, sourceData, sourceStride, elementCount, outData); + case DataType::Uint8: + return ConvertIntegerVertexStreamToFloat32( + attribute, sourceData, sourceStride, elementCount, outData); + case DataType::Int16: + return ConvertIntegerVertexStreamToFloat32( + attribute, sourceData, sourceStride, elementCount, outData); + case DataType::Uint16: + return ConvertIntegerVertexStreamToFloat32( + attribute, sourceData, sourceStride, elementCount, outData); + default: + return false; + } + } + + static Bool RepackVertexStream(const Uint8* sourceData, + SizeT sourceStride, + SizeT elementSize, + SizeT elementCount, + Vector& outData) { + if (sourceData == nullptr || sourceStride == 0 || elementSize == 0) { + return false; + } + outData.resize(elementCount * elementSize); + for (SizeT element = 0; element < elementCount; ++element) { + Memcpy(outData.data() + element * elementSize, + sourceData + element * sourceStride, + elementSize); + } + return true; + } + static NumericDomain GetNumericDomainForTextureInternalFormat(TextureInternalFormat format) { switch (format) { case TextureInternalFormat::R8I: @@ -1911,7 +1993,8 @@ void main() { PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite); } m_programFactory = MakeUnique(m_device, m_config, maxProgramBindings, - m_shaderDrawParametersFeatureEnabled); + m_shaderDrawParametersFeatureEnabled, + m_unformattedFloatStorageImagesEnabled); MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed."); m_samplerManager = MakeUnique(); @@ -1931,7 +2014,7 @@ void main() { m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight, maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get()); MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed."); - m_vertexInputStateFactory = MakeUnique(m_config); + m_vertexInputStateFactory = MakeUnique(m_config, m_physicalDevice.handle); MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed."); // Prime the first frame so Render() always targets an acquired swapchain image. @@ -1946,6 +2029,7 @@ void main() { VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); + m_convertedVertexStreams.clear(); MGLOG_D("VulkanRenderer initialized"); } @@ -2064,7 +2148,8 @@ void main() { Bool VulkanRenderer::UploadAndBindVertexBuffers( VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, - const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams) { + const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams, + Bool indexedDraw) { // programObj is resolved once in SetupDraw and passed in; re-resolving it here would repeat // the GetCurrentProgram + GetOrCreateProgram hash lookup every draw. auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); @@ -2090,6 +2175,39 @@ void main() { return nullptr; }; + auto uploadConvertedStream = [&](VertexInputStateFactory::VertexStreamConversion conversion, + const MG_State::GLState::VertexAttribute& attribute, + const Uint8* sourceData, SizeT sourceStride, + SizeT elementSize, SizeT elementCount, + BufferSlice& outSlice) -> Bool { + const void* uploadData = nullptr; + VkDeviceSize uploadSize = 0; + switch (conversion) { + case VertexInputStateFactory::VertexStreamConversion::Repack: + if (!RepackVertexStream(sourceData, sourceStride, elementSize, elementCount, + m_vertexRepackScratch)) { + return false; + } + uploadData = m_vertexRepackScratch.data(); + uploadSize = static_cast(m_vertexRepackScratch.size()); + break; + case VertexInputStateFactory::VertexStreamConversion::ScaledIntegerToFloat32: + if (!ConvertScaledIntegerVertexStreamToFloat32(attribute, sourceData, sourceStride, + elementCount, m_vertexConversionScratch)) { + return false; + } + uploadData = m_vertexConversionScratch.data(); + uploadSize = static_cast(m_vertexConversionScratch.size() * sizeof(Float)); + break; + case VertexInputStateFactory::VertexStreamConversion::None: + return false; + } + return uploadSize > 0 && + m_bufferManager.UploadTransient(BufferKind::Vertex, + m_frameContext.GetCurrentFrameIndex(), + uploadData, uploadSize, 16, outSlice); + }; + for (SizeT binding = 0; binding < bindingCount; ++binding) { if (binding >= vertexInputState.bindings.size()) { break; @@ -2099,28 +2217,53 @@ void main() { : static_cast(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); const Bool usesClientMemory = binding < vertexInputState.bindingUsesClientMemory.size() && vertexInputState.bindingUsesClientMemory[binding]; + const auto conversion = binding < vertexInputState.bindingConversions.size() + ? vertexInputState.bindingConversions[binding] + : VertexInputStateFactory::VertexStreamConversion::None; if (usesClientMemory) { const Uint32 location = bindingLocation; MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, "UploadAndBindVertexStreams failed to resolve client attribute location"); const auto& attr = vao.GetAttribute(location); - const SizeT componentSize = VertexInputStateFactory::GetComponentSize(attr.Type); - const SizeT elementSize = componentSize * static_cast(attr.Size); + const SizeT elementSize = + VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); const SizeT stride = attr.Stride > 0 ? static_cast(attr.Stride) : elementSize; const auto* clientData = reinterpret_cast(attr.Offset); - if (!clientData || componentSize == 0 || elementSize == 0 || stride == 0) { + if (!clientData || elementSize == 0 || stride == 0) { MGLOG_E("UploadAndBindVertexStreams skipped: invalid client vertex attribute at location %u", location); return false; } + if (conversion != VertexInputStateFactory::VertexStreamConversion::None && indexedDraw) { + // The current indexed setup only carries indexCount, not the maximum effective + // index. Guessing a client-memory range here can truncate the converted stream. + MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute " + "location=%u requires an indexed vertex range", location); + return false; + } + if (conversion != VertexInputStateFactory::VertexStreamConversion::None && + drawParams.vertexCount == 0) { + MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute " + "location=%u has an unknown vertex range", location); + return false; + } + const Uint32 lastVertex = drawParams.vertexCount > 0 ? drawParams.firstVertex + drawParams.vertexCount - 1 : drawParams.firstVertex; - const SizeT uploadSize = static_cast(lastVertex) * stride + elementSize; BufferSlice slice{}; - if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), - clientData, static_cast(uploadSize), 16, slice)) { + Bool uploaded = false; + if (conversion == VertexInputStateFactory::VertexStreamConversion::None) { + const SizeT uploadSize = static_cast(lastVertex) * stride + elementSize; + uploaded = m_bufferManager.UploadTransient( + BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), clientData, + static_cast(uploadSize), 16, slice); + } else { + uploaded = uploadConvertedStream(conversion, attr, clientData, stride, elementSize, + static_cast(lastVertex) + 1, slice); + } + if (!uploaded) { MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload client attribute binding %zu", binding); @@ -2142,6 +2285,64 @@ void main() { const auto& sourceBufferShared = *sourceBufferSharedPtr; BufferSlice slice{}; const SizeT sourceSize = sourceBufferShared->GetSize(); + const SizeT baseOffset = + binding < vertexInputState.bindingBaseOffsets.size() ? vertexInputState.bindingBaseOffsets[binding] : 0; + MOBILEGL_ASSERT(baseOffset <= sourceSize, + "UploadAndBindVertexStreams skipped: binding %zu base offset %zu exceeds buffer size %zu", + binding, baseOffset, sourceSize); + + if (conversion != VertexInputStateFactory::VertexStreamConversion::None) { + MOBILEGL_ASSERT(bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, + "UploadAndBindVertexStreams failed to resolve converted attribute location"); + const auto& attr = vao.GetAttribute(bindingLocation); + const SizeT elementSize = + VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); + const SizeT sourceStride = + attr.Stride > 0 ? static_cast(attr.Stride) : elementSize; + if (sourceBufferShared->MappedData() == nullptr || elementSize == 0 || sourceStride == 0 || + baseOffset > sourceSize || elementSize > sourceSize - baseOffset) { + MGLOG_E("UploadAndBindVertexStreams skipped: invalid converted source binding=%zu " + "location=%u base=%zu size=%zu element=%zu stride=%zu", + binding, bindingLocation, baseOffset, sourceSize, elementSize, sourceStride); + return false; + } + + sourceBufferShared->SyncPersistentMappedRange(); + const SizeT elementCount = 1 + (sourceSize - baseOffset - elementSize) / sourceStride; + const ConvertedVertexStreamKey cacheKey{ + .buffer = sourceBufferShared.get(), + .changeSerial = sourceBufferShared->GetChangeSerial(), + .baseOffset = baseOffset, + .sourceStride = static_cast(sourceStride), + .type = attr.Type, + .size = attr.Size, + .normalized = attr.Normalized, + .isInteger = attr.IsInteger, + .conversion = conversion, + }; + + const Bool cacheable = !sourceBufferShared->IsBackendPersistentMapped(); + auto cached = cacheable ? m_convertedVertexStreams.find(cacheKey) + : m_convertedVertexStreams.end(); + if (cached != m_convertedVertexStreams.end()) { + slice = cached->second; + } else { + const Uint8* sourceData = sourceBufferShared->MappedData() + baseOffset; + if (!uploadConvertedStream(conversion, attr, sourceData, sourceStride, + elementSize, elementCount, slice)) { + MGLOG_E("UploadAndBindVertexStreams skipped: failed to convert binding=%zu location=%u", + binding, bindingLocation); + return false; + } + if (cacheable) { + m_convertedVertexStreams.emplace(cacheKey, slice); + } + } + vkBuffers[binding] = slice.buffer; + vkOffsets[binding] = slice.offset; + continue; + } + if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared)) { if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) { MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding); @@ -2154,11 +2355,6 @@ void main() { } } vkBuffers[binding] = slice.buffer; - const SizeT baseOffset = - binding < vertexInputState.bindingBaseOffsets.size() ? vertexInputState.bindingBaseOffsets[binding] : 0; - MOBILEGL_ASSERT(baseOffset <= sourceSize, - "UploadAndBindVertexStreams skipped: binding %zu base offset %zu exceeds buffer size %zu", - binding, baseOffset, sourceSize); vkOffsets[binding] = slice.offset + static_cast(baseOffset); } @@ -3272,6 +3468,44 @@ void main() { return pipeline; } + Bool VulkanRenderer::PrepareStorageImageTextures( + VkCommandBuffer commandBuffer, + const MG_State::GLState::ProgramObject& program, + const ProgramFactory::VkProgramObject& programObj) { + auto& storageTextures = m_storageImageTexturesScratch; + if (!m_uniformManager->CollectStorageImageTextures(program, programObj, storageTextures)) { + MGLOG_E("%s: failed to collect storage images for program=%u", + __func__, program.GetExternalIndex()); + return false; + } + if (storageTextures.empty()) { + return true; + } + + // Image uploads, deferred-clear materialization, and layout barriers are illegal inside + // a classic render pass. Do this before sampler preparation as well: a texture used by + // both a sampler and an image must stay in GENERAL, and both descriptors must name that + // same layout independent of SPIR-V reflection/binding order. + if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { + VkRenderPassManager::EndRenderPass(commandBuffer); + } + + for (auto* texture : storageTextures) { + MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__); + if (!MaterializePendingClearForTexture(commandBuffer, *texture)) { + MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d", + __func__, texture->GetExternalIndex()); + return false; + } + if (!m_textureManager->TransitionTextureForStorageImage(commandBuffer, *texture)) { + MGLOG_E("%s: failed to prepare storage textureId=%d", + __func__, texture->GetExternalIndex()); + return false; + } + } + return true; + } + Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView) { @@ -3299,6 +3533,11 @@ void main() { m_lastSampledSetValid = false; } + if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { + MGLOG_E("SetupDraw skipped: storage image preparation failed"); + return false; + } + auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); // Check if any of the textures to sample have pending clears, @@ -3469,7 +3708,8 @@ void main() { return false; } - auto vtxUploadOk = UploadAndBindVertexBuffers(frame.commandBuffer, vao, programObj, drawParams); + auto vtxUploadOk = UploadAndBindVertexBuffers( + frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView != nullptr); if (!vtxUploadOk) { MGLOG_E("SetupDraw skipped: failed to upload vertex buffers"); return false; @@ -3518,6 +3758,11 @@ void main() { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } + if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { + MGLOG_E("DispatchCompute skipped: storage image preparation failed"); + return; + } + const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); if (pipeline == VK_NULL_HANDLE) { MGLOG_E("DispatchCompute skipped: compute pipeline creation failed for program=%u", @@ -3553,6 +3798,11 @@ void main() { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } + if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { + MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed"); + return; + } + const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); if (pipeline == VK_NULL_HANDLE) { MGLOG_E("DispatchComputeIndirect skipped: compute pipeline creation failed for program=%u", @@ -6357,6 +6607,7 @@ void main() { CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); + m_convertedVertexStreams.clear(); // Descriptor-set reuse cursors rewind exactly once per frame, here, // after the slot's fence wait proved its previous sets GPU-idle. (The // per-draw-path lazy rewind missed frames whose recording was opened @@ -6674,6 +6925,10 @@ void main() { vkGetPhysicalDeviceFeatures(m_physicalDevice.handle, &supportedDeviceFeatures); VkPhysicalDeviceFeatures deviceFeatures{}; + // Match GL's robust buffer-fetch behavior where the Vulkan device supports it. This covers + // out-of-range fetches; arbitrary GL vertex strides/offsets still need the explicit tight + // repack in VertexInputStateFactory when they violate Vulkan's address-alignment rules. + deviceFeatures.robustBufferAccess = supportedDeviceFeatures.robustBufferAccess; deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader; deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend; m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE; @@ -6687,6 +6942,23 @@ void main() { deviceFeatures.wideLines = supportedDeviceFeatures.wideLines; m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE; deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64; + // Required for desktop GL image load/store semantics. iterationRP writes storage + // images from vertex and fragment stages and uses formats outside Vulkan's small + // mandatory storage-image set. + deviceFeatures.vertexPipelineStoresAndAtomics = + supportedDeviceFeatures.vertexPipelineStoresAndAtomics; + deviceFeatures.fragmentStoresAndAtomics = supportedDeviceFeatures.fragmentStoresAndAtomics; + deviceFeatures.shaderStorageImageExtendedFormats = + supportedDeviceFeatures.shaderStorageImageExtendedFormats; + // The formatless float-storage compatibility path must be all-or-nothing: transformed + // modules declare both capabilities and image bindings may be read, written, or both. + m_unformattedFloatStorageImagesEnabled = + supportedDeviceFeatures.shaderStorageImageReadWithoutFormat == VK_TRUE && + supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat == VK_TRUE; + if (m_unformattedFloatStorageImagesEnabled) { + deviceFeatures.shaderStorageImageReadWithoutFormat = VK_TRUE; + deviceFeatures.shaderStorageImageWriteWithoutFormat = VK_TRUE; + } deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance; deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect; m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE; @@ -6806,8 +7078,12 @@ void main() { deviceCreateInfo.enabledExtensionCount = static_cast(enabledDeviceExtensions.size()); deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data(); - MGLOG_I("Device feature support: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " - "shaderCullDistance=%s wideLines=%s shaderInt64=%s drawIndirectFirstInstance=%s multiDrawIndirect=%s", + MGLOG_I("Device feature support: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " + "shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s " + "fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s " + "storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s " + "multiDrawIndirect=%s", + supportedDeviceFeatures.robustBufferAccess ? "true" : "false", supportedDeviceFeatures.geometryShader ? "true" : "false", supportedDeviceFeatures.independentBlend ? "true" : "false", supportedDeviceFeatures.logicOp ? "true" : "false", @@ -6815,11 +7091,19 @@ void main() { supportedDeviceFeatures.shaderCullDistance ? "true" : "false", supportedDeviceFeatures.wideLines ? "true" : "false", supportedDeviceFeatures.shaderInt64 ? "true" : "false", + supportedDeviceFeatures.vertexPipelineStoresAndAtomics ? "true" : "false", + supportedDeviceFeatures.fragmentStoresAndAtomics ? "true" : "false", + supportedDeviceFeatures.shaderStorageImageExtendedFormats ? "true" : "false", + supportedDeviceFeatures.shaderStorageImageReadWithoutFormat ? "true" : "false", + supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat ? "true" : "false", supportedDeviceFeatures.drawIndirectFirstInstance ? "true" : "false", supportedDeviceFeatures.multiDrawIndirect ? "true" : "false"); - MGLOG_I("Device feature enabled: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " - "shaderCullDistance=%s wideLines=%s shaderInt64=%s drawIndirectFirstInstance=%s multiDrawIndirect=%s " - "shaderDrawParameters=%s", + MGLOG_I("Device feature enabled: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " + "shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s " + "fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s " + "storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s " + "multiDrawIndirect=%s shaderDrawParameters=%s", + deviceFeatures.robustBufferAccess ? "true" : "false", deviceFeatures.geometryShader ? "true" : "false", deviceFeatures.independentBlend ? "true" : "false", deviceFeatures.logicOp ? "true" : "false", @@ -6827,6 +7111,11 @@ void main() { deviceFeatures.shaderCullDistance ? "true" : "false", deviceFeatures.wideLines ? "true" : "false", deviceFeatures.shaderInt64 ? "true" : "false", + deviceFeatures.vertexPipelineStoresAndAtomics ? "true" : "false", + deviceFeatures.fragmentStoresAndAtomics ? "true" : "false", + deviceFeatures.shaderStorageImageExtendedFormats ? "true" : "false", + deviceFeatures.shaderStorageImageReadWithoutFormat ? "true" : "false", + deviceFeatures.shaderStorageImageWriteWithoutFormat ? "true" : "false", deviceFeatures.drawIndirectFirstInstance ? "true" : "false", deviceFeatures.multiDrawIndirect ? "true" : "false", m_shaderDrawParametersFeatureEnabled ? "true" : "false"); @@ -7153,6 +7442,7 @@ void main() { m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); + m_convertedVertexStreams.clear(); } } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index b5c7a57e..23326434 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -365,6 +365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool m_samplerAnisotropyFeatureEnabled = false; Bool m_shaderDrawParametersExtensionEnabled = false; Bool m_shaderDrawParametersFeatureEnabled = false; + Bool m_unformattedFloatStorageImagesEnabled = false; // fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates // per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and // drive a runtime fallback when the device lacks them. @@ -437,9 +438,53 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Per-draw scratch buffers (clear keeps capacity) — these paths run for every // draw call and must not allocate. Vector m_sampledTexturesScratch; + Vector m_storageImageTexturesScratch; Vector m_vertexBuffersScratch; Vector m_vertexOffsetsScratch; Vector m_patchedAttributesScratch; + Vector m_vertexConversionScratch; + Vector m_vertexRepackScratch; + + struct ConvertedVertexStreamKey { + const MG_State::GLState::BufferObject* buffer = nullptr; + Uint64 changeSerial = 0; + SizeT baseOffset = 0; + Uint32 sourceStride = 0; + DataType type = DataType::Float32; + Int size = 0; + Bool normalized = false; + Bool isInteger = false; + VertexInputStateFactory::VertexStreamConversion conversion = + VertexInputStateFactory::VertexStreamConversion::None; + + Bool operator==(const ConvertedVertexStreamKey& other) const { + return buffer == other.buffer && changeSerial == other.changeSerial && + baseOffset == other.baseOffset && sourceStride == other.sourceStride && + type == other.type && size == other.size && normalized == other.normalized && + isInteger == other.isInteger && conversion == other.conversion; + } + }; + + struct ConvertedVertexStreamKeyHash { + SizeT operator()(const ConvertedVertexStreamKey& key) const { + SizeT hash = std::hash{}(key.buffer); + auto combine = [&hash](SizeT value) { + hash ^= value + static_cast(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2); + }; + combine(std::hash{}(key.changeSerial)); + combine(std::hash{}(key.baseOffset)); + combine(std::hash{}(key.sourceStride)); + combine(std::hash{}(static_cast(key.type))); + combine(std::hash{}(key.size)); + combine(std::hash{}(key.normalized)); + combine(std::hash{}(key.isInteger)); + combine(std::hash{}(static_cast(key.conversion))); + return hash; + } + }; + + UnorderedMap + m_convertedVertexStreams; void CreateInstance(); VkResult SetupDebugMessenger(); @@ -462,10 +507,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { const RenderPassEntry& renderPassEntry); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); void DestroyComputePipelines(); + Bool PrepareStorageImageTextures( + VkCommandBuffer commandBuffer, + const MG_State::GLState::ProgramObject& program, + const ProgramFactory::VkProgramObject& programObj); Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, const ProgramFactory::VkProgramObject& programObj, - const DrawCmdParam& drawParams); + const DrawCmdParam& drawParams, Bool indexedDraw); Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame, const MG_State::GLState::VertexArrayObject& vao, const IndexBufferView* pIndexBufferView = nullptr); diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 94991363..727c0544 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -998,8 +998,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers) DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset) DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data) +DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data) +DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes) DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures) @@ -1063,7 +1063,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width) -DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height) +DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height) DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param) DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 869dd4a4..3bf5ec80 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -1175,10 +1175,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxFragmentInputComponents; return; case GL_MAX_FRAGMENT_IMAGE_UNIFORMS: - // TODO: Track per-stage image uniform limits separately instead of reusing the compute/backend stage cap. *params = MG_Backend::pActiveBackendObject - ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxComputeImageUniforms - : MG_Backend::DynamicBackendParameters{}.MaxComputeImageUniforms; + ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxFragmentImageUniforms + : MG_Backend::DynamicBackendParameters{}.MaxFragmentImageUniforms; return; case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS: *params = kFrontendMaxFragmentUniformComponents; @@ -1208,7 +1207,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxGeometryTextureImageUnits; return; case GL_MAX_GEOMETRY_IMAGE_UNIFORMS: - *params = 0; + *params = MG_Backend::pActiveBackendObject + ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxGeometryImageUniforms + : MG_Backend::DynamicBackendParameters{}.MaxGeometryImageUniforms; return; case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS: *params = kFrontendMaxGeometryTotalOutputComponents; @@ -1277,7 +1278,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxVertexAtomicCounters; return; case GL_MAX_VERTEX_IMAGE_UNIFORMS: - *params = 0; + *params = MG_Backend::pActiveBackendObject + ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms + : MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms; return; case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: *params = 16; // TODO diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index fc34411d..7094ffa0 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -470,6 +470,210 @@ namespace MobileGL::MG_Impl::GLImpl { return textureObject; } + namespace { + void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) { + MG_State::pGLContext->RecordError( + code, MakeUnique("MG_Impl/GLImpl", caller, message)); + } + + SharedPtr GetClearTextureObject(GLuint texture, GLint level, + const char* caller) { + if (texture == 0) { + RecordClearTextureError(caller, ErrorCode::InvalidOperation, + "Clear texture operations require a non-zero texture name."); + return nullptr; + } + + auto textureObject = GetTextureObjectByName(texture, caller); + if (!textureObject) return nullptr; + if (textureObject->GetStorageType() != TextureStorageType::Mipmap) { + RecordClearTextureError(caller, ErrorCode::InvalidOperation, + "Buffer textures cannot be cleared with glClearTexImage."); + return nullptr; + } + + auto mipmapTexture = std::static_pointer_cast(textureObject); + if (level < 0 || static_cast(level) >= mipmapTexture->GetMipmapLevelCount()) { + RecordClearTextureError(caller, ErrorCode::InvalidValue, + std::format("Texture level {} is not defined.", level)); + return nullptr; + } + return mipmapTexture; + } + + Bool BuildClearPixel(const SharedPtr& textureObject, + GLenum format, GLenum type, const void* data, Vector& clearPixel) { + const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format); + const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); + if (!TextureImpl::ValidateTextureInputFormat(inputFormat) || + !TextureImpl::ValidateTexturePixelDataType(inputType) || + !TextureImpl::ValidateTextureInternalFormatCompatibleWithInput( + inputFormat, textureObject->GetFormat(), inputType)) { + return false; + } + + clearPixel.clear(); + if (data == nullptr) { + // ARB_clear_texture defines a null clear value as all zeroes. Keeping the + // pattern empty lets the region writer use a fast memset path. + return true; + } + + PixelStoreParameters clearPixelStore{}; + clearPixelStore.Alignment = 1; + SizeT clearPixelSize = 0; + void* converted = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack( + data, clearPixelStore, textureObject->GetFormat(), inputFormat, inputType, + {1, 1, 1}, false, clearPixelSize); + if (!converted || clearPixelSize == 0) { + if (converted) free(converted); + return false; + } + + clearPixel.resize(clearPixelSize); + Memcpy(clearPixel.data(), converted, clearPixelSize); + free(converted); + return true; + } + + Bool ClearMipmapRegion(const SharedPtr& textureObject, + TextureUploadTarget uploadTarget, GLint level, + GLint xoffset, GLint yoffset, GLint zoffset, + GLsizei width, GLsizei height, GLsizei depth, + const Vector& clearPixel, const char* caller) { + const IntVec3 texelSize = textureObject->GetMipmapTexelSize(uploadTarget, static_cast(level)); + if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) { + RecordClearTextureError(caller, ErrorCode::InvalidOperation, + "The requested texture level has no storage."); + return false; + } + if (xoffset < 0 || yoffset < 0 || zoffset < 0 || + width < 0 || height < 0 || depth < 0 || + width > texelSize.x() - xoffset || + height > texelSize.y() - yoffset || + depth > texelSize.z() - zoffset) { + RecordClearTextureError(caller, ErrorCode::InvalidValue, + "The clear region lies outside the requested texture level."); + return false; + } + if (width == 0 || height == 0 || depth == 0) return true; + + const SizeT texelCount = static_cast(texelSize.x()) * + static_cast(texelSize.y()) * + static_cast(texelSize.z()); + const SizeT byteSize = textureObject->GetMipmapByteSize(uploadTarget, static_cast(level)); + if (byteSize == 0 || byteSize % texelCount != 0) { + RecordClearTextureError(caller, ErrorCode::InvalidOperation, + "The requested texture storage cannot be cleared."); + return false; + } + + const SizeT bytesPerTexel = byteSize / texelCount; + if (!clearPixel.empty() && clearPixel.size() != bytesPerTexel) { + RecordClearTextureError( + caller, ErrorCode::InvalidOperation, + std::format("Converted clear value is {} bytes, but the texture stores {} bytes per texel.", + clearPixel.size(), bytesPerTexel)); + return false; + } + + auto* destination = static_cast( + textureObject->MapMipmapData(uploadTarget, static_cast(level))); + if (!destination) { + RecordClearTextureError(caller, ErrorCode::InvalidOperation, + "The requested texture level could not be mapped."); + return false; + } + + const SizeT fullRowBytes = static_cast(texelSize.x()) * bytesPerTexel; + const SizeT fullSliceBytes = static_cast(texelSize.y()) * fullRowBytes; + const SizeT clearRowBytes = static_cast(width) * bytesPerTexel; + Uint8* firstClearRow = nullptr; + + for (GLsizei z = 0; z < depth; ++z) { + for (GLsizei y = 0; y < height; ++y) { + Uint8* row = destination + + static_cast(zoffset + z) * fullSliceBytes + + static_cast(yoffset + y) * fullRowBytes + + static_cast(xoffset) * bytesPerTexel; + if (firstClearRow) { + Memcpy(row, firstClearRow, clearRowBytes); + continue; + } + + firstClearRow = row; + if (clearPixel.empty()) { + Memset(row, 0, clearRowBytes); + continue; + } + + Memcpy(row, clearPixel.data(), bytesPerTexel); + SizeT filled = bytesPerTexel; + while (filled < clearRowBytes) { + const SizeT copySize = std::min(filled, clearRowBytes - filled); + Memcpy(row + filled, row, copySize); + filled += copySize; + } + } + } + + textureObject->MarkStorageDirty(uploadTarget, static_cast(level), true); + return true; + } + } // namespace + + void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data) { + auto textureObject = GetClearTextureObject(texture, level, __func__); + if (!textureObject) return; + + Vector clearPixel; + if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return; + + for (TextureUploadTarget uploadTarget : textureObject->GetUploadTargets()) { + const IntVec3 size = textureObject->GetMipmapTexelSize(uploadTarget, static_cast(level)); + if (!ClearMipmapRegion(textureObject, uploadTarget, level, 0, 0, 0, + size.x(), size.y(), size.z(), clearPixel, __func__)) { + return; + } + } + } + + void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, + GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, + const void* data) { + auto textureObject = GetClearTextureObject(texture, level, __func__); + if (!textureObject) return; + + Vector clearPixel; + if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return; + + const auto& uploadTargets = textureObject->GetUploadTargets(); + if (textureObject->GetTarget() == TextureTarget::TextureCubeMap) { + if (zoffset < 0 || depth < 0 || + static_cast(zoffset) > uploadTargets.size() || + static_cast(depth) > uploadTargets.size() - static_cast(zoffset)) { + RecordClearTextureError(__func__, ErrorCode::InvalidValue, + "The cube-map clear region selects invalid faces."); + return; + } + for (GLsizei face = 0; face < depth; ++face) { + if (!ClearMipmapRegion(textureObject, uploadTargets[static_cast(zoffset + face)], level, + xoffset, yoffset, 0, width, height, 1, clearPixel, __func__)) { + return; + } + } + return; + } + + if (uploadTargets.empty()) { + RecordClearTextureError(__func__, ErrorCode::InvalidOperation, + "The requested texture has no upload target."); + return; + } + ClearMipmapRegion(textureObject, uploadTargets.front(), level, xoffset, yoffset, zoffset, + width, height, depth, clearPixel, __func__); + } + Bool ValidateTextureParameterForTarget(const SharedPtr& textureObject, GLenum pname, GLint param, const char* caller) { const auto target = textureObject->GetTarget(); @@ -3818,6 +4022,14 @@ namespace MobileGL::MG_Impl::GLImpl { CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height); } + void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, + GLsizei width, GLsizei height) { + auto textureObject = GetTextureObjectByName(texture, __func__); + WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { + CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height); + }); + } + void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { CopyTexSubImage1D_State(target, level, xoffset, x, y, width); } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h index eed7fdeb..2b3c34a2 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h @@ -11,6 +11,9 @@ namespace MobileGL::MG_Impl::GLImpl { /* @INSERTION_POINT:FUNCTION_DECLARATION@ */ + void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data); + void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, + GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data); void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); void GenerateMipmap(GLenum target); @@ -95,6 +98,8 @@ namespace MobileGL::MG_Impl::GLImpl { GLsizei width, GLsizei height); void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); + void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, + GLsizei width, GLsizei height); void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width); void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border); diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index 87354376..7af460a5 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -27,6 +27,13 @@ namespace { struct FakeDriverState { // Behavior knobs, configured per test before running the probe. GLint maxVertexSsboBlocks = 4; + GLint glesMajorVersion = 3; + GLint glesMinorVersion = 1; + GLint maxVertexImageUniforms = 2; + GLint maxGeometryImageUniforms = 3; + GLint maxFragmentImageUniforms = 4; + GLint maxComputeImageUniforms = 5; + bool maxGeometryImageUniformsQueried = false; // Emulates ANGLE-on-Vulkan: the draw reads the indirect command's // baseInstance word and exposes it through gl_InstanceID. bool drawLeaksBaseInstanceWord = false; @@ -88,13 +95,26 @@ namespace { case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: *data = g_fake.maxVertexSsboBlocks; break; + case GL_MAX_VERTEX_IMAGE_UNIFORMS: + *data = g_fake.maxVertexImageUniforms; + break; + case GL_MAX_GEOMETRY_IMAGE_UNIFORMS: + g_fake.maxGeometryImageUniformsQueried = true; + *data = g_fake.maxGeometryImageUniforms; + break; + case GL_MAX_FRAGMENT_IMAGE_UNIFORMS: + *data = g_fake.maxFragmentImageUniforms; + break; + case GL_MAX_COMPUTE_IMAGE_UNIFORMS: + *data = g_fake.maxComputeImageUniforms; + break; // FillInGLESCapabilities reads the context version before running the // baseInstance probe, which requires ES >= 3.1. case GL_MAJOR_VERSION: - *data = 3; + *data = g_fake.glesMajorVersion; break; case GL_MINOR_VERSION: - *data = 1; + *data = g_fake.glesMinorVersion; break; case GL_NUM_EXTENSIONS: *data = static_cast(g_fake.extensions.size()); @@ -417,6 +437,31 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) { ExpectProbeReleasedAllObjects(); } +TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGeometry) { + const auto funcs = MakeFakeGLESFunctions(); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + MobileGL::MG_External::GLESCapabilities es31Caps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs)); + EXPECT_EQ(es31Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms); + EXPECT_EQ(es31Caps.MaxGeometryImageUniforms, 0); + EXPECT_EQ(es31Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms); + EXPECT_EQ(es31Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms); + EXPECT_FALSE(g_fake.maxGeometryImageUniformsQueried); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.glesMinorVersion = 2; + MobileGL::MG_External::GLESCapabilities es32Caps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs)); + EXPECT_EQ(es32Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms); + EXPECT_EQ(es32Caps.MaxGeometryImageUniforms, g_fake.maxGeometryImageUniforms); + EXPECT_EQ(es32Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms); + EXPECT_EQ(es32Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms); + EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried); +} + // 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) { diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index a26699bc..ab8d175d 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include +#include #include #include @@ -1325,6 +1326,88 @@ TEST_F(ProgramTest, CompileAndLinkWithExplicitVertexIn) { << "\")"; } +TEST_F(ProgramTest, InactiveExplicitVertexBindingsDoNotReserveLocations) { + const char* vertexSource = R"(#version 430 compatibility + +in vec3 Position; +in vec2 UV0; +in vec3 vaPosition; + +void main() { + gl_Position = vec4(vaPosition, 1.0); +} +)"; + const char* fragmentSource = R"(#version 430 compatibility + +out vec4 fragColor; + +void main() { + fragColor = vec4(1.0); +} +)"; + + GLuint vertexShader = CreateShader(GL_VERTEX_SHADER); + ShaderSource(vertexShader, 1, &vertexSource, nullptr); + CompileShader(vertexShader); + GLint compileStatus = GL_FALSE; + GetShaderiv(vertexShader, GL_COMPILE_STATUS, &compileStatus); + ASSERT_EQ(compileStatus, GL_TRUE); + + GLuint fragmentShader = CreateShader(GL_FRAGMENT_SHADER); + ShaderSource(fragmentShader, 1, &fragmentSource, nullptr); + CompileShader(fragmentShader); + GetShaderiv(fragmentShader, GL_COMPILE_STATUS, &compileStatus); + ASSERT_EQ(compileStatus, GL_TRUE); + + GLuint program = CreateProgram(); + AttachShader(program, vertexShader); + AttachShader(program, fragmentShader); + + // Iris binds these canonical names before linking every program. Its compatibility + // transformer can inject both declarations even when the shader pack instead reads + // vaPosition. Inactive API bindings must not consume locations during the link. + BindAttribLocation(program, 0, "Position"); + BindAttribLocation(program, 1, "UV0"); + LinkProgram(program); + + GLint linkStatus = GL_FALSE; + GetProgramiv(program, GL_LINK_STATUS, &linkStatus); + ASSERT_EQ(linkStatus, GL_TRUE); + + EXPECT_EQ(GetAttribLocation(program, "Position"), -1); + EXPECT_EQ(GetAttribLocation(program, "UV0"), -1); + EXPECT_EQ(GetAttribLocation(program, "vaPosition"), 0); + + auto programObject = MG_State::pGLContext->GetProgramObject(program); + ASSERT_NE(programObject, nullptr); + const Int vertexIndex = programObject->GetShaderIndexByStage(ShaderStage::Vertex); + ASSERT_GE(vertexIndex, 0); + const auto& spirvs = programObject->GetGeneratedSpirv(); + ASSERT_LT(static_cast(vertexIndex), spirvs.size()); + + const auto& vertexSpirv = spirvs[vertexIndex]; + spv_reflect::ShaderModule reflection(vertexSpirv.size() * sizeof(Uint), vertexSpirv.data()); + ASSERT_EQ(reflection.GetResult(), SPV_REFLECT_RESULT_SUCCESS); + + uint32_t inputCount = 0; + ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, nullptr), SPV_REFLECT_RESULT_SUCCESS); + Vector inputs(inputCount); + ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, inputs.data()), SPV_REFLECT_RESULT_SUCCESS); + + Uint32 userInputCount = 0; + Uint32 locationMask = 0; + for (const auto* input : inputs) { + if (input == nullptr || (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0) { + continue; + } + ASSERT_LT(input->location, 32u); + locationMask |= 1u << input->location; + ++userInputCount; + } + EXPECT_EQ(userInputCount, 1u); + EXPECT_EQ(locationMask, 0x1u); +} + TEST_F(ProgramTest, CompileAndLinkWithExplicitFragmentOut) { char infoLog[1024] = ""; diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index afbf2586..5ded08a2 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -92,6 +92,120 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) { EXPECT_EQ(exactSamplerNameCount, 1u); } +TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) { + using namespace MG_Util::ShaderTranspiler; + + const String source = R"(#version 430 core +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; +layout(rgba16, binding = 0) uniform image2D floatImage; +layout(r32ui, binding = 1) uniform uimage2D atomicImage; + +void main() { + ivec2 coordinate = ivec2(gl_GlobalInvocationID.xy); + imageStore(floatImage, coordinate, imageLoad(floatImage, coordinate)); + imageAtomicAdd(atomicImage, coordinate, 1u); +} +)"; + + ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source}; + auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib); + ASSERT_TRUE(shaderResult) << shaderResult.error().log; + + ProgramAttrib programAttrib{.shaders = {shaderResult.value()}}; + auto programResult = ShaderCompiler::LinkProgram(programAttrib); + ASSERT_TRUE(programResult) << programResult.error().log; + + ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()}; + auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + ASSERT_TRUE(binaryResult) << binaryResult.error().log; + ASSERT_EQ(binaryResult->size(), 1u); + const auto& inputBinary = binaryResult->front(); + + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + String inputText; + ASSERT_TRUE(tools.Disassemble(inputBinary, &inputText)); + EXPECT_NE(inputText.find("2D 0 0 0 2 Rgba16"), String::npos) << inputText; + EXPECT_NE(inputText.find("2D 0 0 0 2 R32ui"), String::npos) << inputText; + EXPECT_EQ(inputText.find("StorageImageReadWithoutFormat"), String::npos) << inputText; + EXPECT_EQ(inputText.find("StorageImageWriteWithoutFormat"), String::npos) << inputText; + + Vector outputBinary; + ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(inputBinary, outputBinary)); + + String outputText; + ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText)); + EXPECT_EQ(outputText.find("2D 0 0 0 2 Rgba16"), String::npos) << outputText; + EXPECT_NE(outputText.find("2D 0 0 0 2 Unknown"), String::npos) << outputText; + EXPECT_NE(outputText.find("2D 0 0 0 2 R32ui"), String::npos) << outputText; + + const auto countOccurrences = [](const String& text, const String& needle) { + SizeT count = 0; + for (SizeT offset = 0; (offset = text.find(needle, offset)) != String::npos; + offset += needle.size()) { + ++count; + } + return count; + }; + EXPECT_EQ(countOccurrences(outputText, "OpCapability StorageImageReadWithoutFormat"), 1u) + << outputText; + EXPECT_EQ(countOccurrences(outputText, "OpCapability StorageImageWriteWithoutFormat"), 1u) + << outputText; + EXPECT_TRUE(tools.Validate(outputBinary)); + + Vector secondOutputBinary; + ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(outputBinary, secondOutputBinary)); + EXPECT_EQ(secondOutputBinary, outputBinary); +} + +TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepFloatAtomicImageTypesTyped) { + using namespace MG_Util::ShaderTranspiler; + + const String spirvText = R"( + OpCapability Shader + OpCapability StorageImageExtendedFormats + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %target DescriptorSet 0 + OpDecorate %target Binding 0 + %void = OpTypeVoid + %float = OpTypeFloat 32 + %int = OpTypeInt 32 1 + %v2int = OpTypeVector %int 2 + %image = OpTypeImage %float 2D 0 0 0 2 R32f +%imageUniformPtr = OpTypePointer UniformConstant %image +%imageTexelPtr = OpTypePointer Image %float + %mainType = OpTypeFunction %void + %zero = OpConstant %int 0 + %coordinate = OpConstantComposite %v2int %zero %zero + %target = OpVariable %imageUniformPtr UniformConstant + %main = OpFunction %void None %mainType + %entry = OpLabel + %texelPtr = OpImageTexelPointer %imageTexelPtr %target %coordinate %zero + OpReturn + OpFunctionEnd +)"; + + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + Vector inputBinary; + ASSERT_TRUE(tools.Assemble(spirvText, &inputBinary)); + + Vector outputBinary; + ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(inputBinary, outputBinary)); + + String outputText; + ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText)); + EXPECT_NE(outputText.find("2D 0 0 0 2 R32f"), String::npos) << outputText; + EXPECT_EQ(outputText.find("StorageImageReadWithoutFormat"), String::npos) << outputText; + EXPECT_EQ(outputText.find("StorageImageWriteWithoutFormat"), String::npos) << outputText; + String validationDiagnostics; + tools.SetMessageConsumer([&validationDiagnostics](spv_message_level_t, const char*, + const spv_position_t&, const char* message) { + validationDiagnostics += message; + }); + EXPECT_TRUE(tools.Validate(outputBinary)) << validationDiagnostics; +} + TEST_F(ProgramUtilTest, PreprocessLegacyVertexShaderModernizesGlmarkStyleSource) { using namespace MG_Util::ShaderTranspiler; diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 4293ba9a..9cf420d7 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -460,6 +462,58 @@ TEST(DirectVulkanSanity, ClampsAdvertisedTextureAndDrawBufferLimitsToFrontendSta EXPECT_EQ(lowParams.MaxColorAttachments, 6); } +TEST(DirectVulkanSanity, GatesPerStageImageUniformLimitsOnPhysicalDeviceFeatures) { + using namespace MobileGL; + + MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; + MG_External::VulkanCapabilities caps; + caps.MaxImageUnits = 12; + caps.MaxCombinedImageUniforms = 10; + caps.MaxComputeImageUniforms = 9; + caps.SupportsVertexPipelineStoresAndAtomics = true; + caps.SupportsFragmentStoresAndAtomics = true; + caps.SupportsGeometryShader = false; + backend.ApplyVulkanCapabilitiesForTesting(caps); + + const auto& withoutGeometry = backend.GetDynamicParameters(); + EXPECT_EQ(withoutGeometry.MaxVertexImageUniforms, 10); + EXPECT_EQ(withoutGeometry.MaxGeometryImageUniforms, 0); + EXPECT_EQ(withoutGeometry.MaxFragmentImageUniforms, 10); + EXPECT_EQ(withoutGeometry.MaxComputeImageUniforms, 9); + + caps.SupportsGeometryShader = true; + backend.ApplyVulkanCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxGeometryImageUniforms, 10); + + caps.SupportsVertexPipelineStoresAndAtomics = false; + caps.SupportsFragmentStoresAndAtomics = false; + backend.ApplyVulkanCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxVertexImageUniforms, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxGeometryImageUniforms, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxFragmentImageUniforms, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxComputeImageUniforms, 9); +} + +TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) { + using namespace MobileGL; + + MG_Backend::DirectGLES::BackendObject_DirectGLES backend; + MG_External::GLESCapabilities caps; + caps.MaxImageUnits = 8; + caps.MaxCombinedImageUniforms = 16; + caps.MaxVertexImageUniforms = 2; + caps.MaxGeometryImageUniforms = 3; + caps.MaxFragmentImageUniforms = 4; + caps.MaxComputeImageUniforms = 5; + backend.ApplyGLESCapabilitiesForTesting(caps); + + const auto& params = backend.GetDynamicParameters(); + EXPECT_EQ(params.MaxVertexImageUniforms, 2); + EXPECT_EQ(params.MaxGeometryImageUniforms, 3); + EXPECT_EQ(params.MaxFragmentImageUniforms, 4); + EXPECT_EQ(params.MaxComputeImageUniforms, 5); +} + TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) { using namespace MobileGL; @@ -582,6 +636,54 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) { MG_State::pGLContext.reset(); } +TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) { + using namespace MobileGL; + + MG_Backend::DynamicBackendParameters params; + params.MaxImageUnits = 8; + params.MaxCombinedImageUniforms = 8; + params.MaxVertexImageUniforms = 1; + params.MaxGeometryImageUniforms = 2; + params.MaxFragmentImageUniforms = 3; + params.MaxComputeImageUniforms = 4; + MG_Backend::pActiveBackendObject = MakeUnique(params); + + GLint reported = -1; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &reported); + EXPECT_EQ(reported, 1); + MG_Impl::GLImpl::GetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &reported); + EXPECT_EQ(reported, 2); + MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &reported); + EXPECT_EQ(reported, 3); + MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &reported); + EXPECT_EQ(reported, 4); + + const String vertexImageStore = R"(#version 430 core +layout(r32ui, binding = 0) uniform uimage2D targetImages[gl_MaxVertexImageUniforms]; +void main() { + imageStore(targetImages[0], ivec2(0), uvec4(1)); + gl_Position = vec4(0.0, 0.0, 0.0, 1.0); +} +)"; + auto supported = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({ + .shaderType = GL_VERTEX_SHADER, + .sourceStr = vertexImageStore, + }); + EXPECT_TRUE(supported) << (supported ? "" : supported.error().log); + + params.MaxVertexImageUniforms = 0; + MG_Backend::pActiveBackendObject = MakeUnique(params); + MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &reported); + EXPECT_EQ(reported, 0); + auto unsupported = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({ + .shaderType = GL_VERTEX_SHADER, + .sourceStr = vertexImageStore, + }); + EXPECT_FALSE(unsupported); + + MG_Backend::pActiveBackendObject.reset(); +} + TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) { using namespace MobileGL; @@ -675,6 +777,175 @@ TEST(DirectVulkanSanity, SampledDepthStencilViewUsesSingleDepthAspect) { VK_IMAGE_ASPECT_DEPTH_BIT); } +TEST(DirectVulkanSanity, SpirvStorageImageFormatsMapToVulkanFormats) { + using MobileGL::MG_Backend::DirectVulkan::ProgramFactory; + + struct FormatCase { + SpvImageFormat spirv; + VkFormat vulkan; + }; + const FormatCase cases[] = { + {SpvImageFormatUnknown, VK_FORMAT_UNDEFINED}, + {SpvImageFormatRgba32f, VK_FORMAT_R32G32B32A32_SFLOAT}, + {SpvImageFormatRgba16f, VK_FORMAT_R16G16B16A16_SFLOAT}, + {SpvImageFormatR32f, VK_FORMAT_R32_SFLOAT}, + {SpvImageFormatRgba8, VK_FORMAT_R8G8B8A8_UNORM}, + {SpvImageFormatRgba8Snorm, VK_FORMAT_R8G8B8A8_SNORM}, + {SpvImageFormatRg32f, VK_FORMAT_R32G32_SFLOAT}, + {SpvImageFormatRg16f, VK_FORMAT_R16G16_SFLOAT}, + {SpvImageFormatR11fG11fB10f, VK_FORMAT_B10G11R11_UFLOAT_PACK32}, + {SpvImageFormatR16f, VK_FORMAT_R16_SFLOAT}, + {SpvImageFormatRgba16, VK_FORMAT_R16G16B16A16_UNORM}, + {SpvImageFormatRgb10A2, VK_FORMAT_A2R10G10B10_UNORM_PACK32}, + {SpvImageFormatRg16, VK_FORMAT_R16G16_UNORM}, + {SpvImageFormatRg8, VK_FORMAT_R8G8_UNORM}, + {SpvImageFormatR16, VK_FORMAT_R16_UNORM}, + {SpvImageFormatR8, VK_FORMAT_R8_UNORM}, + {SpvImageFormatRgba16Snorm, VK_FORMAT_R16G16B16A16_SNORM}, + {SpvImageFormatRg16Snorm, VK_FORMAT_R16G16_SNORM}, + {SpvImageFormatRg8Snorm, VK_FORMAT_R8G8_SNORM}, + {SpvImageFormatR16Snorm, VK_FORMAT_R16_SNORM}, + {SpvImageFormatR8Snorm, VK_FORMAT_R8_SNORM}, + {SpvImageFormatRgba32i, VK_FORMAT_R32G32B32A32_SINT}, + {SpvImageFormatRgba16i, VK_FORMAT_R16G16B16A16_SINT}, + {SpvImageFormatRgba8i, VK_FORMAT_R8G8B8A8_SINT}, + {SpvImageFormatR32i, VK_FORMAT_R32_SINT}, + {SpvImageFormatRg32i, VK_FORMAT_R32G32_SINT}, + {SpvImageFormatRg16i, VK_FORMAT_R16G16_SINT}, + {SpvImageFormatRg8i, VK_FORMAT_R8G8_SINT}, + {SpvImageFormatR16i, VK_FORMAT_R16_SINT}, + {SpvImageFormatR8i, VK_FORMAT_R8_SINT}, + {SpvImageFormatRgba32ui, VK_FORMAT_R32G32B32A32_UINT}, + {SpvImageFormatRgba16ui, VK_FORMAT_R16G16B16A16_UINT}, + {SpvImageFormatRgba8ui, VK_FORMAT_R8G8B8A8_UINT}, + {SpvImageFormatR32ui, VK_FORMAT_R32_UINT}, + {SpvImageFormatRgb10a2ui, VK_FORMAT_A2R10G10B10_UINT_PACK32}, + {SpvImageFormatRg32ui, VK_FORMAT_R32G32_UINT}, + {SpvImageFormatRg16ui, VK_FORMAT_R16G16_UINT}, + {SpvImageFormatRg8ui, VK_FORMAT_R8G8_UINT}, + {SpvImageFormatR16ui, VK_FORMAT_R16_UINT}, + {SpvImageFormatR8ui, VK_FORMAT_R8_UINT}, + {SpvImageFormatR64ui, VK_FORMAT_R64_UINT}, + {SpvImageFormatR64i, VK_FORMAT_R64_SINT}, + }; + + for (const auto& testCase : cases) { + EXPECT_EQ(ProgramFactory::ConvertSpirvImageFormatToVkFormat(testCase.spirv), testCase.vulkan) + << "SpvImageFormat=" << static_cast(testCase.spirv); + } +} + +TEST(DirectVulkanSanity, MutableStorageImageViewsUseVulkanCompatibilityClasses) { + using MobileGL::MG_Backend::DirectVulkan::VkTextureManager; + + EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible( + VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT)); + EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible( + VK_FORMAT_R32_UINT, VK_FORMAT_R32_SINT)); + EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible( + VK_FORMAT_R16G16B16A16_UNORM, VK_FORMAT_R16G16B16A16_SFLOAT)); + EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible( + VK_FORMAT_R32_SFLOAT, VK_FORMAT_R8G8B8A8_UINT)); + EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible( + VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_SFLOAT)); + EXPECT_FALSE(VkTextureManager::AreStorageImageViewFormatsCompatible( + VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT)); + EXPECT_FALSE(VkTextureManager::AreStorageImageViewFormatsCompatible( + VK_FORMAT_R32_SFLOAT, VK_FORMAT_D32_SFLOAT)); +} + +TEST(DirectVulkanSanity, StorageImageViewFormatUsesBindingOnlyForFormatlessFloatPolicy) { + using MobileGL::MG_Backend::DirectVulkan::UniformManager; + + EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat( + VK_FORMAT_UNDEFINED, GL_RGBA16F, VK_FORMAT_R16G16B16A16_UNORM, true), + VK_FORMAT_R16G16B16A16_SFLOAT); + EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat( + VK_FORMAT_UNDEFINED, GL_RGBA16, VK_FORMAT_R16G16B16A16_SFLOAT, true), + VK_FORMAT_R16G16B16A16_UNORM); + EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat( + VK_FORMAT_R32_UINT, GL_RGBA16F, VK_FORMAT_R32_SFLOAT, false), + VK_FORMAT_R32_UINT); + EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat( + VK_FORMAT_UNDEFINED, GL_RGBA16F, VK_FORMAT_R32_SFLOAT, false), + VK_FORMAT_R32_SFLOAT); + EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat( + VK_FORMAT_UNDEFINED, GL_NONE, VK_FORMAT_R16G16B16A16_SFLOAT, true), + VK_FORMAT_UNDEFINED); +} + +TEST(DirectVulkanSanity, ProgramObjectMovePreservesStorageImageFormatPolicy) { + using MobileGL::MG_Backend::DirectVulkan::ProgramFactory; + + ProgramFactory::VkProgramObject source; + source.storageImageFormatByBinding = {VK_FORMAT_UNDEFINED, VK_FORMAT_R32_UINT}; + source.storageImageUsesBindingFormatByBinding = {true, false}; + + ProgramFactory::VkProgramObject moved(std::move(source)); + ASSERT_EQ(moved.storageImageFormatByBinding.size(), 2u); + ASSERT_EQ(moved.storageImageUsesBindingFormatByBinding.size(), 2u); + EXPECT_EQ(moved.storageImageFormatByBinding[0], VK_FORMAT_UNDEFINED); + EXPECT_EQ(moved.storageImageFormatByBinding[1], VK_FORMAT_R32_UINT); + EXPECT_TRUE(moved.storageImageUsesBindingFormatByBinding[0]); + EXPECT_FALSE(moved.storageImageUsesBindingFormatByBinding[1]); + + ProgramFactory::VkProgramObject assigned; + assigned = std::move(moved); + ASSERT_EQ(assigned.storageImageFormatByBinding.size(), 2u); + ASSERT_EQ(assigned.storageImageUsesBindingFormatByBinding.size(), 2u); + EXPECT_TRUE(assigned.storageImageUsesBindingFormatByBinding[0]); + EXPECT_FALSE(assigned.storageImageUsesBindingFormatByBinding[1]); +} + +TEST(DirectVulkanSanity, SamplerUniformTypesPreserveTheirNumericDomain) { + using namespace MobileGL::MG_Backend::DirectVulkan; + + EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_SAMPLER_2D), + SamplerNumericDomain::Float); + EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW), + SamplerNumericDomain::Float); + EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_INT_SAMPLER_2D_ARRAY), + SamplerNumericDomain::SignedInteger); + EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_UNSIGNED_INT_SAMPLER_2D), + SamplerNumericDomain::UnsignedInteger); + EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_IMAGE_2D), + SamplerNumericDomain::Unknown); +} + +TEST(DirectVulkanSanity, SampledViewFormatMatchesSamplerNumericDomainWithoutChangingComponentLayout) { + using namespace MobileGL::MG_Backend::DirectVulkan; + + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat( + VK_FORMAT_R32_SFLOAT, SamplerNumericDomain::UnsignedInteger), + VK_FORMAT_R32_UINT); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat( + VK_FORMAT_R32_SFLOAT, SamplerNumericDomain::SignedInteger), + VK_FORMAT_R32_SINT); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat( + VK_FORMAT_R32_UINT, SamplerNumericDomain::Float), + VK_FORMAT_R32_SFLOAT); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat( + VK_FORMAT_R16G16B16A16_SFLOAT, SamplerNumericDomain::UnsignedInteger), + VK_FORMAT_R16G16B16A16_UINT); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat( + VK_FORMAT_R8G8B8A8_UNORM, SamplerNumericDomain::UnsignedInteger), + VK_FORMAT_R8G8B8A8_UINT); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat( + VK_FORMAT_R32_UINT, SamplerNumericDomain::UnsignedInteger), + VK_FORMAT_R32_UINT); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat( + VK_FORMAT_B10G11R11_UFLOAT_PACK32, SamplerNumericDomain::UnsignedInteger), + VK_FORMAT_UNDEFINED); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat( + VK_FORMAT_D32_SFLOAT, SamplerNumericDomain::UnsignedInteger), + VK_FORMAT_UNDEFINED); + + EXPECT_TRUE(VkTextureManager::AreSampledImageViewFormatsCompatible( + VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT)); + EXPECT_FALSE(VkTextureManager::AreSampledImageViewFormatsCompatible( + VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16G16B16A16_UINT)); +} + TEST(RenderStateSanity, ProvokingVertexUpdatesStateAndValidatesEnum) { using namespace MobileGL; diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 6d147e3b..e2f32287 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -158,6 +158,39 @@ namespace { auto* mipmapObject = static_cast(textureObject.get()); return static_cast(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, level)); } + + class ScopedTextureBackendFunctionsOverride { + public: + ScopedTextureBackendFunctionsOverride(): m_snapshot(MG_Backend::gBackendFunctionsTable) {} + ~ScopedTextureBackendFunctionsOverride() { MG_Backend::gBackendFunctionsTable = m_snapshot; } + + private: + MG_Backend::GlobalBackendFunctionsTable m_snapshot; + }; + + struct CopyTexSubImage2DCall { + Bool Called = false; + GLenum Target = GL_NONE; + GLint Level = -1; + GLint XOffset = -1; + GLint YOffset = -1; + GLint X = -1; + GLint Y = -1; + GLsizei Width = -1; + GLsizei Height = -1; + GLuint BoundTexture = 0; + } g_copyTexSubImage2DCall; + + void RecordCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, + GLsizei width, GLsizei height) { + g_copyTexSubImage2DCall = { + true, target, level, xoffset, yoffset, x, y, width, height, + MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject() + ->GetExternalIndex(), + }; + } } // namespace TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { @@ -178,6 +211,113 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(TextureTest, ClearTexImageNullClearsWholeNamedTextureAndMarksStorageDirty) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 initialPixels[] = { + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16, + }; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, + GL_RGBA, GL_UNSIGNED_BYTE, initialPixels); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + auto* mipmapObject = static_cast(textureObject.get()); + mipmapObject->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, false); + + MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + const Uint8* stored = GetBoundTexture2DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + const Uint8 zeros[sizeof(initialPixels)] = {}; + EXPECT_EQ(std::memcmp(stored, zeros, sizeof(zeros)), 0); + EXPECT_TRUE(mipmapObject->IsStorageDirty(TextureUploadTarget::Texture2D, 0)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, ClearTexImageRepeatsConvertedClearPixel) { + 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, 2, 0, + GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + const Uint8 clearPixel[] = {17, 34, 51, 68}; + MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, clearPixel); + + const Uint8* stored = GetBoundTexture2DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + const Uint8 expected[] = { + 17, 34, 51, 68, + 17, 34, 51, 68, + 17, 34, 51, 68, + 17, 34, 51, 68, + }; + EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, ClearTexSubImageClearsOnlyRequestedRectangle) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + Uint8 initialPixels[3 * 2 * 4]; + std::memset(initialPixels, 0x7f, sizeof(initialPixels)); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 3, 2, 0, + GL_RGBA, GL_UNSIGNED_BYTE, initialPixels); + + MG_Impl::GLImpl::ClearTexSubImage(texture, 0, 1, 0, 0, 1, 2, 1, + GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + const Uint8* stored = GetBoundTexture2DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + for (Int y = 0; y < 2; ++y) { + for (Int x = 0; x < 3; ++x) { + for (Int channel = 0; channel < 4; ++channel) { + EXPECT_EQ(stored[(y * 3 + x) * 4 + channel], x == 1 ? 0 : 0x7f); + } + } + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D = RecordCopyTexSubImage2D; + g_copyTexSubImage2DCall = {}; + + GLuint namedTexture = 0; + GLuint boundTexture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &namedTexture); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &boundTexture); + MG_Impl::GLImpl::BindTextureUnit(0, boundTexture); + + const auto boundBefore = MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(); + MG_Impl::GLImpl::CopyTextureSubImage2D(namedTexture, 2, 3, 4, 5, 6, 7, 8); + + EXPECT_TRUE(g_copyTexSubImage2DCall.Called); + EXPECT_EQ(g_copyTexSubImage2DCall.Target, GL_TEXTURE_2D); + EXPECT_EQ(g_copyTexSubImage2DCall.Level, 2); + EXPECT_EQ(g_copyTexSubImage2DCall.XOffset, 3); + EXPECT_EQ(g_copyTexSubImage2DCall.YOffset, 4); + EXPECT_EQ(g_copyTexSubImage2DCall.X, 5); + EXPECT_EQ(g_copyTexSubImage2DCall.Y, 6); + EXPECT_EQ(g_copyTexSubImage2DCall.Width, 7); + EXPECT_EQ(g_copyTexSubImage2DCall.Height, 8); + EXPECT_EQ(g_copyTexSubImage2DCall.BoundTexture, namedTexture); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + boundBefore); + 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) { diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 87caa900..606769c1 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -865,6 +865,9 @@ namespace MobileGL::MG_Util::BackendLoader { GLint maxUniformBlockSize = 16384; GLint maxImageUnits = 8; GLint maxCombinedImageUniforms = 8; + GLint maxVertexImageUniforms = 0; + GLint maxGeometryImageUniforms = 0; + GLint maxFragmentImageUniforms = 8; GLint maxComputeImageUniforms = 8; GLint maxDrawBuffers = 8; GLint maxColorAttachments = 8; @@ -904,7 +907,16 @@ namespace MobileGL::MG_Util::BackendLoader { glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize); glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits); glesFuncs.glGetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImageUniforms); + glesFuncs.glGetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &maxVertexImageUniforms); + glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms); glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms); + // Geometry shaders and their image-uniform query are core only in ES 3.2. DirectGLES + // emits ESSL 3.10 on an ES 3.1 context, so reporting zero there is both legal and an + // accurate description of what the backend compiler can consume. + if (caps.GLESVersion.Major > 3 || + (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) { + glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms); + } glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments); glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances); @@ -955,6 +967,9 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxUniformBlockSize = maxUniformBlockSize; caps.MaxImageUnits = maxImageUnits; caps.MaxCombinedImageUniforms = maxCombinedImageUniforms; + caps.MaxVertexImageUniforms = maxVertexImageUniforms; + caps.MaxGeometryImageUniforms = maxGeometryImageUniforms; + caps.MaxFragmentImageUniforms = maxFragmentImageUniforms; caps.MaxComputeImageUniforms = maxComputeImageUniforms; caps.MaxDrawBuffers = maxDrawBuffers; caps.MaxColorAttachments = maxColorAttachments; @@ -1000,6 +1015,9 @@ namespace MobileGL::MG_Util::BackendLoader { MGLOG_I(" GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize); MGLOG_I(" GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits); MGLOG_I(" GL_MAX_COMBINED_IMAGE_UNIFORMS: %d", caps.MaxCombinedImageUniforms); + MGLOG_I(" GL_MAX_VERTEX_IMAGE_UNIFORMS: %d", caps.MaxVertexImageUniforms); + MGLOG_I(" GL_MAX_GEOMETRY_IMAGE_UNIFORMS: %d", caps.MaxGeometryImageUniforms); + MGLOG_I(" GL_MAX_FRAGMENT_IMAGE_UNIFORMS: %d", caps.MaxFragmentImageUniforms); MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms); MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers); MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments); diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index c87066e3..e9092dfb 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1102,6 +1102,9 @@ namespace MobileGL { Int MaxUniformBlockSize = 16384; Int MaxImageUnits = 8; Int MaxCombinedImageUniforms = 8; + Int MaxVertexImageUniforms = 0; + Int MaxGeometryImageUniforms = 0; + Int MaxFragmentImageUniforms = 8; Int MaxComputeImageUniforms = 8; Int MaxDrawBuffers = 8; Int MaxColorAttachments = 8; diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp index 3105abb6..a0e5e667 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp @@ -174,6 +174,10 @@ namespace MobileGL::MG_Util::BackendLoader { VkPhysicalDeviceFeatures supportedFeatures{}; vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures); caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE; + caps.SupportsVertexPipelineStoresAndAtomics = + supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE; + caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE; + caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE; caps.MaxShaderStorageBlockSize = static_cast(p.limits.maxStorageBufferRange); const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 && HasUsableShaderSubgroupSupport(subgroupProps); @@ -256,6 +260,11 @@ namespace MobileGL::MG_Util::BackendLoader { caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1]; caps.ViewportSubpixelBits = static_cast(properties.limits.viewportSubPixelBits); caps.SupportsWideLines = false; + // This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional + // stage writes disabled rather than inferring them from descriptor limits alone. + caps.SupportsVertexPipelineStoresAndAtomics = false; + caps.SupportsFragmentStoresAndAtomics = false; + caps.SupportsGeometryShader = false; caps.MaxShaderStorageBlockSize = static_cast(properties.limits.maxStorageBufferRange); caps.SupportsShaderSubgroup = false; caps.SubgroupSize = 0; diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h index 887d8541..eec0dcc3 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h @@ -67,6 +67,12 @@ namespace MobileGL { Float ViewportBoundsRangeMax = 0.0f; Int ViewportSubpixelBits = 0; Bool SupportsWideLines = false; + // Storage-image descriptors are limited per stage by + // maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally + // require these core Vulkan features to be enabled on the logical device. + Bool SupportsVertexPipelineStoresAndAtomics = false; + Bool SupportsFragmentStoresAndAtomics = false; + Bool SupportsGeometryShader = false; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; Bool SupportsShaderSubgroup = false; Uint32 SubgroupSize = 0; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index fe9fa2c0..66538393 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -6,6 +6,10 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header +#define SPV_ENABLE_UTILITY_CODE +#include "glslang/SPIRV/spirv.hpp11" +#undef SPV_ENABLE_UTILITY_CODE + #include "ShaderCompiler.h" #include "SpirvPasses/EliminateFloatEqualsZeroPass.h" @@ -19,6 +23,7 @@ #include "spirv-tools/optimizer.hpp" #include "ShaderSourceProcessor.h" +#include #include #include #include @@ -26,8 +31,8 @@ namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { - TBuiltInResource& GetTBuiltInResourceInstance() { - static TBuiltInResource Resources{}; + TBuiltInResource BuildTBuiltInResource() { + TBuiltInResource Resources{}; Resources.maxLights = 32; Resources.maxClipPlanes = 6; Resources.maxTextureUnits = 32; @@ -122,6 +127,22 @@ namespace MobileGL { Resources.maxTaskWorkGroupSizeZ_NV = 1; Resources.maxMeshViewCountNV = 4; + // Resource checking must describe the same backend contract exposed through + // glGetIntegerv. Keeping this copy local also avoids racing on a process-global + // TBuiltInResource when Iris compiles shaders concurrently. + const MG_Backend::DynamicBackendParameters fallbackParameters{}; + const auto& activeBackend = MG_Backend::pActiveBackendObject; + const auto& dynamicParameters = + activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters; + Resources.maxImageUnits = dynamicParameters.MaxImageUnits; + Resources.maxCombinedImageUnitsAndFragmentOutputs = + dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers; + Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms; + Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms; + Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms; + Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms; + Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms; + Resources.limits.nonInductiveForLoops = true; Resources.limits.whileLoops = true; Resources.limits.doWhileLoops = true; @@ -167,7 +188,8 @@ namespace MobileGL { tshader->setAutoMapLocations(true); tshader->setAutoMapBindings(true); tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME); - if (!tshader->parse(&GetTBuiltInResourceInstance(), 460, ECoreProfile, + auto resources = BuildTBuiltInResource(); + if (!tshader->parse(&resources, 460, ECoreProfile, /*forceDefaultVersionAndProfile: */ false, /*forwardCompatible: */ true, EShMsgDefault)) { ResultInfo r; @@ -392,6 +414,136 @@ namespace MobileGL { return true; } + bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan( + const Vector& inputBinary, Vector& outputBinary) { + constexpr SizeT kSpirvHeaderWordCount = 5; + outputBinary.clear(); + if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) { + return false; + } + + Vector floatTypeIds; + Vector resultTypeById(inputBinary[3], 0); + Vector pointerPointeeTypeById(inputBinary[3], 0); + Bool hasReadWithoutFormatCapability = false; + Bool hasWriteWithoutFormatCapability = false; + SizeT capabilityInsertOffset = kSpirvHeaderWordCount; + + for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) { + const Uint32 instructionWord = inputBinary[offset]; + const Uint32 wordCount = instructionWord >> 16u; + const auto opcode = static_cast(instructionWord & 0xffffu); + if (wordCount == 0 || offset + wordCount > inputBinary.size()) { + return false; + } + + if (opcode == spv::Op::OpCapability && wordCount >= 2) { + capabilityInsertOffset = offset + wordCount; + const auto capability = static_cast(inputBinary[offset + 1]); + hasReadWithoutFormatCapability |= + capability == spv::Capability::StorageImageReadWithoutFormat; + hasWriteWithoutFormatCapability |= + capability == spv::Capability::StorageImageWriteWithoutFormat; + } else if (opcode == spv::Op::OpTypeFloat && wordCount >= 3) { + floatTypeIds.push_back(inputBinary[offset + 1]); + } else if (opcode == spv::Op::OpTypePointer && wordCount >= 4) { + const Uint32 pointerTypeId = inputBinary[offset + 1]; + if (pointerTypeId >= pointerPointeeTypeById.size()) { + return false; + } + pointerPointeeTypeById[pointerTypeId] = inputBinary[offset + 3]; + } + + bool hasResult = false; + bool hasResultType = false; + spv::HasResultAndType(opcode, &hasResult, &hasResultType); + if (hasResult && hasResultType && wordCount >= 3) { + const Uint32 resultTypeId = inputBinary[offset + 1]; + const Uint32 resultId = inputBinary[offset + 2]; + if (resultId >= resultTypeById.size()) { + return false; + } + resultTypeById[resultId] = resultTypeId; + } + offset += wordCount; + } + + // OpImageTexelPointer is the bridge to image atomic instructions. Vulkan requires + // those image types to retain an atomic-compatible declared format, so exclude only + // the exact image types used by an atomic path rather than disabling formatless + // access for unrelated float images in the same module. + Vector atomicImageTypeIds; + for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) { + const Uint32 instructionWord = inputBinary[offset]; + const Uint32 wordCount = instructionWord >> 16u; + const auto opcode = static_cast(instructionWord & 0xffffu); + if (opcode == spv::Op::OpImageTexelPointer && wordCount >= 6) { + const Uint32 imageId = inputBinary[offset + 3]; + if (imageId >= resultTypeById.size()) { + return false; + } + Uint32 imageTypeId = resultTypeById[imageId]; + if (imageTypeId < pointerPointeeTypeById.size() && + pointerPointeeTypeById[imageTypeId] != 0) { + imageTypeId = pointerPointeeTypeById[imageTypeId]; + } + if (imageTypeId != 0 && + std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) == + atomicImageTypeIds.end()) { + atomicImageTypeIds.push_back(imageTypeId); + } + } + offset += wordCount; + } + + outputBinary = inputBinary; + Bool hasFloatStorageImage = false; + for (SizeT offset = kSpirvHeaderWordCount; offset < outputBinary.size();) { + const Uint32 instructionWord = outputBinary[offset]; + const Uint32 wordCount = instructionWord >> 16u; + const auto opcode = static_cast(instructionWord & 0xffffu); + + // OpTypeImage operands are: result id, sampled type, dim, depth, arrayed, + // multisampled, sampled, image format, and an optional access qualifier. + if (opcode == spv::Op::OpTypeImage && wordCount >= 9) { + const Uint32 imageTypeId = outputBinary[offset + 1]; + const Uint32 sampledTypeId = outputBinary[offset + 2]; + const Uint32 sampled = outputBinary[offset + 7]; + const Bool hasFloatSampledType = + std::find(floatTypeIds.begin(), floatTypeIds.end(), sampledTypeId) != floatTypeIds.end(); + const Bool usedByAtomic = + std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) != + atomicImageTypeIds.end(); + if (sampled == 2 && hasFloatSampledType && !usedByAtomic) { + outputBinary[offset + 8] = static_cast(spv::ImageFormat::Unknown); + hasFloatStorageImage = true; + } + } + offset += wordCount; + } + + if (!hasFloatStorageImage) { + return true; + } + + Vector addedCapabilities; + const Uint32 capabilityInstruction = + (2u << 16u) | static_cast(spv::Op::OpCapability); + if (!hasReadWithoutFormatCapability) { + addedCapabilities.push_back(capabilityInstruction); + addedCapabilities.push_back( + static_cast(spv::Capability::StorageImageReadWithoutFormat)); + } + if (!hasWriteWithoutFormatCapability) { + addedCapabilities.push_back(capabilityInstruction); + addedCapabilities.push_back( + static_cast(spv::Capability::StorageImageWriteWithoutFormat)); + } + outputBinary.insert(outputBinary.begin() + static_cast(capabilityInsertOffset), + addedCapabilities.begin(), addedCapabilities.end()); + return true; + } + Result ShaderCompiler::DecompileShader(SpvcSession& session) { spvc_compiler_options options; session.CreateOptions(&options); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index d9dc1542..87065fa3 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -46,6 +46,14 @@ namespace MobileGL { // decoration. DirectVulkan only. static bool DecoratePositionInvariantForVulkan(const Vector& inputBinary, Vector& outputBinary); + // Replaces the declared format of float storage images with Unknown and adds the + // matching SPIR-V capabilities. DirectVulkan uses this only when both Vulkan + // shaderStorageImage*WithoutFormat features are enabled, allowing the + // glBindImageTexture format to select the descriptor view at runtime. Integer + // storage images deliberately keep their declared format for GL-compatible bit + // reinterpretation paths (for example, R32F storage accessed as r32ui). + static bool UseUnformattedFloatStorageImagesForVulkan( + const Vector& inputBinary, Vector& outputBinary); static Result DecompileShader(SpvcSession& session); }; } // namespace ShaderTranspiler diff --git a/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp b/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp index c891106f..f78e207a 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp @@ -63,7 +63,17 @@ namespace MobileGL { void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) { const glslang::TType& type = ent.symbol->getType(); const glslang::TString& name = ent.symbol->getAccessName(); - if (currentStage == EShLangVertex && type.getQualifier().isPipeInput()) { + // OpenGL assigns generic vertex attribute locations only to active inputs. glslang gathers + // both live and dead declarations before mapping, so allowing the default collector to + // reserve a dead vertex input would make it consume a location that an active input should + // reuse. Other stage interfaces still need the default cross-stage matching behavior. + if (!ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) { + return; + } + // glBindAttribLocation only affects active inputs in the linked program. Applying an API + // binding to an inactive declaration would reserve its slot in glslang's collector and + // incorrectly push an active, automatically mapped input to a different location. + if (ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) { auto it = m_explicitVertexIns.find(name.c_str()); if (it != m_explicitVertexIns.end()) { auto& writableType = ent.symbol->getWritableType(); @@ -94,6 +104,13 @@ namespace MobileGL { TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink); } + int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) { + if (!ent.live && stage == EShLangVertex && ent.symbol->getType().getQualifier().isPipeInput()) { + return ent.newLocation = -1; + } + return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent); + } + void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) { const glslang::TType& type = ent.symbol->getType(); if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler && diff --git a/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h b/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h index bed30d1b..86e9068a 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h +++ b/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h @@ -37,6 +37,7 @@ namespace MobileGL { opaqueUniformBindings) {} void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override; void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override; + int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override; int resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override; protected: