diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e1e7ea4..d8b56dc6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 8eac285f..93585cc9 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -359,6 +359,13 @@ namespace MobileGL { // array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false // so a backend that never sets it gets the conservative answer. Bool SupportsPerLayerFramebufferAttachment = false; + // Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e. + // whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected, + // never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the + // attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the + // bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at + // all. Defaults to false so a backend that never sets it gets the conservative answer. + Bool SupportsFloat64VertexAttributes = false; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; Uint32 SubgroupSize = 0; Uint32 SubgroupSupportedStages = 0; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 34a2ecf6..5b203f5b 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1114,6 +1114,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // given - provided the driver resolved the entry point at all. m_dynamicParameters.SupportsPerLayerFramebufferAttachment = DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr; + // Not a driver question and never will be: OpenGL ES has no double-precision vertex format + // and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to + // land on this backend regardless of what the driver underneath happens to support. + m_dynamicParameters.SupportsFloat64VertexAttributes = false; 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/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 125c1b58..7feae4f3 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1305,6 +1305,22 @@ namespace MobileGL::MG_Backend::DirectGLES { m_syncedAttributeVersions[attribIndex].BufferVersion; if (!needsSyncFormat && !needsSyncBuffer) continue; + // Defence in depth. The frontend already declines glVertexAttribLFormat on this + // backend (SupportsFloat64VertexAttributes is false - ES has no GL_DOUBLE vertex + // format and ESSL has no fp64 type), so IsLong should never arrive here; if it ever + // did, passing GL_DOUBLE to glVertexAttribPointer would only raise GL_INVALID_ENUM on + // the real driver. Disabling rather than merely skipping matters: becoming long bumps + // FormatVersion, not SwitchVersion, so the enable/disable block above will not run + // again and an already-enabled array would stay enabled with no pointer and no + // ARRAY_BUFFER binding - which ES 3.1+ makes an INVALID_OPERATION at draw. + if (attrib.IsLong) { + MGLOG_E("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this " + "backend cannot feed - disabling the array", + attribIndex); + g_GLESFuncs.glDisableVertexAttribArray(attribIndex); + continue; + } + if (!BindAttributeBuffer(attrib)) { continue; } @@ -1366,6 +1382,13 @@ namespace MobileGL::MG_Backend::DirectGLES { continue; } + // Same reason as SyncToBackend: there is no ES vertex format for a 64-bit array, and + // this path only ever reaches glVertexAttribPointer/IPointer. + if (attrib.IsLong) { + g_GLESFuncs.glDisableVertexAttribArray(attribIndex); + continue; + } + const auto* clientData = reinterpret_cast(attrib.Offset); const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra); if (!clientData || elementSize == 0 || attrib.Size <= 0) { diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 6f40d3b4..b5e55321 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -810,6 +810,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines; + m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64; m_dynamicParameters.MaxShaderStorageBlockSize = std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize); if (m_vulkanCaps.SupportsShaderSubgroup) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 39c55858..dce2c9b8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -2458,6 +2458,33 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + // A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is + // optional and lavapipe advertises none of them at all. The pass is unconditional so it + // always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and + // ReflectVertexInputs below then sees an ordinary uvec2/uvec4 input. + // + // Failure here is not recoverable and must not be swallowed: ToVkVertexFormat has already + // committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring + // `in double` would reconcile to Unknown and build a pipeline with a UINT format under a + // double input - garbage with no diagnostic anywhere. + if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) { + Vector packedSpirv; + const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan( + moduleSpirvs[i], packedSpirv); + MOBILEGL_ASSERT(packOk, + "ProgramFactory: 64-bit vertex input packing failed for program %u; the " + "vertex-input format and the shader input type now disagree", + program.GetExternalIndex()); + if (packOk) { + moduleSpirvs[i] = std::move(packedSpirv); + } else { + MGLOG_E("ProgramFactory: failed to pack 64-bit vertex inputs for program %u; " + "double-typed vertex attributes will be fetched as uint32 words and not " + "reinterpreted", + program.GetExternalIndex()); + } + } + // 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index b2a9ba35..921be6e5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -29,6 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride))); XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset))); XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger))); + XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong))); XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra))); XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor))); @@ -97,7 +98,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } const VkFormat sourceVkFormat = - ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra); + ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong); 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", @@ -273,7 +274,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, - Bool isBgra) { + Bool isBgra, Bool isLong) { if (isBgra) { // GL_BGRA: four reversed-order components, always normalized (enforced at validation), only // legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the @@ -298,6 +299,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { case DataType::Int2101010Rev: if (isInteger || size != 4) return VK_FORMAT_UNDEFINED; return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32; + case DataType::Float64: + // A 64-bit attribute is fetched as its 32-bit word pair and bitcast back to double in the + // shader (PackDoubleVertexInputsPass does the shader half). That is bit-exact and, unlike + // VK_FORMAT_R64*_SFLOAT, needs no format capability: lavapipe reports bufferFeatures = 0 + // for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there + // while shaderFloat64 is not. Both halves key off nothing but the attribute being long, + // so they always agree without extra plumbing. + if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED; + switch (size) { + case 1: return VK_FORMAT_R32G32_UINT; + case 2: return VK_FORMAT_R32G32B32A32_UINT; + // A dvec3/dvec4 input is 6/8 uint32 components: no single VkFormat, and GL spreads it + // over two attribute locations, which the location-per-VAO-index model here does not + // express. Declined rather than fetched wrong. + default: return VK_FORMAT_UNDEFINED; + } case DataType::Float32: switch (size) { case 1: return VK_FORMAT_R32_SFLOAT; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h index 8714019b..1626f29b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h @@ -93,7 +93,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra); private: - static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false); + static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false, + Bool isLong = false); static Bool IsScaledIntegerVertexFormat(VkFormat format); static VkFormat ToFloat32VertexFormat(Int componentCount); Bool SupportsVertexBufferFormat(VkFormat format) const; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index ac5042b3..80baa217 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -9925,6 +9925,12 @@ void main() { deviceFeatures.wideLines = supportedDeviceFeatures.wideLines; m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE; deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64; + // Required for any module that declares OpCapability Float64 - which is every shader with a + // double in it, including the 64-bit vertex attribute path (the attribute itself arrives as + // uint32 words, but the bitcast result and everything computed from it is Float64). Without + // it vkCreateShaderModule is invalid usage (VUID-VkShaderModuleCreateInfo-pCode-08740), + // which is why SupportsFloat64VertexAttributes gates the entry point on the same feature. + deviceFeatures.shaderFloat64 = supportedDeviceFeatures.shaderFloat64; // 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. diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp index 17b4b637..ca011da3 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp @@ -8,6 +8,7 @@ #include "GL_VertexArray.h" #include "Validators.h" +#include #include #include #include @@ -173,6 +174,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_CURRENT_VERTEX_ATTRIB: case GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING: case GL_VERTEX_ATTRIB_ARRAY_INTEGER: + // Core since GL 4.1 (ARB_vertex_attrib_64bit). It was rejected while no attribute could + // ever be long; now that IsLong is real state the pname has to be accepted. + case GL_VERTEX_ATTRIB_ARRAY_LONG: case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: case GL_VERTEX_ATTRIB_ARRAY_POINTER: return true; @@ -460,19 +464,38 @@ namespace MobileGL::MG_Impl::GLImpl { relativeoffset, isBgra); } - // The long (64-bit) attribute format. MobileGL has no 64-bit vertex attributes, so nothing is - // recorded; what the entry point owes the application is the parameter validation, which is - // observable through glGetError regardless of whether the format could be used in a draw. - static void VertexAttribLFormatSeparate_State(GLuint attribindex, GLint size, GLenum type, + // The long (64-bit) attribute format: the values reach the shader as doubles, unconverted + // (GL 4.6 core 10.3.2). ValidateVertexAttribLFormat has already pinned type to GL_DOUBLE, so the + // recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form, + // as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion. + // + // Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64, + // and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error + // plus a log line naming the reason - rather than accepting state no draw could honour and + // rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp. + static void VertexAttribLFormatSeparate_State(const SharedPtr& vao, + GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) { if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return; if (!VertexArrayImpl::ValidateVertexAttribLFormat(attribindex, size, type)) return; if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return; - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "VertexAttribLFormat", - "64-bit vertex attributes are not supported.")); + if (!MG_Backend::pActiveBackendObject || + !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) { + MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this " + "backend has no double-precision vertex attribute support - see the " + "\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs", + attribindex); + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "VertexAttribLFormat", + "64-bit vertex attributes are not supported by this backend.")); + return; + } + + vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type), + /*normalized: */ false, /*isInteger: */ false, relativeoffset, + /*isBgra: */ false, /*isLong: */ true); } void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, @@ -915,6 +938,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_VERTEX_ATTRIB_ARRAY_INTEGER: params[0] = attr->IsInteger ? 1.0f : 0.0f; return; + case GL_VERTEX_ATTRIB_ARRAY_LONG: + params[0] = attr->IsLong ? 1.0f : 0.0f; + return; case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: params[0] = static_cast(attr->Divisor); return; @@ -975,6 +1001,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_VERTEX_ATTRIB_ARRAY_INTEGER: params[0] = attr->IsInteger ? 1.0 : 0.0; return; + case GL_VERTEX_ATTRIB_ARRAY_LONG: + params[0] = attr->IsLong ? 1.0 : 0.0; + return; case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: params[0] = static_cast(attr->Divisor); return; @@ -1031,6 +1060,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_VERTEX_ATTRIB_ARRAY_INTEGER: params[0] = attr->IsInteger ? GL_TRUE : GL_FALSE; return; + case GL_VERTEX_ATTRIB_ARRAY_LONG: + params[0] = attr->IsLong ? GL_TRUE : GL_FALSE; + return; case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: params[0] = static_cast(attr->Divisor); return; @@ -1164,8 +1196,7 @@ namespace MobileGL::MG_Impl::GLImpl { *param = attr.IsInteger ? GL_TRUE : GL_FALSE; return; case GL_VERTEX_ATTRIB_ARRAY_LONG: - // 64-bit attributes are not supported, so no attribute is ever a long one. - *param = GL_FALSE; + *param = attr.IsLong ? GL_TRUE : GL_FALSE; return; case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: *param = static_cast(attr.Divisor); @@ -1259,13 +1290,13 @@ namespace MobileGL::MG_Impl::GLImpl { void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) { auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat"); if (!vao) return; - VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset); + VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset); } void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) { auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayAttribLFormat"); if (!vao) return; - VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset); + VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset); } void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) { diff --git a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp index e8a9c731..a0c0aaa8 100644 --- a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp +++ b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp @@ -57,7 +57,7 @@ namespace MobileGL::MG_State::GLState { if (m_attributes[index].Size == size && m_attributes[index].Type == type && m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride && m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger && - m_attributes[index].IsBgra == isBgra) { + m_attributes[index].IsBgra == isBgra && !m_attributes[index].IsLong) { return; } @@ -73,6 +73,11 @@ namespace MobileGL::MG_State::GLState { attr.Offset = offset; attr.IsInteger = isInteger; attr.IsBgra = isBgra; + // glVertexAttribPointer / glVertexAttribIPointer are never the long form, so they always + // take the attribute back out of it - and "only IsLong changed" is a real change that has to + // reach the backends, which is why the early-out above tests it too. Cleared inside the + // mutation block so the clear and the version bump stay atomic. + attr.IsLong = false; BumpAttributeFormatVersion(index); } @@ -212,18 +217,21 @@ namespace MobileGL::MG_State::GLState { } void VertexArrayObject::SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized, - Bool isInteger, Uint relativeOffset, Bool isBgra) { + Bool isInteger, Uint relativeOffset, Bool isBgra, + Bool isLong) { if (attribIndex >= MAX_VERTEX_ATTRIBS) return; if (size < 1 || size > 4) return; auto& attr = m_attributes[attribIndex]; if (attr.Size != size || attr.Type != type || attr.Normalized != normalized || attr.IsInteger != isInteger || - attr.IsBgra != isBgra || m_attributeRelativeOffset[attribIndex] != relativeOffset) { + attr.IsBgra != isBgra || attr.IsLong != isLong || + m_attributeRelativeOffset[attribIndex] != relativeOffset) { attr.Size = size; attr.Type = type; attr.Normalized = normalized; attr.IsInteger = isInteger; attr.IsBgra = isBgra; + attr.IsLong = isLong; m_attributeRelativeOffset[attribIndex] = relativeOffset; BumpAttributeFormatVersion(attribIndex); } diff --git a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h index 8c3b6f78..f0b67403 100644 --- a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h +++ b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h @@ -23,6 +23,12 @@ namespace MobileGL { SizeT Offset = 0; Bool IsInteger = false; // GL_BGRA vertex size: four components in reversed (B,G,R,A) memory order. Size stays 4. + // Set only by the long (L) format entry points. It is NOT implied by + // Type == Float64: VertexAttribFormat(GL_DOUBLE) also reads doubles from memory but + // asks for them *converted to float*, while VertexAttribLFormat keeps all 64 bits + // (GL 4.6 core 10.3.2). Backends have to tell the two apart, and it is what + // GL_VERTEX_ATTRIB_ARRAY_LONG reports. + Bool IsLong = false; Bool IsBgra = false; Uint Divisor = 0; SharedPtr Buffer; @@ -88,7 +94,8 @@ namespace MobileGL { void SetBindingDivisor(Uint bindingIndex, Uint divisor); void SetAttributeBinding(Uint attribIndex, Uint bindingIndex); void SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized, - Bool isInteger, Uint relativeOffset, Bool isBgra = false); + Bool isInteger, Uint relativeOffset, Bool isBgra = false, + Bool isLong = false); // The binding-point view the attributes were resolved from. Kept queryable // because glGetVertexArrayIndexed[64]iv reports it verbatim, and the resolved diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp index be80e933..db6342bf 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp @@ -198,6 +198,7 @@ namespace MobileGL::MG_Util::BackendLoader { VkPhysicalDeviceFeatures supportedFeatures{}; vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures); caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE; + caps.SupportsShaderFloat64 = supportedFeatures.shaderFloat64 == VK_TRUE; caps.SupportsVertexPipelineStoresAndAtomics = supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE; caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE; @@ -288,6 +289,7 @@ namespace MobileGL::MG_Util::BackendLoader { caps.ViewportSubpixelBits = static_cast(properties.limits.viewportSubPixelBits); FillFragmentInterpolationLimits(caps, properties.limits); caps.SupportsWideLines = false; + caps.SupportsShaderFloat64 = false; // This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional // stage writes disabled rather than inferring them from descriptor limits alone. caps.SupportsVertexPipelineStoresAndAtomics = false; diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h index f731c558..37ba8ce5 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h @@ -74,6 +74,10 @@ namespace MobileGL { Float MaxFragmentInterpolationOffset = 0.4375f; Int FragmentInterpolationOffsetBits = 4; Bool SupportsWideLines = false; + // VkPhysicalDeviceFeatures::shaderFloat64. Any module declaring OpCapability Float64 + // needs it, which includes every 64-bit vertex attribute: the attribute itself arrives + // as 32-bit words, but the bitcast result and everything computed from it is Float64. + Bool SupportsShaderFloat64 = 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. diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index c52077d7..9edd4156 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -319,6 +319,14 @@ namespace MobileGL::MG_Util::SelfTest { "map array texture gets no driver storage at all, so sampling one reads nothing " "and rendering to one does not reach the screen"); } + // Reported rather than probed: this one cannot come out any other way. OpenGL ES has no + // double-precision vertex format and ESSL has no fp64 type, so there is no driver and no + // extension that could make it work - the row exists so the loss is named at startup + // instead of discovered as an unexplained GL_INVALID_OPERATION at draw setup. + builder.Warn("64-bit vertex attributes", + "not supported on any GLES driver (ES has no GL_DOUBLE vertex format and ESSL has " + "no fp64 type); glVertexAttribLFormat / glVertexArrayAttribLFormat report " + "GL_INVALID_OPERATION - use the Vulkan backend if the application needs them"); if (glesFuncs.glPatchParameteri != nullptr) { builder.Pass("Tessellation patch parameters", "glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)"); @@ -1411,6 +1419,14 @@ namespace MobileGL::MG_Util::SelfTest { } else { builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw"); } + if (features.shaderFloat64 == VK_TRUE) { + builder.Pass("shaderFloat64", + "GLSL double/dvec/dmat and 64-bit vertex attributes (glVertexAttribLFormat) supported"); + } else { + builder.Warn("shaderFloat64", + "unsupported; any shader declaring a double fails to create a shader module, and " + "glVertexAttribLFormat reports GL_INVALID_OPERATION instead of feeding the attribute"); + } Bool shaderDrawParameters = false; if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 169f1e77..1e8fdb4e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -18,6 +18,7 @@ #include "SpirvPasses/DecomposeWorkgroupVec3Pass.h" #include "SpirvPasses/DecoratePositionInvariantPass.h" #include "SpirvPasses/LowerDrawParametersPass.h" +#include "SpirvPasses/PackDoubleVertexInputsPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h" #include "SpirvPasses/NormalizeRectCoordinatesPass.h" #include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" @@ -327,6 +328,18 @@ namespace MobileGL { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } + bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); + + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass()); + + return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); + } + bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector& inputBinary, Vector& outputBinary) { using namespace spvtools; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 2cdb9c01..726b4009 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -54,6 +54,13 @@ namespace MobileGL { static bool LowerRectImages(const Vector& inputBinary, Vector& outputBinary); static bool RebaseInstanceIndexForVulkan(const Vector& inputBinary, Vector& outputBinary); + // Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair + // (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no + // VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex + // buffers. Vertex stage, DirectVulkan only; pairs with the Float64 case in + // VertexInputStateFactory::ToVkVertexFormat. + static bool PackDoubleVertexInputsForVulkan(const Vector& inputBinary, + Vector& outputBinary); // Adds the Invariant decoration to every Position builtin output. GL apps // routinely rely on cross-program position invariance for multi-pass // equality depth tests (e.g. GEQUAL re-draws of the same geometry), and diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp new file mode 100644 index 00000000..9afff299 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp @@ -0,0 +1,211 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "PackDoubleVertexInputsPass.h" + +#include "spirv.hpp" +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_context.h" +#include "source/opt/module.h" +#include "source/opt/types.h" +#include "source/util/make_unique.h" + +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::IRContext; + using spvtools::opt::Instruction; + using spvtools::opt::Operand; + namespace analysis = spvtools::opt::analysis; + + // Component count of a 64-bit float input, or 0 if the type is not one. + Uint32 DoubleComponentCount(const analysis::Type* type) { + if (type == nullptr) return 0; + if (const auto* scalar = type->AsFloat()) { + return scalar->width() == 64 ? 1u : 0u; + } + if (const auto* vector = type->AsVector()) { + const auto* element = vector->element_type()->AsFloat(); + if (element == nullptr || element->width() != 64) return 0; + return vector->element_count(); + } + return 0; + } + } // namespace + + spvtools::opt::Pass::Status PackDoubleVertexInputsPass::Process() { + auto* irContext = context(); + auto entryPoints = irContext->module()->entry_points(); + if (entryPoints.begin() == entryPoints.end()) return Status::SuccessWithoutChange; + + Instruction* entryPoint = &*entryPoints.begin(); + if (static_cast(entryPoint->GetSingleWordInOperand(0)) != + spv::ExecutionModel::Vertex) { + return Status::SuccessWithoutChange; + } + + auto* defUseMgr = irContext->get_def_use_mgr(); + auto* typeMgr = irContext->get_type_mgr(); + + struct Target { + Instruction* variable = nullptr; + Uint32 doubleTypeId = 0; + Uint32 componentCount = 0; + Uint32 packedTypeId = 0; + Uint32 packedPointerTypeId = 0; + Uint32 privatePointerTypeId = 0; + }; + std::vector targets; + + for (Instruction& inst : irContext->types_values()) { + if (inst.opcode() != spv::Op::OpVariable) continue; + if (static_cast(inst.GetSingleWordInOperand(0)) != + spv::StorageClass::Input) { + continue; + } + Instruction* pointerType = defUseMgr->GetDef(inst.type_id()); + if (pointerType == nullptr) continue; + const Uint32 pointeeTypeId = pointerType->GetSingleWordInOperand(1); + const Uint32 components = DoubleComponentCount(typeMgr->GetType(pointeeTypeId)); + if (components == 0) continue; + if (components > 2) { + // 6 or 8 uint32 components has no single vertex format, and GL spreads such + // an input over two attribute locations. Left alone; the vertex-input + // factory declines the matching attribute for the same reason. + MGLOG_E("PackDoubleVertexInputsPass: vertex input %%%u is a %u-component 64-bit " + "float; only double and dvec2 inputs can be packed", + inst.result_id(), components); + continue; + } + targets.push_back({&inst, pointeeTypeId, components}); + } + + if (targets.empty()) return Status::SuccessWithoutChange; + + // Entry block insertion point: after the block's leading OpVariable run, which + // SPIR-V requires to stay at the top of a function's first block. + const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1); + spvtools::opt::Function* entryFunction = nullptr; + for (auto& function : *irContext->module()) { + if (function.result_id() == entryFunctionId) { + entryFunction = &function; + break; + } + } + if (entryFunction == nullptr || entryFunction->begin() == entryFunction->end()) { + return Status::SuccessWithoutChange; + } + auto& entryBlock = *entryFunction->begin(); + auto insertPoint = entryBlock.begin(); + while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) { + ++insertPoint; + } + if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange; + + const Uint32 uintTypeId = typeMgr->GetUIntTypeId(); + const analysis::Integer* uintType = typeMgr->GetType(uintTypeId)->AsInteger(); + + // Every type instruction has to exist before any variable that names it: the + // types-and-variables section is walked in order and a forward reference to a type is + // invalid SPIR-V. GetTypeInstruction/FindPointerToType append, and the variables are + // appended (or re-appended) below, so all three lookups run first for every target. + for (auto& target : targets) { + analysis::Vector packedVectorType(uintType, target.componentCount * 2u); + target.packedTypeId = typeMgr->GetTypeInstruction(&packedVectorType); + target.packedPointerTypeId = + typeMgr->FindPointerToType(target.packedTypeId, spv::StorageClass::Input); + target.privatePointerTypeId = + typeMgr->FindPointerToType(target.doubleTypeId, spv::StorageClass::Private); + } + + for (const auto& target : targets) { + Instruction* variable = target.variable; + const Uint32 oldVariableId = variable->result_id(); + const Uint32 packedTypeId = target.packedTypeId; + const Uint32 packedPointerTypeId = target.packedPointerTypeId; + + const Uint32 packedVariableId = irContext->TakeNextId(); + irContext->AddGlobalValue(spvtools::MakeUnique( + irContext, spv::Op::OpVariable, packedPointerTypeId, packedVariableId, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, + {static_cast(spv::StorageClass::Input)}}})); + + // The interface decorations belong to whatever is actually the Input now. + std::vector deadDecorations; + for (auto& annotation : irContext->annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate) continue; + if (annotation.GetSingleWordInOperand(0) != oldVariableId) continue; + const auto decoration = + static_cast(annotation.GetSingleWordInOperand(1)); + if (decoration == spv::Decoration::Location || + decoration == spv::Decoration::Component || + decoration == spv::Decoration::RelaxedPrecision) { + annotation.SetInOperand(0, {packedVariableId}); + } else { + deadDecorations.push_back(&annotation); + } + } + for (auto* annotation : deadDecorations) { + irContext->KillInst(annotation); + } + + // Demote the original to a Private global: every existing OpLoad / + // OpAccessChain on it stays valid and keeps its double type. It is also moved to + // the end of the section, because the pointer-to-Private type it now names was + // appended above and a variable may not forward-reference its own type. + variable->SetResultType(target.privatePointerTypeId); + variable->SetInOperand(0, {static_cast(spv::StorageClass::Private)}); + variable->RemoveFromList(); + irContext->AddGlobalValue(std::unique_ptr(variable)); + + // SPIR-V 1.3 lists only Input/Output in the entry-point interface. + std::vector interfaceOperands; + for (Uint32 i = 0; i < entryPoint->NumInOperands(); ++i) { + const Operand& operand = entryPoint->GetInOperand(i); + if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID && + entryPoint->GetSingleWordInOperand(i) == oldVariableId) { + interfaceOperands.push_back({SPV_OPERAND_TYPE_ID, {packedVariableId}}); + continue; + } + interfaceOperands.push_back(operand); + } + entryPoint->SetInOperands(std::move(interfaceOperands)); + + const Uint32 loadedId = irContext->TakeNextId(); + const Uint32 bitcastId = irContext->TakeNextId(); + insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpLoad, packedTypeId, loadedId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {packedVariableId}}})); + ++insertPoint; + insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpBitcast, target.doubleTypeId, bitcastId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {loadedId}}})); + ++insertPoint; + insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpStore, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {oldVariableId}}, + {SPV_OPERAND_TYPE_ID, {bitcastId}}})); + ++insertPoint; + } + + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.h new file mode 100644 index 00000000..5b2fef1f --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.h @@ -0,0 +1,43 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include "source/opt/pass.h" +#include "spirv-tools/optimizer.hpp" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // Re-declares every 64-bit floating-point *vertex input* as the 32-bit unsigned word + // pair that holds the same bytes (double -> uvec2, dvec2 -> uvec4), demotes the + // original variable to a Private global, and seeds it once at the top of the entry + // point with an OpBitcast of the new input. Everything downstream keeps loading the + // same id and the same double type, so no other instruction is rewritten. + // + // Why not just use VK_FORMAT_R64*_SFLOAT: those formats are optional, and lavapipe + // reports zero bufferFeatures for all four of them, so a 64-bit vertex fetch is + // impossible there even though shaderFloat64 is supported. The word-pair form needs no + // format capability at all and is bit-exact, so it is applied unconditionally rather + // than as a fallback - which also keeps it in lockstep with + // VertexInputStateFactory::ToVkVertexFormat, since both branch on nothing but "is this + // a 64-bit vertex input". + // + // DirectVulkan only. The 64-bit *arithmetic* still needs the Float64 capability, i.e. + // an enabled VkPhysicalDeviceFeatures::shaderFloat64. + class PackDoubleVertexInputsPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "mobilegl-pack-double-vertex-inputs"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreatePackDoubleVertexInputsPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL