From 964a7fcc9221fdcd74bd8e439c94fd372f22fde5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 11 Aug 2026 08:39:39 -0400 Subject: [PATCH] [Fix, Test] (MG_State, MG_Backend/DirectVulkan): resolve transform-feedback captures that name a member of an output interface block --- .../DirectVulkan/Renderer/ProgramFactory.cpp | 118 +++++++++- .../GLState/ProgramState/ProgramLinkTask.cpp | 65 ++++- .../GLState/ProgramState/ProgramObject.h | 15 ++ MobileGL/MG_Test/Program/CMakeLists.txt | 17 ++ .../MG_Test/Program/XfbBlockVaryingTest.cpp | 222 ++++++++++++++++++ 5 files changed, 433 insertions(+), 4 deletions(-) create mode 100644 MobileGL/MG_Test/Program/XfbBlockVaryingTest.cpp diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 85f54fc8..e80ddb67 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -12,7 +12,10 @@ #include "MG_Util/ShaderTranspiler/ShaderCompiler.h" #include "MG_Util/ShaderTranspiler/SpvcSession.h" #include "MG_Util/ShaderTranspiler/Types.h" +#include #include +#include +#include #include #include #include @@ -1131,6 +1134,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::string name; Uint32 bufferIndex = 0; Uint32 offsetBytes = 0; + // Set when the capture names a member of an output interface block + // ("Block.member"): the decoration target is then the block's struct TYPE, + // decorated per member, not the variable. `name` keeps the GL spelling and + // is useless for the id lookup, so the instance name is carried separately. + std::string blockInstanceName; + std::string blockName; + Int blockMemberIndex = -1; + Int blockMemberElement = -1; // array element of that member, -1 = the whole member + Uint32 byteSize = 0; }; const char* name() const override { return "mobilegl-xfb-capture-decorate"; } XfbCaptureDecoratePass(Vector varyings, Vector strides) @@ -1162,6 +1174,33 @@ namespace MobileGL::MG_Backend::DirectVulkan { decorationManager->AddDecorationVal(targetId, static_cast(spv::Decoration::Offset), offsetBytes); }; + // SPIR-V puts XfbBuffer/XfbStride/Offset on the struct MEMBER when the + // captured varying lives in an interface block (SPIR-V 1.6 ยง3.20 lists all + // three as member-decoratable); Offset in particular is illegal on the block + // variable once the type is decorated Block. + const auto decorateMemberForXfb = [&](Uint32 structTypeId, Uint32 memberIndex, Uint32 bufferIndex, + Uint32 offsetBytes) { + const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0; + decorationManager->AddMemberDecoration(structTypeId, memberIndex, + static_cast(spv::Decoration::XfbBuffer), + bufferIndex); + decorationManager->AddMemberDecoration(structTypeId, memberIndex, + static_cast(spv::Decoration::XfbStride), stride); + decorationManager->AddMemberDecoration(structTypeId, memberIndex, + static_cast(spv::Decoration::Offset), offsetBytes); + }; + + // A member array captured element by element ("Block.attrib[0]" .. "[15]") + // is one SPIR-V member, so its captures collapse into a single decoration + // placed at the first element's offset - the rest follow from the member's + // own layout. Collected first so the group is complete before it decorates. + struct MemberGroup { + Uint32 bufferIndex = 0; + Uint32 minOffset = 0; + Uint32 elementBytes = 0; + Vector offsets; + }; + std::map, MemberGroup> memberGroups; Bool modified = false; Bool needsPositionMirror = false; @@ -1174,6 +1213,41 @@ namespace MobileGL::MG_Backend::DirectVulkan { positionOffset = varying.offsetBytes; continue; } + if (varying.blockMemberIndex >= 0) { + // glslang names the block's instance variable and its struct type + // separately; an anonymous instance leaves only the type named, so + // both spellings are tried before giving up. + Uint32 structTypeId = 0; + if (const auto it = idsByName.find(varying.blockInstanceName); it != idsByName.end()) { + structTypeId = BlockStructTypeOf(it->second); + } + if (structTypeId == 0) { + if (const auto it = idsByName.find(varying.blockName); it != idsByName.end()) { + const spvtools::opt::Instruction* def = context()->get_def_use_mgr()->GetDef(it->second); + if (def != nullptr && def->opcode() == spv::Op::OpTypeStruct) { + structTypeId = it->second; + } else if (def != nullptr && def->opcode() == spv::Op::OpVariable) { + structTypeId = BlockStructTypeOf(it->second); + } + } + } + if (structTypeId == 0) { + MGLOG_E("XfbCaptureDecoratePass: no SPIR-V interface block '%s' (instance '%s') for " + "capture '%s'", + varying.blockName.c_str(), varying.blockInstanceName.c_str(), + varying.name.c_str()); + continue; + } + auto& group = + memberGroups[{structTypeId, static_cast(varying.blockMemberIndex)}]; + if (group.offsets.empty() || varying.offsetBytes < group.minOffset) { + group.minOffset = varying.offsetBytes; + } + group.bufferIndex = varying.bufferIndex; + group.elementBytes = varying.byteSize; + group.offsets.push_back(varying.offsetBytes); + continue; + } const auto idIt = idsByName.find(varying.name); if (idIt == idsByName.end()) { MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str()); @@ -1183,6 +1257,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { modified = true; } + for (auto& [key, group] : memberGroups) { + // The single Offset can only stand for the whole group when the group's + // captures are a gap-free ascending run - that is what SPIR-V lays the + // member's elements out as. Anything else still gets a best-effort + // decoration, but say so, because the capture layout will not match GL. + std::sort(group.offsets.begin(), group.offsets.end()); + for (SizeT i = 1; i < group.offsets.size(); ++i) { + if (group.elementBytes == 0 || + group.offsets[i] != group.offsets[i - 1] + group.elementBytes) { + MGLOG_I("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a " + "non-contiguous element set; the capture layout will differ from GL's", + key.second, key.first); + break; + } + } + decorateMemberForXfb(key.first, key.second, group.bufferIndex, group.minOffset); + modified = true; + } + if (needsPositionMirror) { modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex, positionOffset, decorateForXfb); @@ -1204,6 +1297,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { } private: + // The struct type an interface-block variable points at, peeling an array of + // block instances on the way. 0 when the id is not a block variable at all. + Uint32 BlockStructTypeOf(Uint32 variableId) { + auto* defUse = context()->get_def_use_mgr(); + const spvtools::opt::Instruction* variable = defUse->GetDef(variableId); + if (variable == nullptr || variable->opcode() != spv::Op::OpVariable) return 0; + const spvtools::opt::Instruction* pointer = defUse->GetDef(variable->type_id()); + if (pointer == nullptr || pointer->opcode() != spv::Op::OpTypePointer) return 0; + Uint32 pointeeId = pointer->GetSingleWordInOperand(1); + for (const spvtools::opt::Instruction* pointee = defUse->GetDef(pointeeId); pointee != nullptr; + pointee = defUse->GetDef(pointeeId)) { + if (pointee->opcode() == spv::Op::OpTypeStruct) return pointeeId; + if (pointee->opcode() != spv::Op::OpTypeArray && + pointee->opcode() != spv::Op::OpTypeRuntimeArray) { + return 0; + } + pointeeId = pointee->GetSingleWordInOperand(0); + } + return 0; + } + template Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint, Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) { @@ -1522,7 +1636,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector varyings; varyings.reserve(program.GetTransformFeedbackVaryingCount()); for (const auto& varying : program.GetTransformFeedbackVaryings()) { - varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes}); + varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes, + varying.blockInstanceName, varying.blockName, varying.blockMemberIndex, + varying.blockMemberElement, varying.byteSize}); } Vector strides; strides.reserve(program.GetTransformFeedbackBufferCount()); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index f20cc3d5..b19f0c3b 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -1030,21 +1030,80 @@ namespace MobileGL::MG_State::GLState { } } } + // GL 4.6 core 11.1.2.1 (and the resource-name rule of 7.3.1.1): a member of + // an output interface block is named "." - the block's + // TYPE name, never the instance name, and that holds for an anonymous + // instance too. glslang's linker object for such a block is the *instance* + // symbol ("vs_out", or "anon@N" when there is none), so the head of the + // dotted path has to be matched against getType().getTypeName() instead of + // getName(). Without this every capture of a block member resolved to + // nothing and the link failed with "is not an output of the vertex stage". + String blockName; + String memberName; + if (const SizeT dot = declaredName.find('.'); dot != String::npos) { + blockName = declaredName.substr(0, dot); + memberName = declaredName.substr(dot + 1); + // An array of block instances is spelled "[i]."; every + // instance shares one member list, so the subscript only has to go. + if (!blockName.empty() && blockName.back() == ']') { + const SizeT bracket = blockName.rfind('['); + if (bracket != String::npos) blockName.resize(bracket); + } + } + for (const auto* node : linkerObjects->getSequence()) { const glslang::TIntermSymbol* symbol = node->getAsSymbolNode(); if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) { continue; } - if (symbol->getName() != declaredName.c_str()) { - continue; + const glslang::TType& symbolType = symbol->getType(); + const glslang::TType* capturedType = nullptr; + if (memberName.empty()) { + if (symbol->getName() != declaredName.c_str()) { + continue; + } + capturedType = &symbolType; + } else { + if (symbolType.getBasicType() != glslang::EbtBlock) { + continue; + } + // The spec spelling is the block name; the instance name is accepted + // as a fallback so a request written the (common, non-conformant) + // instance-qualified way resolves instead of failing the whole link. + if (symbolType.getTypeName() != blockName.c_str() && + symbol->getName() != blockName.c_str()) { + continue; + } + const glslang::TTypeList* members = symbolType.getStruct(); + if (members == nullptr) { + continue; + } + for (SizeT m = 0; m < members->size(); ++m) { + const glslang::TType* memberType = (*members)[m].type; + if (memberType == nullptr || memberType->getFieldName() != memberName.c_str()) { + continue; + } + capturedType = memberType; + varying.blockMemberIndex = static_cast(m); + break; + } + if (capturedType == nullptr) { + // Right block, wrong member: no other linker object can match. + break; + } + varying.blockName = symbolType.getTypeName().c_str(); + varying.blockInstanceName = symbol->getName().c_str(); } - resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement); + resolved = ResolveXfbSymbolType(*capturedType, varying.type, varying.size, bytesPerElement); if (resolved && singleElement) { if (static_cast(element) >= varying.size) { resolved = false; break; } varying.size = 1; + if (varying.blockMemberIndex >= 0) { + varying.blockMemberElement = static_cast(element); + } } break; } diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index a888266e..b350ed0c 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -672,6 +672,21 @@ namespace MobileGL::MG_State::GLState { // Offset within the gap-free record a backend that cannot express the GL // layout captures into; see NeedsScatteredTransformFeedbackCapture. Uint32 packedOffsetBytes = 0; + + // GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is + // captured under ".". `name` keeps that GL spelling (it is + // what the interface queries and the ESSL backend's driver-side capture list + // need, since SPIRV-Cross re-emits the block under its own type name), while + // the three fields below carry what a SPIR-V backend needs instead: the + // decoration target is the block's *instance* variable and the member index + // inside it. blockMemberIndex < 0 means "not a block member". + String blockInstanceName; + String blockName; + Int blockMemberIndex = -1; + // Which element of an arrayed block member this capture names, -1 for "the + // member as a whole". SPIR-V cannot decorate a single array element, so a + // backend needs the element index to tell a full run from a partial one. + Int blockMemberElement = -1; }; // ---- P1: everything a link PRODUCES, in one movable block ---- diff --git a/MobileGL/MG_Test/Program/CMakeLists.txt b/MobileGL/MG_Test/Program/CMakeLists.txt index ef17765a..808e53c5 100644 --- a/MobileGL/MG_Test/Program/CMakeLists.txt +++ b/MobileGL/MG_Test/Program/CMakeLists.txt @@ -164,6 +164,22 @@ target_link_libraries( ${LINK_LIBRARIES} ) +add_executable( + XfbBlockVaryingTest + XfbBlockVaryingTest.cpp +) + +target_include_directories(XfbBlockVaryingTest PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL +) + +target_link_libraries( + XfbBlockVaryingTest PRIVATE + GTest::gtest_main + ${LINK_LIBRARIES} +) + add_executable( ProgramInterfaceTest ProgramInterfaceTest.cpp @@ -195,6 +211,7 @@ include(GoogleTest) gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) +gtest_discover_tests(XfbBlockVaryingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) # Heavier than the rest of the unit suite by design: several cases deliberately saturate the # compile pool so there is something in flight to race against. gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300) diff --git a/MobileGL/MG_Test/Program/XfbBlockVaryingTest.cpp b/MobileGL/MG_Test/Program/XfbBlockVaryingTest.cpp new file mode 100644 index 00000000..1f5a5cbf --- /dev/null +++ b/MobileGL/MG_Test/Program/XfbBlockVaryingTest.cpp @@ -0,0 +1,222 @@ +// MobileGL - MobileGL/MG_Test/Program/XfbBlockVaryingTest.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 + +// Transform-feedback capture of a member of an output interface block. +// +// GL 4.6 core 11.1.2.1 names such a varying "." - the block's TYPE +// name, never the instance name - which is exactly what KHR-GL4x.vertex_attrib_binding +// (gl4cVertexAttribBindingTests.cpp:419-437, `out StageData { vec4 attrib[16]; } vs_out;` +// captured as "StageData.attrib[0]".."[15]") relies on. The resolver used to match the +// requested name against glslang's linker-object symbol name, which for a block is the +// INSTANCE ("vs_out"), so every one of those captures came back unresolved and the link +// failed with "is not an output of the vertex stage" + GL_INVALID_VALUE. +// +// GPU-free: everything asserted here is a property of the link, not of any driver. + +#include + +#include +#include + +#include "Includes.h" +#include "Init.h" +#include "MG_Impl/GLImpl/Getter/GL_Getter.h" +#include "MG_Impl/GLImpl/Program/GL_Program.h" +#include "MG_State/GLState/Core.h" + +using namespace MobileGL; +using namespace MobileGL::MG_Impl::GLImpl; + +namespace { + class XfbBlockVaryingTest : public ::testing::Test { + protected: + void SetUp() override { MobileGL::Initialize(); } + }; + + GLuint MakeVsOnlyProgram(const char* vs) { + const GLuint program = CreateProgram(); + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + ShaderSource(shader, 1, &vs, nullptr); + CompileShader(shader); + GLint compiled = GL_FALSE; + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + EXPECT_EQ(compiled, GL_TRUE) << [&] { + char log[4096] = ""; + GetShaderInfoLog(shader, sizeof(log), nullptr, log); + return std::string(log); + }(); + AttachShader(program, shader); + return program; + } + + std::string LinkLog(GLuint program) { + char log[4096] = ""; + GetProgramInfoLog(program, sizeof(log), nullptr, log); + return std::string(log); + } + + GLint Programiv(GLuint program, GLenum pname) { + GLint value = -1; + GetProgramiv(program, pname, &value); + return value; + } + + struct VaryingRecord { + std::string name; + GLsizei size = 0; + GLenum type = 0; + }; + + VaryingRecord Varying(GLuint program, GLuint index) { + VaryingRecord record; + GLchar buffer[256] = {'\0'}; + GLsizei length = 0; + GetTransformFeedbackVarying(program, index, sizeof(buffer), &length, &record.size, &record.type, buffer); + record.name.assign(buffer, buffer + (length < 0 ? 0 : length)); + return record; + } + + void ClearErrors() { + for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) { + } + } + + // The CTS shader, narrowed to two elements so the expectations stay readable. + const char* kNamedBlockVs = R"(#version 430 core +layout(location = 0) in vec4 vs_in_attrib[2]; +out StageData { + vec4 attrib[2]; +} vs_out; +void main() { + for (int i = 0; i < vs_in_attrib.length(); ++i) { + vs_out.attrib[i] = vs_in_attrib[i]; + } +} +)"; + + TEST_F(XfbBlockVaryingTest, CapturesBlockMemberElementsByBlockTypeName) { + ClearErrors(); + const GLuint program = MakeVsOnlyProgram(kNamedBlockVs); + const GLchar* const varyings[2] = {"StageData.attrib[0]", "StageData.attrib[1]"}; + TransformFeedbackVaryings(program, 2, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(program); + + ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program); + EXPECT_EQ(GetError(), GL_NO_ERROR); + EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 2); + EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_BUFFER_MODE), GL_INTERLEAVED_ATTRIBS); + + for (GLuint i = 0; i < 2; ++i) { + const VaryingRecord record = Varying(program, i); + EXPECT_EQ(record.name, std::string("StageData.attrib[") + std::to_string(i) + "]"); + // One element of the member array, not the whole array. + EXPECT_EQ(record.size, 1) << "index " << i; + EXPECT_EQ(record.type, static_cast(GL_FLOAT_VEC4)) << "index " << i; + } + } + + // The whole member, no subscript: the array size has to survive. + TEST_F(XfbBlockVaryingTest, CapturesAWholeBlockMemberArray) { + ClearErrors(); + const GLuint program = MakeVsOnlyProgram(kNamedBlockVs); + const GLchar* const varyings[1] = {"StageData.attrib"}; + TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(program); + + ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program); + const VaryingRecord record = Varying(program, 0); + EXPECT_EQ(record.name, "StageData.attrib"); + EXPECT_EQ(record.size, 2); + EXPECT_EQ(record.type, static_cast(GL_FLOAT_VEC4)); + } + + // Members of an anonymous instance are named the same way - the block name is still + // what identifies them, and there is no instance name to fall back on. + TEST_F(XfbBlockVaryingTest, CapturesAnonymousInstanceBlockMember) { + ClearErrors(); + const GLuint program = MakeVsOnlyProgram(R"(#version 430 core +layout(location = 0) in vec4 vs_in_attrib; +out StageData { + vec4 color; + vec2 uv; +}; +void main() { + color = vs_in_attrib; + uv = vs_in_attrib.xy; +} +)"); + const GLchar* const varyings[2] = {"StageData.color", "StageData.uv"}; + TransformFeedbackVaryings(program, 2, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(program); + + ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program); + EXPECT_EQ(Varying(program, 0).type, static_cast(GL_FLOAT_VEC4)); + EXPECT_EQ(Varying(program, 1).type, static_cast(GL_FLOAT_VEC2)); + } + + // The instance-qualified spelling is not what the spec asks for, but it is what a lot of + // application code writes; resolving it too costs nothing and keeps those links alive. + TEST_F(XfbBlockVaryingTest, AlsoAcceptsTheInstanceQualifiedSpelling) { + ClearErrors(); + const GLuint program = MakeVsOnlyProgram(kNamedBlockVs); + const GLchar* const varyings[1] = {"vs_out.attrib[1]"}; + TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(program); + + ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program); + EXPECT_EQ(Varying(program, 0).size, 1); + EXPECT_EQ(Varying(program, 0).type, static_cast(GL_FLOAT_VEC4)); + } + + // A dotted path that resolves to nothing must still fail the link, and say so - the + // fix must not turn "unknown member" into a silently dropped capture. + TEST_F(XfbBlockVaryingTest, RejectsAnUnknownBlockMember) { + ClearErrors(); + const GLuint program = MakeVsOnlyProgram(kNamedBlockVs); + const GLchar* const varyings[1] = {"StageData.missing"}; + TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(program); + + EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE); + EXPECT_NE(LinkLog(program).find("StageData.missing"), std::string::npos) << LinkLog(program); + } + + TEST_F(XfbBlockVaryingTest, RejectsAnUnknownBlock) { + ClearErrors(); + const GLuint program = MakeVsOnlyProgram(kNamedBlockVs); + const GLchar* const varyings[1] = {"NoSuchBlock.attrib[0]"}; + TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(program); + + EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE); + } + + // Plain (non-block) outputs must keep resolving exactly as before. + TEST_F(XfbBlockVaryingTest, StillResolvesPlainOutputs) { + ClearErrors(); + const GLuint program = MakeVsOnlyProgram(R"(#version 430 core +layout(location = 0) in vec4 vs_in_attrib; +out vec4 plain[2]; +out vec3 single; +void main() { + plain[0] = vs_in_attrib; + plain[1] = vs_in_attrib; + single = vs_in_attrib.xyz; +} +)"); + const GLchar* const varyings[3] = {"plain[1]", "single", "gl_Position"}; + TransformFeedbackVaryings(program, 3, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(program); + + ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program); + EXPECT_EQ(Varying(program, 0).size, 1); + EXPECT_EQ(Varying(program, 0).type, static_cast(GL_FLOAT_VEC4)); + EXPECT_EQ(Varying(program, 1).type, static_cast(GL_FLOAT_VEC3)); + EXPECT_EQ(Varying(program, 2).type, static_cast(GL_FLOAT_VEC4)); + } +} // namespace