[Feat] (MG_State, MG_Backend, MG_Util): feed a 64-bit vertex attribute on DirectVulkan

glVertexAttribLFormat validated its arguments and then refused unconditionally
with "64-bit vertex attributes are not supported", so
direct_state_access.vertex_arrays_attribute_format failed every GL_DOUBLE
subcase on both backends - the format never landed, the draw fetched whatever
the attribute held before, and the captured values came back as reinterpreted
garbage.

The attribute is now real state. IsLong is its own bit rather than being
inferred from Float64, because glVertexAttribFormat(GL_DOUBLE) also reads
doubles - it just asks for them converted to float - so the type alone cannot
tell the two apart. It participates in the format comparison, so an L-format
call over a plain one still bumps the version, and glVertexAttribPointer clears
it inside the mutation block so the clear and the bump stay atomic.
GL_VERTEX_ATTRIB_ARRAY_LONG stops being hardcoded false, and the pname is now
accepted by the attribute queries at all.

Support is detected, never assumed. SupportsFloat64VertexAttributes comes from
VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan and is false on
DirectGLES - not a driver question there and never will be, since ES has no
GL_DOUBLE vertex format and ESSL has no fp64 type to consume one with. A backend
without it declines in the entry point, with the GL error and a log line naming
the reason, rather than accepting state no draw could honour. Both cases get a
DriverPost row so the loss is named at startup instead of at draw setup.

On DirectVulkan the attribute deliberately does not use VK_FORMAT_R64*_SFLOAT:
those are optional and lavapipe advertises zero features for all four of them.
It is fetched as its 32-bit word pair (R32G32_UINT / R32G32B32A32_UINT) and
bitcast back to double in the shader by a new SPIR-V pass, which is bit-exact
and needs no format capability at all. The pass re-declares the input as uvec2 /
uvec4, demotes the original variable to a Private global and seeds it once at
the top of the entry point, so every existing load keeps its id and its double
type and no other instruction is rewritten. Both halves branch on nothing but
"is this attribute long", so they cannot disagree - and if the pass ever fails,
the assertion fires rather than letting a UINT format sit under a double input.
The pointer types are all created before any variable that names them and the
demoted variable is moved after them, since the types-and-variables section may
not forward-reference a type.

dvec3/dvec4 are declined rather than fetched wrong: six or eight uint32
components have no single VkFormat, and GL spreads such an input over two
attribute locations, which the location-per-index model here does not express.

Fixes vertex_arrays_attribute_format on Magma (369/371). On Espryt it stays
failing, now as a detected and explained decline rather than a blanket refusal.
This commit is contained in:
BZLZHH
2026-08-05 08:49:23 -04:00
parent 3b65e646e1
commit 34f09291da
19 changed files with 448 additions and 19 deletions
@@ -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<Uint> 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
@@ -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;
@@ -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;
@@ -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.