Browse Source

use location numbers for vertex input attributes, instead of name-based binding.

- add 'location' named field to buffer and mesh vertex format tables, replaces 'name' field.
- location numbers are expected to match 'layout (location = #)' qualifiers in vertex inputs in shader code.
- deprecate vertex inputs in shader code that don't have layout location qualifiers.
- love's default vertex attributes use location 0 for position, 1 for texcoord, and 2 for color.
- add new variants of Mesh:attachAttribute, setAttributeEnabled, and isAttributeEnabled which take location numbers instead of names.

Improves draw performance in vulkan and metal backends.
Sasha Szpakowski 11 months ago
parent
commit
50ff1e4d2b

+ 38 - 22
src/modules/graphics/Buffer.cpp

@@ -106,8 +106,13 @@ Buffer::Buffer(Graphics *gfx, const Settings &settings, const std::vector<DataDe
 			if (info.baseType == DATA_BASETYPE_BOOL)
 				throw love::Exception("Bool types are not supported in vertex buffers.");
 
-			if (decl.name.empty())
-				throw love::Exception("Vertex buffer attributes must have a name.");
+			if (decl.bindingLocation < 0 || decl.bindingLocation >= VertexAttributes::MAX)
+			{
+				if (decl.bindingLocation == -1 && !decl.name.empty())
+					legacyVertexBindings = true;
+				else
+					throw love::Exception("Vertex buffer attributes must have a valid binding location value within [0, %d).", VertexAttributes::MAX);
+			}
 		}
 
 		if (texelbuffer)
@@ -258,6 +263,17 @@ int Buffer::getDataMemberIndex(const std::string &name) const
 	return -1;
 }
 
+int Buffer::getDataMemberIndex(int bindingLocation) const
+{
+	for (size_t i = 0; i < dataMembers.size(); i++)
+	{
+		if (dataMembers[i].decl.bindingLocation == bindingLocation)
+			return (int)i;
+	}
+
+	return -1;
+}
+
 void Buffer::clear(size_t offset, size_t size)
 {
 	if (isImmutable())
@@ -280,53 +296,53 @@ std::vector<Buffer::DataDeclaration> Buffer::getCommonFormatDeclaration(CommonFo
 		return {};
 	case CommonFormat::XYf:
 		return {
-			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2 }
+			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_POS }
 		};
 	case CommonFormat::XYZf:
 		return {
-			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC3 }
+			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC3, 0, ATTRIB_POS }
 		};
 	case CommonFormat::RGBAub:
 		return {
-			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4 }
+			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4, 0, ATTRIB_COLOR }
 		};
 	case CommonFormat::STf_RGBAub:
 		return {
-			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC2 },
-			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4 },
+			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_TEXCOORD },
+			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4, 0, ATTRIB_COLOR },
 		};
 	case CommonFormat::STPf_RGBAub:
 		return {
-			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC3 },
-			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4 },
+			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC3, 0, ATTRIB_TEXCOORD },
+			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4, 0, ATTRIB_COLOR },
 		};
 	case CommonFormat::XYf_STf:
 		return {
-			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2 },
-			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC2 },
+			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_POS },
+			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_TEXCOORD },
 		};
 	case CommonFormat::XYf_STPf:
 		return {
-			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2 },
-			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC3 },
+			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_POS },
+			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC3, 0, ATTRIB_TEXCOORD },
 		};
 	case CommonFormat::XYf_STf_RGBAub:
 		return {
-			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2 },
-			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC2 },
-			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4 },
+			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_POS },
+			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_TEXCOORD },
+			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4, 0, ATTRIB_COLOR },
 		};
 	case CommonFormat::XYf_STus_RGBAub:
 		return {
-			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2 },
-			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_UNORM16_VEC2 },
-			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4 },
+			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_POS },
+			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_UNORM16_VEC2, 0, ATTRIB_TEXCOORD },
+			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4, 0, ATTRIB_COLOR },
 		};
 	case CommonFormat::XYf_STPf_RGBAub:
 		return {
-			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2 },
-			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC2 },
-			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4 },
+			{ getConstant(ATTRIB_POS), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_POS },
+			{ getConstant(ATTRIB_TEXCOORD), DATAFORMAT_FLOAT_VEC2, 0, ATTRIB_TEXCOORD },
+			{ getConstant(ATTRIB_COLOR), DATAFORMAT_UNORM8_VEC4, 0, ATTRIB_COLOR },
 		};
 	}
 

+ 8 - 1
src/modules/graphics/Buffer.h

@@ -65,11 +65,13 @@ public:
 		std::string name;
 		DataFormat format;
 		int arrayLength;
+		int bindingLocation;
 
-		DataDeclaration(const std::string &name, DataFormat format, int arrayLength = 0)
+		DataDeclaration(const std::string &name, DataFormat format, int arrayLength = 0, int bindingLocation = -1)
 			: name(name)
 			, format(format)
 			, arrayLength(arrayLength)
+			, bindingLocation(bindingLocation)
 		{}
 	};
 
@@ -117,6 +119,7 @@ public:
 	const DataMember &getDataMember(int index) const { return dataMembers[index]; }
 	size_t getMemberOffset(int index) const { return dataMembers[index].offset; }
 	int getDataMemberIndex(const std::string &name) const;
+	int getDataMemberIndex(int bindingLocation) const;
 	const std::string &getDebugName() const { return debugName; }
 
 	void setImmutable(bool immutable) { this->immutable = immutable; };
@@ -154,6 +157,8 @@ public:
 	 **/
 	virtual ptrdiff_t getTexelBufferHandle() const = 0;
 
+	bool hasLegacyVertexBindings() const { return legacyVertexBindings; }
+
 	static std::vector<DataDeclaration> getCommonFormatDeclaration(CommonFormat format);
 
 	class Mapper
@@ -198,6 +203,8 @@ protected:
 	bool mapped;
 	MapType mappedType;
 	bool immutable;
+
+	bool legacyVertexBindings = false;
 	
 }; // Buffer
 

+ 2 - 0
src/modules/graphics/Font.cpp

@@ -92,6 +92,8 @@ Font::Font(love::font::Rasterizer *r, const SamplerState &s)
 	if (pixelFormat == PIXELFORMAT_LA8_UNORM && !gfx->isPixelFormatSupported(pixelFormat, PIXELFORMATUSAGEFLAGS_SAMPLE))
 		pixelFormat = PIXELFORMAT_RGBA8_UNORM;
 
+	vertexAttributesID = gfx->registerVertexAttributes(VertexAttributes(vertexFormat, 0));
+
 	loadVolatile();
 	++fontCount;
 }

+ 4 - 0
src/modules/graphics/Font.h

@@ -150,6 +150,8 @@ public:
 
 	uint32 getTextureCacheID() const;
 
+	VertexAttributesID getVertexAttributesID() const { return vertexAttributesID; }
+
 	// Implements Volatile.
 	bool loadVolatile() override;
 	void unloadVolatile() override;
@@ -204,6 +206,8 @@ private:
 	// ID which is incremented when the texture cache is invalidated.
 	uint32 textureCacheID;
 
+	VertexAttributesID vertexAttributesID;
+
 	// 1 pixel of transparent padding between glyphs (so quads won't pick up
 	// other glyphs), plus one pixel of transparent padding that the quads will
 	// use, for edge antialiasing.

+ 43 - 17
src/modules/graphics/Graphics.cpp

@@ -212,8 +212,10 @@ Graphics::Graphics(const char *name)
 	states.reserve(10);
 	states.push_back(DisplayState());
 
+	noAttributesID = registerVertexAttributes(VertexAttributes());
+
 	if (!Shader::initialize())
-		throw love::Exception("Shader support failed to initialize!");
+		throw love::Exception("Shader support failed to initialize.");
 }
 
 Graphics::~Graphics()
@@ -1413,6 +1415,29 @@ void Graphics::updatePendingReadbacks()
 	}
 }
 
+VertexAttributesID Graphics::registerVertexAttributes(const VertexAttributes &attributes)
+{
+	for (size_t i = 0; i < vertexAttributesDatabase.size(); i++)
+	{
+		if (attributes == vertexAttributesDatabase[i])
+			return { (int)i + 1 };
+	}
+
+	vertexAttributesDatabase.push_back(attributes);
+	return { (int)vertexAttributesDatabase.size() };
+}
+
+bool Graphics::findVertexAttributes(VertexAttributesID id, VertexAttributes &attributes)
+{
+	int index = id.id - 1;
+
+	if (index < 0 || index >= (int)vertexAttributesDatabase.size())
+		return false;
+
+	attributes = vertexAttributesDatabase[index];
+	return true;
+}
+
 void Graphics::intersectScissor(const Rect &rect)
 {
 	Rect currect = states.back().scissorRect;
@@ -2022,6 +2047,17 @@ void Graphics::flushBatchedDraws()
 	VertexAttributes attributes;
 	BufferBindings buffers;
 
+	VertexAttributesID attributesID = sbstate.attributesIDs[(int)sbstate.formats[0]][(int)sbstate.formats[1]];
+
+	if (!findVertexAttributes(attributesID, attributes))
+	{
+		for (int i = 0; i < 2; i++)
+			attributes.setCommonFormat(sbstate.formats[i], (uint8)i);
+		
+		attributesID = registerVertexAttributes(attributes);
+		sbstate.attributesIDs[(int)sbstate.formats[0]][(int)sbstate.formats[1]] = attributesID;
+	}
+
 	size_t usedsizes[3] = {0, 0, 0};
 
 	for (int i = 0; i < 2; i++)
@@ -2029,8 +2065,6 @@ void Graphics::flushBatchedDraws()
 		if (sbstate.formats[i] == CommonFormat::NONE)
 			continue;
 
-		attributes.setCommonFormat(sbstate.formats[i], (uint8) i);
-
 		usedsizes[i] = getFormatStride(sbstate.formats[i]) * sbstate.vertexCount;
 
 		size_t offset = sbstate.vb[i]->unmap(usedsizes[i]);
@@ -2053,7 +2087,7 @@ void Graphics::flushBatchedDraws()
 	{
 		usedsizes[2] = sizeof(uint16) * sbstate.indexCount;
 
-		DrawIndexedCommand cmd(&attributes, &buffers, sbstate.indexBuffer);
+		DrawIndexedCommand cmd(attributesID, &buffers, sbstate.indexBuffer);
 		cmd.primitiveType = sbstate.primitiveMode;
 		cmd.indexCount = sbstate.indexCount;
 		cmd.indexType = INDEX_UINT16;
@@ -2065,7 +2099,7 @@ void Graphics::flushBatchedDraws()
 	}
 	else
 	{
-		DrawCommand cmd(&attributes, &buffers);
+		DrawCommand cmd(attributesID, &buffers);
 		cmd.primitiveType = sbstate.primitiveMode;
 		cmd.vertexStart = 0;
 		cmd.vertexCount = sbstate.vertexCount;
@@ -2156,10 +2190,8 @@ void Graphics::drawFromShader(PrimitiveType primtype, int vertexcount, int insta
 
 	Shader::current->validateDrawState(primtype, maintexture);
 
-	VertexAttributes attributes;
 	BufferBindings buffers;
-
-	DrawCommand cmd(&attributes, &buffers);
+	DrawCommand cmd(noAttributesID, &buffers);
 
 	cmd.primitiveType = primtype;
 	cmd.vertexCount = vertexcount;
@@ -2190,10 +2222,8 @@ void Graphics::drawFromShader(Buffer *indexbuffer, int indexcount, int instancec
 
 	Shader::current->validateDrawState(PRIMITIVE_TRIANGLES, maintexture);
 
-	VertexAttributes attributes;
 	BufferBindings buffers;
-
-	DrawIndexedCommand cmd(&attributes, &buffers, indexbuffer);
+	DrawIndexedCommand cmd(noAttributesID, &buffers, indexbuffer);
 
 	cmd.primitiveType = PRIMITIVE_TRIANGLES;
 	cmd.indexCount = indexcount;
@@ -2221,10 +2251,8 @@ void Graphics::drawFromShaderIndirect(PrimitiveType primtype, Buffer *indirectar
 
 	Shader::current->validateDrawState(primtype, maintexture);
 
-	VertexAttributes attributes;
 	BufferBindings buffers;
-
-	DrawCommand cmd(&attributes, &buffers);
+	DrawCommand cmd(noAttributesID, &buffers);
 
 	cmd.primitiveType = primtype;
 	cmd.indirectBuffer = indirectargs;
@@ -2248,10 +2276,8 @@ void Graphics::drawFromShaderIndirect(Buffer *indexbuffer, Buffer *indirectargs,
 
 	Shader::current->validateDrawState(PRIMITIVE_TRIANGLES, maintexture);
 
-	VertexAttributes attributes;
 	BufferBindings buffers;
-
-	DrawIndexedCommand cmd(&attributes, &buffers, indexbuffer);
+	DrawIndexedCommand cmd(noAttributesID, &buffers, indexbuffer);
 
 	cmd.primitiveType = PRIMITIVE_TRIANGLES;
 	cmd.indexType = getIndexDataType(indexbuffer->getDataMember(0).decl.format);

+ 19 - 17
src/modules/graphics/Graphics.h

@@ -233,7 +233,7 @@ public:
 	{
 		PrimitiveType primitiveType = PRIMITIVE_TRIANGLES;
 
-		const VertexAttributes *attributes;
+		VertexAttributesID attributesID;
 		const BufferBindings *buffers;
 
 		int vertexStart = 0;
@@ -248,8 +248,8 @@ public:
 		// TODO: This should be moved out to a state transition API?
 		CullMode cullMode = CULL_NONE;
 
-		DrawCommand(const VertexAttributes *attribs, const BufferBindings *buffers)
-			: attributes(attribs)
+		DrawCommand(VertexAttributesID attributesID, const BufferBindings *buffers)
+			: attributesID(attributesID)
 			, buffers(buffers)
 		{}
 	};
@@ -258,7 +258,7 @@ public:
 	{
 		PrimitiveType primitiveType = PRIMITIVE_TRIANGLES;
 
-		const VertexAttributes *attributes;
+		VertexAttributesID attributesID;
 		const BufferBindings *buffers;
 
 		int indexCount = 0;
@@ -276,8 +276,8 @@ public:
 		// TODO: This should be moved out to a state transition API?
 		CullMode cullMode = CULL_NONE;
 
-		DrawIndexedCommand(const VertexAttributes *attribs, const BufferBindings *buffers, Resource *indexbuffer)
-			: attributes(attribs)
+		DrawIndexedCommand(VertexAttributesID attributesID, const BufferBindings *buffers, Resource *indexbuffer)
+			: attributesID(attributesID)
 			, buffers(buffers)
 			, indexBuffer(indexbuffer)
 		{}
@@ -877,7 +877,7 @@ public:
 
 	virtual void draw(const DrawCommand &cmd) = 0;
 	virtual void draw(const DrawIndexedCommand &cmd) = 0;
-	virtual void drawQuads(int start, int count, const VertexAttributes &attributes, const BufferBindings &buffers, Texture *texture) = 0;
+	virtual void drawQuads(int start, int count, VertexAttributesID attributesID, const BufferBindings &buffers, Texture *texture) = 0;
 
 	void flushBatchedDraws();
 	BatchedVertexData requestBatchedDraw(const BatchedDrawCommand &command);
@@ -894,6 +894,9 @@ public:
 
 	void validateIndirectArgsBuffer(IndirectArgsType argstype, Buffer *indirectargs, int argsindex);
 
+	VertexAttributesID registerVertexAttributes(const VertexAttributes &attributes);
+	bool findVertexAttributes(VertexAttributesID id, VertexAttributes &attributes);
+
 	template <typename T>
 	T *getScratchBuffer(size_t count)
 	{
@@ -964,27 +967,22 @@ protected:
 
 	struct BatchedDrawState
 	{
-		StreamBuffer *vb[2];
+		StreamBuffer *vb[2] = {};
 		StreamBuffer *indexBuffer = nullptr;
 
 		PrimitiveType primitiveMode = PRIMITIVE_TRIANGLES;
-		CommonFormat formats[2];
+		CommonFormat formats[2] = {};
 		StrongRef<Texture> texture;
 		Shader::StandardShader standardShaderType = Shader::STANDARD_DEFAULT;
 		int vertexCount = 0;
 		int indexCount = 0;
 
-		StreamBuffer::MapInfo vbMap[2];
+		VertexAttributesID attributesIDs[(int)CommonFormat::COUNT][(int)CommonFormat::COUNT] = {};
+
+		StreamBuffer::MapInfo vbMap[2] = {};
 		StreamBuffer::MapInfo indexBufferMap = StreamBuffer::MapInfo();
 
 		bool flushing = false;
-
-		BatchedDrawState()
-		{
-			vb[0] = vb[1] = nullptr;
-			formats[0] = formats[1] = CommonFormat::NONE;
-			vbMap[0] = vbMap[1] = StreamBuffer::MapInfo();
-		}
 	};
 
 	struct TemporaryBuffer
@@ -1107,6 +1105,10 @@ private:
 
 	std::unordered_map<std::string, ShaderStage *> cachedShaderStages[SHADERSTAGE_MAX_ENUM];
 
+	std::vector<VertexAttributes> vertexAttributesDatabase;
+
+	VertexAttributesID noAttributesID;
+
 }; // Graphics
 
 STRINGMAP_DECLARE(Renderer);

+ 154 - 32
src/modules/graphics/Mesh.cpp

@@ -115,9 +115,19 @@ Mesh::Mesh(const std::vector<Mesh::BufferAttribute> &attributes, PrimitiveType d
 
 		finalizeAttribute(attrib);
 
-		int attributeIndex = getAttachedAttributeIndex(attrib.name);
-		if (attributeIndex != i && attributeIndex != -1)
-			throw love::Exception("Duplicate vertex attribute name: %s", attrib.name.c_str());
+		if (attrib.bindingLocation >= 0)
+		{
+			int attributeIndex = getAttachedAttributeIndex(attrib.bindingLocation);
+			if (attributeIndex != i && attributeIndex != -1)
+				throw love::Exception("Duplicate vertex attribute binding location: %d", attrib.bindingLocation);
+		}
+
+		if (!attrib.name.empty())
+		{
+			int attributeIndex = getAttachedAttributeIndex(attrib.name);
+			if (attributeIndex != i && attributeIndex != -1)
+				throw love::Exception("Duplicate vertex attribute name: %s", attrib.name.c_str());
+		}
 
 		vertexCount = std::min(vertexCount, attrib.buffer->getArrayLength());
 	}
@@ -137,16 +147,28 @@ void Mesh::setupAttachedAttributes()
 	for (size_t i = 0; i < vertexFormat.size(); i++)
 	{
 		const std::string &name = vertexFormat[i].decl.name;
+		int bindingLocation = vertexFormat[i].decl.bindingLocation;
 
-		if (getAttachedAttributeIndex(name) != -1)
-			throw love::Exception("Duplicate vertex attribute name: %s", name.c_str());
+		if (bindingLocation >= 0)
+		{
+			if (getAttachedAttributeIndex(bindingLocation) != -1)
+				throw love::Exception("Duplicate vertex attribute binding location: %d", bindingLocation);
+		}
 
-		BuiltinVertexAttribute builtinattrib;
-		int builtinAttribIndex = -1;
-		if (getConstant(name.c_str(), builtinattrib))
-			builtinAttribIndex = (int)builtinattrib;
+		if (!name.empty())
+		{
+			if (getAttachedAttributeIndex(name) != -1)
+				throw love::Exception("Duplicate vertex attribute name: %s", name.c_str());
+		}
 
-		attachedAttributes.push_back({name, vertexBuffer, nullptr, name, (int) i, 0, STEP_PER_VERTEX, builtinAttribIndex, true});
+		if (bindingLocation < 0)
+		{
+			BuiltinVertexAttribute builtinattrib;
+			if (getConstant(name.c_str(), builtinattrib))
+				bindingLocation = (int)builtinattrib;
+		}
+
+		attachedAttributes.push_back({name, vertexBuffer, nullptr, name, bindingLocation, (int) i, 0, STEP_PER_VERTEX, bindingLocation, true});
 	}
 }
 
@@ -161,6 +183,17 @@ int Mesh::getAttachedAttributeIndex(const std::string &name) const
 	return -1;
 }
 
+int Mesh::getAttachedAttributeIndex(int bindingLocation) const
+{
+	for (int i = 0; i < (int)attachedAttributes.size(); i++)
+	{
+		if (attachedAttributes[i].bindingLocation == bindingLocation)
+			return i;
+	}
+
+	return -1;
+}
+
 void Mesh::finalizeAttribute(BufferAttribute &attrib) const
 {
 	if ((attrib.buffer->getUsageFlags() & BUFFERUSAGEFLAG_VERTEX) == 0)
@@ -169,17 +202,33 @@ void Mesh::finalizeAttribute(BufferAttribute &attrib) const
 	if (attrib.startArrayIndex < 0 || attrib.startArrayIndex >= (int)attrib.buffer->getArrayLength())
 		throw love::Exception("Invalid start array index %d.", attrib.startArrayIndex + 1);
 
-	int indexInBuffer = attrib.buffer->getDataMemberIndex(attrib.nameInBuffer);
-	if (indexInBuffer < 0)
-		throw love::Exception("Buffer does not have a vertex attribute with name '%s'.", attrib.nameInBuffer.c_str());
-
-	BuiltinVertexAttribute builtinattrib;
-	if (getConstant(attrib.name.c_str(), builtinattrib))
-		attrib.builtinAttributeIndex = (int)builtinattrib;
+	if (attrib.bindingLocationInBuffer >= 0)
+	{
+		int indexInBuffer = attrib.buffer->getDataMemberIndex(attrib.bindingLocationInBuffer);
+		if (indexInBuffer < 0)
+			throw love::Exception("Buffer does not have a vertex attribute with binding location %d.", attrib.bindingLocationInBuffer);
+		attrib.indexInBuffer = indexInBuffer;
+	}
 	else
-		attrib.builtinAttributeIndex = -1;
+	{
+		int indexInBuffer = attrib.buffer->getDataMemberIndex(attrib.nameInBuffer);
+		if (indexInBuffer < 0)
+			throw love::Exception("Buffer does not have a vertex attribute with name '%s'.", attrib.nameInBuffer.c_str());
+		attrib.indexInBuffer = indexInBuffer;
+	}
+
+	if (attrib.bindingLocation < 0)
+		attrib.bindingLocation = attrib.buffer->getDataMember(attrib.indexInBuffer).decl.bindingLocation;
+
+	if (attrib.bindingLocation < 0)
+	{
+		BuiltinVertexAttribute builtinattrib;
+		if (getConstant(attrib.name.c_str(), builtinattrib))
+			attrib.bindingLocation = (int)builtinattrib;
+	}
 
-	attrib.indexInBuffer = indexInBuffer;
+	if (attrib.bindingLocation >= VertexAttributes::MAX || (attrib.bindingLocation < 0 && attrib.name.empty()))
+		throw love::Exception("Vertex attributes must have a valid binding location value within [0, %d).", VertexAttributes::MAX);
 }
 
 void *Mesh::checkVertexDataOffset(size_t vertindex, size_t *byteoffset)
@@ -224,6 +273,7 @@ void Mesh::setAttributeEnabled(const std::string &name, bool enable)
 		throw love::Exception("Mesh does not have an attached vertex attribute named '%s'", name.c_str());
 
 	attachedAttributes[index].enabled = enable;
+	attributesID.invalidate();
 }
 
 bool Mesh::isAttributeEnabled(const std::string &name) const
@@ -235,10 +285,29 @@ bool Mesh::isAttributeEnabled(const std::string &name) const
 	return attachedAttributes[index].enabled;
 }
 
+void Mesh::setAttributeEnabled(int bindingLocation, bool enable)
+{
+	int index = getAttachedAttributeIndex(bindingLocation);
+	if (index == -1)
+		throw love::Exception("Mesh does not have an attached vertex attribute with binding location %d", bindingLocation);
+
+	attachedAttributes[index].enabled = enable;
+	attributesID.invalidate();
+}
+
+bool Mesh::isAttributeEnabled(int bindingLocation) const
+{
+	int index = getAttachedAttributeIndex(bindingLocation);
+	if (index == -1)
+		throw love::Exception("Mesh does not have an attached vertex attribute with binding location %d", bindingLocation);
+
+	return attachedAttributes[index].enabled;
+}
+
 void Mesh::attachAttribute(const std::string &name, Buffer *buffer, Mesh *mesh, const std::string &attachname, int startindex, AttributeStep step)
 {
-	BufferAttribute oldattrib = {};
-	BufferAttribute newattrib = {};
+	BufferAttribute oldattrib;
+	BufferAttribute newattrib;
 
 	int oldindex = getAttachedAttributeIndex(name);
 	if (oldindex != -1)
@@ -257,13 +326,42 @@ void Mesh::attachAttribute(const std::string &name, Buffer *buffer, Mesh *mesh,
 
 	finalizeAttribute(newattrib);
 
-	if (newattrib.indexInBuffer < 0)
-		throw love::Exception("The specified vertex buffer does not have a vertex attribute named '%s'", attachname.c_str());
+	if (oldindex != -1)
+		attachedAttributes[oldindex] = newattrib;
+	else
+		attachedAttributes.push_back(newattrib);
+
+	attributesID.invalidate();
+}
+
+void Mesh::attachAttribute(int bindingLocation, Buffer *buffer, Mesh *mesh, int attachBindingLocation, int startindex, AttributeStep step)
+{
+	BufferAttribute oldattrib;
+	BufferAttribute newattrib;
+
+	int oldindex = getAttachedAttributeIndex(bindingLocation);
+	if (oldindex != -1)
+		oldattrib = attachedAttributes[oldindex];
+	else if (attachedAttributes.size() + 1 > VertexAttributes::MAX)
+		throw love::Exception("A maximum of %d attributes can be attached at once.", VertexAttributes::MAX);
+
+	newattrib.bindingLocation = bindingLocation;
+	newattrib.buffer = buffer;
+	newattrib.mesh = mesh;
+	newattrib.enabled = oldattrib.buffer.get() ? oldattrib.enabled : true;
+	newattrib.bindingLocationInBuffer = attachBindingLocation;
+	newattrib.indexInBuffer = -1;
+	newattrib.startArrayIndex = startindex;
+	newattrib.step = step;
+
+	finalizeAttribute(newattrib);
 
 	if (oldindex != -1)
 		attachedAttributes[oldindex] = newattrib;
 	else
 		attachedAttributes.push_back(newattrib);
+
+	attributesID.invalidate();
 }
 
 bool Mesh::detachAttribute(const std::string &name)
@@ -277,6 +375,22 @@ bool Mesh::detachAttribute(const std::string &name)
 	if (vertexBuffer.get() && vertexBuffer->getDataMemberIndex(name) != -1)
 		attachAttribute(name, vertexBuffer, nullptr, name);
 
+	attributesID.invalidate();
+	return true;
+}
+
+bool Mesh::detachAttribute(int bindingLocation)
+{
+	int index = getAttachedAttributeIndex(bindingLocation);
+	if (index == -1)
+		return false;
+
+	attachedAttributes.erase(attachedAttributes.begin() + index);
+
+	if (vertexBuffer.get() && vertexBuffer->getDataMemberIndex(bindingLocation) != -1)
+		attachAttribute(bindingLocation, vertexBuffer, nullptr, bindingLocation);
+
+	attributesID.invalidate();
 	return true;
 }
 
@@ -593,6 +707,8 @@ void Mesh::drawInternal(Graphics *gfx, const Matrix4 &m, int instancecount, Buff
 	if (Shader::current)
 		Shader::current->validateDrawState(primitiveType, texture);
 
+	bool attributesIDneedsupdate = !attributesID.isValid();
+
 	VertexAttributes attributes;
 	BufferBindings buffers;
 
@@ -604,14 +720,17 @@ void Mesh::drawInternal(Graphics *gfx, const Matrix4 &m, int instancecount, Buff
 			continue;
 
 		Buffer *buffer = attrib.buffer.get();
-		int attributeindex = attrib.builtinAttributeIndex;
+		int bindinglocation = attrib.bindingLocation;
 
-		// If the attribute is one of the LOVE-defined ones, use the constant
-		// attribute index for it, otherwise query the index from the shader.
-		if (attributeindex < 0 && Shader::current)
-			attributeindex = Shader::current->getVertexAttributeIndex(attrib.name);
+		// Query the index from the shader as a fallback to support old code that
+		// hasn't set a binding location.
+		if (bindinglocation < 0 && Shader::current)
+		{
+			bindinglocation = Shader::current->getVertexAttributeIndex(attrib.name);
+			attributesIDneedsupdate = true;
+		}
 
-		if (attributeindex >= 0)
+		if (bindinglocation >= 0)
 		{
 			if (attrib.mesh.get())
 				attrib.mesh->flush();
@@ -622,7 +741,7 @@ void Mesh::drawInternal(Graphics *gfx, const Matrix4 &m, int instancecount, Buff
 			uint16 stride = (uint16) buffer->getArrayStride();
 			size_t bufferoffset = (size_t) stride * attrib.startArrayIndex;
 
-			attributes.set(attributeindex, member.decl.format, offset, activebuffers);
+			attributes.set(bindinglocation, member.decl.format, offset, activebuffers);
 			attributes.setBufferLayout(activebuffers, stride, attrib.step);
 
 			// TODO: Ideally we want to reuse buffers with the same stride+step.
@@ -635,6 +754,9 @@ void Mesh::drawInternal(Graphics *gfx, const Matrix4 &m, int instancecount, Buff
 	if ((attributes.enableBits & ~(ATTRIBFLAG_TEXCOORD | ATTRIBFLAG_COLOR)) == 0)
 		throw love::Exception("Mesh must have an enabled VertexPosition or custom attribute to be drawn.");
 
+	if (attributesIDneedsupdate)
+		attributesID = gfx->registerVertexAttributes(attributes);
+
 	Graphics::TempTransform transform(gfx, m);
 
 	Buffer *indexbuffer = useIndexBuffer ? indexBuffer : nullptr;
@@ -660,7 +782,7 @@ void Mesh::drawInternal(Graphics *gfx, const Matrix4 &m, int instancecount, Buff
 		if (range.isValid())
 			r.intersect(range);
 
-		Graphics::DrawIndexedCommand cmd(&attributes, &buffers, indexbuffer);
+		Graphics::DrawIndexedCommand cmd(attributesID, &buffers, indexbuffer);
 
 		cmd.primitiveType = primitiveType;
 		cmd.indexType = indexDataType;
@@ -683,7 +805,7 @@ void Mesh::drawInternal(Graphics *gfx, const Matrix4 &m, int instancecount, Buff
 		if (range.isValid())
 			r.intersect(range);
 
-		Graphics::DrawCommand cmd(&attributes, &buffers);
+		Graphics::DrawCommand cmd(attributesID, &buffers);
 
 		cmd.primitiveType = primitiveType;
 		cmd.vertexStart = (int) r.getOffset();

+ 13 - 5
src/modules/graphics/Mesh.h

@@ -57,11 +57,12 @@ public:
 		StrongRef<Buffer> buffer;
 		StrongRef<Mesh> mesh;
 		std::string nameInBuffer;
-		int indexInBuffer;
-		int startArrayIndex;
-		AttributeStep step;
-		int builtinAttributeIndex;
-		bool enabled;
+		int bindingLocationInBuffer = -1;
+		int indexInBuffer = 0;
+		int startArrayIndex = 0;
+		AttributeStep step = STEP_PER_VERTEX;
+		int bindingLocation = -1;
+		bool enabled = false;
 	};
 
 	static love::Type type;
@@ -104,6 +105,8 @@ public:
 	 **/
 	void setAttributeEnabled(const std::string &name, bool enable);
 	bool isAttributeEnabled(const std::string &name) const;
+	void setAttributeEnabled(int bindingLocation, bool enable);
+	bool isAttributeEnabled(int bindingLocation) const;
 
 	/**
 	 * Attaches a vertex attribute from another vertex buffer to this Mesh. The
@@ -114,6 +117,8 @@ public:
 	 **/
 	void attachAttribute(const std::string &name, Buffer *buffer, Mesh *mesh, const std::string &attachname, int startindex = 0, AttributeStep step = STEP_PER_VERTEX);
 	bool detachAttribute(const std::string &name);
+	void attachAttribute(int bindingLocation, Buffer *buffer, Mesh *mesh, int attachBindingLocation, int startindex = 0, AttributeStep step = STEP_PER_VERTEX);
+	bool detachAttribute(int bindingLocation);
 	const std::vector<BufferAttribute> &getAttachedAttributes() const;
 
 	void *getVertexData() const;
@@ -188,6 +193,7 @@ private:
 
 	void setupAttachedAttributes();
 	int getAttachedAttributeIndex(const std::string &name) const;
+	int getAttachedAttributeIndex(int bindingLocation) const;
 	void finalizeAttribute(BufferAttribute &attrib) const;
 
 	void drawInternal(Graphics *gfx, const Matrix4 &m, int instancecount, Buffer *indirectargs, int argsindex);
@@ -196,6 +202,8 @@ private:
 
 	std::vector<BufferAttribute> attachedAttributes;
 
+	VertexAttributesID attributesID;
+
 	// Vertex buffer, for the vertex data.
 	StrongRef<Buffer> vertexBuffer;
 	uint8 *vertexData = nullptr;

+ 3 - 3
src/modules/graphics/ParticleSystem.cpp

@@ -93,7 +93,7 @@ ParticleSystem::ParticleSystem(Texture *texture, uint32 size)
 	, offset(float(texture->getWidth())*0.5f, float(texture->getHeight())*0.5f)
 	, defaultOffset(true)
 	, relativeRotation(false)
-	, vertexAttributes(CommonFormat::XYf_STf_RGBAub, 0)
+	, vertexAttributesID(Module::getInstance<Graphics>(Module::M_GRAPHICS)->registerVertexAttributes(VertexAttributes(CommonFormat::XYf_STf_RGBAub, 0)))
 	, buffer(nullptr)
 {
 	if (size == 0 || size > MAX_PARTICLES)
@@ -154,7 +154,7 @@ ParticleSystem::ParticleSystem(const ParticleSystem &p)
 	, colors(p.colors)
 	, quads(p.quads)
 	, relativeRotation(p.relativeRotation)
-	, vertexAttributes(p.vertexAttributes)
+	, vertexAttributesID(p.vertexAttributesID)
 	, buffer(nullptr)
 {
 	setBufferSize(maxParticles);
@@ -1087,7 +1087,7 @@ void ParticleSystem::draw(Graphics *gfx, const Matrix4 &m)
 	vertexbuffers.set(0, buffer, 0);
 
 	Texture *tex = gfx->getTextureOrDefaultForActiveShader(texture);
-	gfx->drawQuads(0, pCount, vertexAttributes, vertexbuffers, tex);
+	gfx->drawQuads(0, pCount, vertexAttributesID, vertexbuffers, tex);
 }
 
 bool ParticleSystem::getConstant(const char *in, AreaSpreadDistribution &out)

+ 1 - 1
src/modules/graphics/ParticleSystem.h

@@ -673,7 +673,7 @@ private:
 
 	bool relativeRotation;
 
-	const VertexAttributes vertexAttributes;
+	VertexAttributesID vertexAttributesID;
 	Buffer *buffer;
 
 	static StringMap<AreaSpreadDistribution, DISTRIBUTION_MAX_ENUM>::Entry distributionsEntries[];

+ 27 - 0
src/modules/graphics/Shader.cpp

@@ -650,6 +650,22 @@ Shader::Shader(StrongRef<ShaderStage> _stages[], const CompileOptions &options)
 	if (!validateInternal(_stages, err, reflection))
 		throw love::Exception("%s", err.c_str());
 
+	std::vector<std::string> unsetVertexInputLocations;
+
+	for (const auto &kvp : reflection.vertexInputs)
+	{
+		if (kvp.second < 0)
+			unsetVertexInputLocations.push_back(kvp.first);
+	}
+
+	if (!unsetVertexInputLocations.empty())
+	{
+		std::string str = unsetVertexInputLocations[0];
+		for (size_t i = 1; i < unsetVertexInputLocations.size(); i++)
+			str += ", " + unsetVertexInputLocations[i];
+		unsetVertexInputLocationsString = str;
+	}
+
 	activeTextures.resize(reflection.textureCount);
 	activeBuffers.resize(reflection.bufferCount);
 
@@ -1150,6 +1166,17 @@ bool Shader::validateInternal(StrongRef<ShaderStage> stages[], std::string &err,
 		}
 	}
 
+	for (int i = 0; i < program.getNumPipeInputs(); i++)
+	{
+		const glslang::TObjectReflection &info = program.getPipeInput(i);
+
+		int location = info.layoutLocation();
+		if (location == glslang::TQualifier::layoutLocationEnd)
+			location = -1;
+
+		reflection.vertexInputs[info.name] = location;
+	}
+
 	reflection.textureCount = 0;
 	reflection.bufferCount = 0;
 

+ 6 - 0
src/modules/graphics/Shader.h

@@ -269,6 +269,8 @@ public:
 	bool isUsingDeprecatedTextureFunctions() const;
 	bool isUsingDeprecatedTextureUniform() const;
 
+	const std::string& getUnsetVertexInputLocationsString() const { return unsetVertexInputLocationsString; }
+
 	static SourceInfo getSourceInfo(const std::string &src);
 	static std::string createShaderStageCode(Graphics *gfx, ShaderStageType stage, const std::string &code, const CompileOptions &options, const SourceInfo &info, bool gles, bool checksystemfeatures);
 
@@ -289,6 +291,8 @@ protected:
 
 	struct Reflection
 	{
+		std::map<std::string, int> vertexInputs;
+
 		std::map<std::string, UniformInfo> texelBuffers;
 		std::map<std::string, UniformInfo> storageBuffers;
 		std::map<std::string, UniformInfo> sampledTextures;
@@ -332,6 +336,8 @@ protected:
 
 	std::string debugName;
 
+	std::string unsetVertexInputLocationsString;
+
 }; // Shader
 
 } // graphics

+ 22 - 11
src/modules/graphics/SpriteBatch.cpp

@@ -46,6 +46,7 @@ SpriteBatch::SpriteBatch(Graphics *gfx, Texture *texture, int size, BufferDataUs
 	, next(0)
 	, color(255, 255, 255, 255)
 	, colorf(1.0f, 1.0f, 1.0f, 1.0f)
+	, attributesID()
 	, array_buf(nullptr)
 	, vertex_data(nullptr)
 	, modified_sprites()
@@ -134,7 +135,7 @@ int SpriteBatch::addLayer(int layer, const Matrix4 &m, int index)
 int SpriteBatch::addLayer(int layer, Quad *quad, const Matrix4 &m, int index)
 {
 	if (vertex_format != CommonFormat::XYf_STPf_RGBAub)
-		throw love::Exception("addLayer can only be called on a SpriteBatch that uses an Array Texture!");
+		throw love::Exception("addLayer can only be called on a SpriteBatch that uses an Array Texture.");
 
 	if (index < -1 || index >= size)
 		throw love::Exception("Invalid sprite index: %d", index + 1);
@@ -284,14 +285,16 @@ void SpriteBatch::attachAttribute(const std::string &name, Buffer *buffer, Mesh
 
 	newattrib.buffer = buffer;
 	newattrib.mesh = mesh;
+	newattrib.bindingIndex = buffer->getDataMember(newattrib.index).decl.bindingLocation;
 
 	BuiltinVertexAttribute builtinattrib;
-	if (getConstant(name.c_str(), builtinattrib))
-		newattrib.builtinAttributeIndex = (int)builtinattrib;
-	else
-		newattrib.builtinAttributeIndex = -1;
+	if (newattrib.bindingIndex < 0 && getConstant(name.c_str(), builtinattrib))
+		newattrib.bindingIndex = (int)builtinattrib;
 
 	attached_attributes[name] = newattrib;
+
+	// Invalidate attributes ID.
+	attributesID = VertexAttributesID();
 }
 
 void SpriteBatch::setDrawRange(int start, int count)
@@ -342,6 +345,8 @@ void SpriteBatch::draw(Graphics *gfx, const Matrix4 &m)
 
 	flush(); // Upload any modified sprite data to the GPU.
 
+	bool attributesIDneedsupdate = !attributesID.isValid();
+
 	VertexAttributes attributes;
 	BufferBindings buffers;
 
@@ -361,14 +366,17 @@ void SpriteBatch::draw(Graphics *gfx, const Matrix4 &m)
 		if (buffer->getArrayLength() < (size_t) next * 4)
 			throw love::Exception("Buffer with attribute '%s' attached to this SpriteBatch has too few vertices", it.first.c_str());
 
-		int attributeindex = it.second.builtinAttributeIndex;
+		int bindingindex = it.second.bindingIndex;
 
 		// If the attribute is one of the LOVE-defined ones, use the constant
 		// attribute index for it, otherwise query the index from the shader.
-		if (attributeindex < 0 && Shader::current)
-			attributeindex = Shader::current->getVertexAttributeIndex(it.first);
+		if (bindingindex < 0 && Shader::current)
+		{
+			bindingindex = Shader::current->getVertexAttributeIndex(it.first);
+			attributesIDneedsupdate = true;
+		}
 
-		if (attributeindex >= 0)
+		if (bindingindex >= 0)
 		{
 			if (it.second.mesh.get())
 				it.second.mesh->flush();
@@ -378,7 +386,7 @@ void SpriteBatch::draw(Graphics *gfx, const Matrix4 &m)
 			uint16 offset = (uint16) buffer->getMemberOffset(it.second.index);
 			uint16 stride = (uint16) buffer->getArrayStride();
 
-			attributes.set(attributeindex, member.decl.format, offset, activebuffers);
+			attributes.set(bindingindex, member.decl.format, offset, activebuffers);
 			attributes.setBufferLayout(activebuffers, stride);
 
 			// TODO: We should reuse buffer bindings with the same buffer+stride+step.
@@ -387,6 +395,9 @@ void SpriteBatch::draw(Graphics *gfx, const Matrix4 &m)
 		}
 	}
 
+	if (attributesIDneedsupdate)
+		attributesID = gfx->registerVertexAttributes(attributes);
+
 	Graphics::TempTransform transform(gfx, m);
 
 	int start = std::min(std::max(0, range_start), next - 1);
@@ -400,7 +411,7 @@ void SpriteBatch::draw(Graphics *gfx, const Matrix4 &m)
 	if (count > 0)
 	{
 		Texture *tex = gfx->getTextureOrDefaultForActiveShader(texture);
-		gfx->drawQuads(start, count, attributes, buffers, tex);
+		gfx->drawQuads(start, count, attributesID, buffers, tex);
 	}
 }
 

+ 3 - 1
src/modules/graphics/SpriteBatch.h

@@ -113,7 +113,7 @@ private:
 		StrongRef<Buffer> buffer;
 		StrongRef<Mesh> mesh;
 		int index;
-		int builtinAttributeIndex;
+		int bindingIndex;
 	};
 
 	/**
@@ -137,6 +137,8 @@ private:
 	CommonFormat vertex_format;
 	size_t vertex_stride;
 
+	VertexAttributesID attributesID;
+
 	StrongRef<love::graphics::Buffer> array_buf;
 	uint8 *vertex_data;
 

+ 3 - 2
src/modules/graphics/TextBatch.cpp

@@ -32,7 +32,7 @@ love::Type TextBatch::type("TextBatch", &Drawable::type);
 
 TextBatch::TextBatch(Font *font, const std::vector<love::font::ColoredString> &text)
 	: font(font)
-	, vertexAttributes(Font::vertexFormat, 0)
+	, vertexAttributesID(font->getVertexAttributesID())
 	, vertexData(nullptr)
 	, modifiedVertices()
 	, vertOffset(0)
@@ -218,6 +218,7 @@ void TextBatch::setFont(Font *f)
 	// Invalidate the texture cache ID since the font is different. We also have
 	// to re-upload all the vertices based on the new font's textures.
 	textureCacheID = (uint32) -1;
+	vertexAttributesID = font->getVertexAttributesID();
 	regenerateVertices();
 }
 
@@ -292,7 +293,7 @@ void TextBatch::draw(Graphics *gfx, const Matrix4 &m)
 	for (const Font::DrawCommand &cmd : drawCommands)
 	{
 		Texture *tex = gfx->getTextureOrDefaultForActiveShader(cmd.texture);
-		gfx->drawQuads(cmd.startvertex / 4, cmd.vertexcount / 4, vertexAttributes, vertexBuffers, tex);
+		gfx->drawQuads(cmd.startvertex / 4, cmd.vertexcount / 4, vertexAttributesID, vertexBuffers, tex);
 	}
 }
 

+ 1 - 1
src/modules/graphics/TextBatch.h

@@ -86,7 +86,7 @@ private:
 
 	StrongRef<Font> font;
 
-	VertexAttributes vertexAttributes;
+	VertexAttributesID vertexAttributesID;
 	BufferBindings vertexBuffers;
 
 	StrongRef<Buffer> vertexBuffer;

+ 2 - 2
src/modules/graphics/metal/Graphics.h

@@ -75,7 +75,7 @@ public:
 
 	void draw(const DrawCommand &cmd) override;
 	void draw(const DrawIndexedCommand &cmd) override;
-	void drawQuads(int start, int count, const VertexAttributes &attributes, const BufferBindings &buffers, love::graphics::Texture *texture) override;
+	void drawQuads(int start, int count, VertexAttributesID attributesID, const BufferBindings &buffers, love::graphics::Texture *texture) override;
 
 	void clear(OptionalColorD color, OptionalInt stencil, OptionalDouble depth) override;
 	void clear(const std::vector<OptionalColorD> &colors, OptionalInt stencil, OptionalDouble depth) override;
@@ -209,7 +209,7 @@ private:
 	void endPass(bool presenting);
 
 	id<MTLDepthStencilState> getCachedDepthStencilState(const DepthState &depth, const StencilState &stencil);
-	void applyRenderState(id<MTLRenderCommandEncoder> renderEncoder, const VertexAttributes &attributes);
+	void applyRenderState(id<MTLRenderCommandEncoder> renderEncoder, VertexAttributesID attributesID);
 	bool applyShaderUniforms(id<MTLComputeCommandEncoder> encoder, love::graphics::Shader *shader);
 	void applyShaderUniforms(id<MTLRenderCommandEncoder> renderEncoder, love::graphics::Shader *shader, Texture *maintex);
 

+ 12 - 9
src/modules/graphics/metal/Graphics.mm

@@ -910,7 +910,7 @@ id<MTLDepthStencilState> Graphics::getCachedDepthStencilState(const DepthState &
 	return mtlstate;
 }
 
-void Graphics::applyRenderState(id<MTLRenderCommandEncoder> encoder, const VertexAttributes &attributes)
+void Graphics::applyRenderState(id<MTLRenderCommandEncoder> encoder, VertexAttributesID attributesID)
 {
 	const uint32 pipelineStateBits = STATEBIT_SHADER | STATEBIT_BLEND | STATEBIT_COLORMASK;
 
@@ -987,18 +987,18 @@ void Graphics::applyRenderState(id<MTLRenderCommandEncoder> encoder, const Verte
 		[encoder setCullMode:mode];
 	}
 
-	if ((dirtyState & pipelineStateBits) != 0 || !(attributes == lastRenderPipelineKey.vertexAttributes))
+	if ((dirtyState & pipelineStateBits) != 0 || attributesID != lastRenderPipelineKey.vertexAttributesID)
 	{
 		auto &key = lastRenderPipelineKey;
 
-		key.vertexAttributes = attributes;
+		key.vertexAttributesID = attributesID;
 
 		Shader *shader = (Shader *) Shader::current;
 		id<MTLRenderPipelineState> pipeline = nil;
 
 		if (shader)
 		{
-			key.blend = state.blend;
+			key.blendStateKey = state.blend.toKey();
 			key.colorChannelMask = state.colorMask;
 
 			pipeline = shader->getCachedRenderPipeline(this, key);
@@ -1233,7 +1233,7 @@ void Graphics::draw(const DrawCommand &cmd)
 		dirtyRenderState |= STATEBIT_CULLMODE;
 	}
 
-	applyRenderState(encoder, *cmd.attributes);
+	applyRenderState(encoder, cmd.attributesID);
 	applyShaderUniforms(encoder, Shader::current, cmd.texture);
 
 	setVertexBuffers(encoder, Shader::current, cmd.buffers, renderBindings);
@@ -1265,7 +1265,7 @@ void Graphics::draw(const DrawIndexedCommand &cmd)
 		dirtyRenderState |= STATEBIT_CULLMODE;
 	}
 
-	applyRenderState(encoder, *cmd.attributes);
+	applyRenderState(encoder, cmd.attributesID);
 	applyShaderUniforms(encoder, Shader::current, cmd.texture);
 
 	setVertexBuffers(encoder, Shader::current, cmd.buffers, renderBindings);
@@ -1317,7 +1317,7 @@ static inline void advanceVertexOffsets(const VertexAttributes &attributes, Buff
 	}
 }
 
-void Graphics::drawQuads(int start, int count, const VertexAttributes &attributes, const BufferBindings &buffers, love::graphics::Texture *texture)
+void Graphics::drawQuads(int start, int count, VertexAttributesID attributesID, const BufferBindings &buffers, love::graphics::Texture *texture)
 { @autoreleasepool {
 	const int MAX_VERTICES_PER_DRAW = LOVE_UINT16_MAX;
 	const int MAX_QUADS_PER_DRAW    = MAX_VERTICES_PER_DRAW / 4;
@@ -1330,7 +1330,7 @@ void Graphics::drawQuads(int start, int count, const VertexAttributes &attribute
 		dirtyRenderState |= STATEBIT_CULLMODE;
 	}
 
-	applyRenderState(encoder, attributes);
+	applyRenderState(encoder, attributesID);
 	applyShaderUniforms(encoder, Shader::current, texture);
 
 	id<MTLBuffer> ib = getMTLBuffer(quadIndexBuffer);
@@ -1361,6 +1361,9 @@ void Graphics::drawQuads(int start, int count, const VertexAttributes &attribute
 	}
 	else
 	{
+		VertexAttributes attributes;
+		findVertexAttributes(attributesID, attributes);
+
 		BufferBindings bufferscopy = buffers;
 		if (start > 0)
 			advanceVertexOffsets(attributes, bufferscopy, start * 4);
@@ -1490,7 +1493,7 @@ void Graphics::setRenderTargetsInternal(const RenderTargets &rts, int /*pixelw*/
 	}
 
 	lastRenderPipelineKey.depthStencilFormat = dsformat;
-	lastRenderPipelineKey.vertexAttributes = VertexAttributes();
+	lastRenderPipelineKey.vertexAttributesID = VertexAttributesID();
 
 	dirtyRenderState = STATEBIT_ALL;
 }}

+ 2 - 2
src/modules/graphics/metal/Shader.h

@@ -61,8 +61,8 @@ public:
 
 	struct RenderPipelineKey
 	{
-		VertexAttributes vertexAttributes;
-		BlendState blend;
+		VertexAttributesID vertexAttributesID;
+		uint32 blendStateKey;
 		uint64 colorRenderTargetFormats;
 		uint32 depthStencilFormat;
 		ColorChannelMask colorChannelMask;

+ 12 - 8
src/modules/graphics/metal/Shader.mm

@@ -882,15 +882,17 @@ id<MTLRenderPipelineState> Shader::getCachedRenderPipeline(graphics::Graphics *g
 		auto formatdesc = Metal::convertPixelFormat(device, format);
 		attachment.pixelFormat = formatdesc.format;
 
-		if (key.blend.enable && gfx->isPixelFormatSupported(format, PIXELFORMATUSAGEFLAGS_BLEND))
+		BlendState blend = BlendState::fromKey(key.blendStateKey);
+
+		if (blend.enable && gfx->isPixelFormatSupported(format, PIXELFORMATUSAGEFLAGS_BLEND))
 		{
 			attachment.blendingEnabled = YES;
-			attachment.sourceRGBBlendFactor = getMTLBlendFactor(key.blend.srcFactorRGB);
-			attachment.destinationRGBBlendFactor = getMTLBlendFactor(key.blend.dstFactorRGB);
-			attachment.rgbBlendOperation = getMTLBlendOperation(key.blend.operationRGB);
-			attachment.sourceAlphaBlendFactor = getMTLBlendFactor(key.blend.srcFactorA);
-			attachment.destinationAlphaBlendFactor = getMTLBlendFactor(key.blend.dstFactorA);
-			attachment.alphaBlendOperation = getMTLBlendOperation(key.blend.operationA);
+			attachment.sourceRGBBlendFactor = getMTLBlendFactor(blend.srcFactorRGB);
+			attachment.destinationRGBBlendFactor = getMTLBlendFactor(blend.dstFactorRGB);
+			attachment.rgbBlendOperation = getMTLBlendOperation(blend.operationRGB);
+			attachment.sourceAlphaBlendFactor = getMTLBlendFactor(blend.srcFactorA);
+			attachment.destinationAlphaBlendFactor = getMTLBlendFactor(blend.dstFactorA);
+			attachment.alphaBlendOperation = getMTLBlendOperation(blend.operationA);
 		}
 
 		MTLColorWriteMask writeMask = MTLColorWriteMaskNone;
@@ -923,8 +925,10 @@ id<MTLRenderPipelineState> Shader::getCachedRenderPipeline(graphics::Graphics *g
 		}
 	}
 
+	VertexAttributes attributes;
+	gfx->findVertexAttributes(key.vertexAttributesID, attributes);
+
 	MTLVertexDescriptor *vertdesc = [MTLVertexDescriptor vertexDescriptor];
-	const auto &attributes = key.vertexAttributes;
 
 	for (const auto &pair : this->attributes)
 	{

+ 12 - 3
src/modules/graphics/opengl/Graphics.cpp

@@ -569,8 +569,11 @@ bool Graphics::dispatch(love::graphics::Shader *s, love::graphics::Buffer *indir
 
 void Graphics::draw(const DrawCommand &cmd)
 {
+	VertexAttributes attributes;
+	findVertexAttributes(cmd.attributesID, attributes);
+
 	gl.prepareDraw(this);
-	gl.setVertexAttributes(*cmd.attributes, *cmd.buffers);
+	gl.setVertexAttributes(attributes, *cmd.buffers);
 	gl.bindTextureToUnit(cmd.texture, 0, false);
 	gl.setCullMode(cmd.cullMode);
 
@@ -591,8 +594,11 @@ void Graphics::draw(const DrawCommand &cmd)
 
 void Graphics::draw(const DrawIndexedCommand &cmd)
 {
+	VertexAttributes attributes;
+	findVertexAttributes(cmd.attributesID, attributes);
+
 	gl.prepareDraw(this);
-	gl.setVertexAttributes(*cmd.attributes, *cmd.buffers);
+	gl.setVertexAttributes(attributes, *cmd.buffers);
 	gl.bindTextureToUnit(cmd.texture, 0, false);
 	gl.setCullMode(cmd.cullMode);
 
@@ -640,11 +646,14 @@ static inline void advanceVertexOffsets(const VertexAttributes &attributes, Buff
 	}
 }
 
-void Graphics::drawQuads(int start, int count, const VertexAttributes &attributes, const BufferBindings &buffers, love::graphics::Texture *texture)
+void Graphics::drawQuads(int start, int count, VertexAttributesID attributesID, const BufferBindings &buffers, love::graphics::Texture *texture)
 {
 	const int MAX_VERTICES_PER_DRAW = LOVE_UINT16_MAX;
 	const int MAX_QUADS_PER_DRAW    = MAX_VERTICES_PER_DRAW / 4;
 
+	VertexAttributes attributes;
+	findVertexAttributes(attributesID, attributes);
+
 	gl.prepareDraw(this);
 	gl.bindTextureToUnit(texture, 0, false);
 	gl.setCullMode(CULL_NONE);

+ 1 - 1
src/modules/graphics/opengl/Graphics.h

@@ -71,7 +71,7 @@ public:
 
 	void draw(const DrawCommand &cmd) override;
 	void draw(const DrawIndexedCommand &cmd) override;
-	void drawQuads(int start, int count, const VertexAttributes &attributes, const BufferBindings &buffers, love::graphics::Texture *texture) override;
+	void drawQuads(int start, int count, VertexAttributesID attributesID, const BufferBindings &buffers, love::graphics::Texture *texture) override;
 
 	void clear(OptionalColorD color, OptionalInt stencil, OptionalDouble depth) override;
 	void clear(const std::vector<OptionalColorD> &colors, OptionalInt stencil, OptionalDouble depth) override;

+ 12 - 0
src/modules/graphics/vertex.h

@@ -214,6 +214,7 @@ enum class CommonFormat
 	XYf_STf_RGBAub,
 	XYf_STus_RGBAub,
 	XYf_STPf_RGBAub,
+	COUNT,
 };
 
 struct DataFormatInfo
@@ -379,6 +380,17 @@ struct VertexAttributes
 	bool operator == (const VertexAttributes &other) const;
 };
 
+struct VertexAttributesID
+{
+	int id = 0;
+
+	bool isValid() const { return id > 0; }
+	void invalidate() { id = 0; }
+
+	bool operator == (VertexAttributesID other) const { return other.id == id; }
+	bool operator != (VertexAttributesID other) const { return other.id != id; }
+};
+
 size_t getFormatStride(CommonFormat format);
 
 uint32 getFormatFlags(CommonFormat format);

+ 51 - 40
src/modules/graphics/vulkan/Graphics.cpp

@@ -867,7 +867,7 @@ Graphics::RendererInfo Graphics::getRendererInfo() const
 
 void Graphics::draw(const DrawCommand &cmd)
 {
-	prepareDraw(*cmd.attributes, *cmd.buffers, cmd.texture, cmd.primitiveType, cmd.cullMode);
+	prepareDraw(cmd.attributesID, *cmd.buffers, cmd.texture, cmd.primitiveType, cmd.cullMode);
 
 	if (cmd.indirectBuffer != nullptr)
 	{
@@ -893,7 +893,7 @@ void Graphics::draw(const DrawCommand &cmd)
 
 void Graphics::draw(const DrawIndexedCommand &cmd)
 {
-	prepareDraw(*cmd.attributes, *cmd.buffers, cmd.texture, cmd.primitiveType, cmd.cullMode);
+	prepareDraw(cmd.attributesID, *cmd.buffers, cmd.texture, cmd.primitiveType, cmd.cullMode);
 
 	vkCmdBindIndexBuffer(
 		commandBuffers.at(currentFrame),
@@ -924,12 +924,12 @@ void Graphics::draw(const DrawIndexedCommand &cmd)
 	drawCalls++;
 }
 
-void Graphics::drawQuads(int start, int count, const VertexAttributes &attributes, const BufferBindings &buffers, graphics::Texture *texture)
+void Graphics::drawQuads(int start, int count, VertexAttributesID attributesID, const BufferBindings &buffers, graphics::Texture *texture)
 {
 	const int MAX_VERTICES_PER_DRAW = LOVE_UINT16_MAX;
 	const int MAX_QUADS_PER_DRAW = MAX_VERTICES_PER_DRAW / 4;
 
-	prepareDraw(attributes, buffers, texture, PRIMITIVE_TRIANGLES, CULL_NONE);
+	prepareDraw(attributesID, buffers, texture, PRIMITIVE_TRIANGLES, CULL_NONE);
 
 	vkCmdBindIndexBuffer(
 		commandBuffers.at(currentFrame),
@@ -2328,7 +2328,7 @@ void Graphics::createVulkanVertexFormat(
 	}
 }
 
-void Graphics::prepareDraw(const VertexAttributes &attributes, const BufferBindings &buffers, graphics::Texture *texture, PrimitiveType primitiveType, CullMode cullmode)
+void Graphics::prepareDraw(VertexAttributesID attributesID, const BufferBindings &buffers, graphics::Texture *texture, PrimitiveType primitiveType, CullMode cullmode)
 {
 	if (!renderPassState.active)
 		startRenderPass();
@@ -2337,31 +2337,37 @@ void Graphics::prepareDraw(const VertexAttributes &attributes, const BufferBindi
 
 	usedShadersInFrame.insert(s);
 
-	GraphicsPipelineConfiguration configuration{};
+	GraphicsPipelineConfigurationFull configuration{};
 
-	configuration.renderPass = renderPassState.beginInfo.renderPass;
-	configuration.vertexAttributes = attributes;
-	configuration.wireFrame = states.back().wireframe;
-	configuration.blendState = states.back().blend;
-	configuration.colorChannelMask = states.back().colorMask;
-	configuration.msaaSamples = renderPassState.msaa;
-	configuration.numColorAttachments = renderPassState.numColorAttachments;
-	configuration.packedColorAttachmentFormats = renderPassState.packedColorAttachmentFormats;
-	configuration.primitiveType = primitiveType;
+	configuration.core.renderPass = renderPassState.beginInfo.renderPass;
+	configuration.core.attributesID = attributesID;
+	configuration.core.wireFrame = states.back().wireframe;
+	configuration.core.blendStateKey = states.back().blend.toKey();
+	configuration.core.colorChannelMask = states.back().colorMask;
+	configuration.core.msaaSamples = renderPassState.msaa;
+	configuration.core.numColorAttachments = renderPassState.numColorAttachments;
+	configuration.core.packedColorAttachmentFormats = renderPassState.packedColorAttachmentFormats;
+	configuration.core.primitiveType = primitiveType;
+
+	VkPipeline pipeline = VK_NULL_HANDLE;
 
 	if (optionalDeviceExtensions.extendedDynamicState)
+	{
 		vkCmdSetCullModeEXT(commandBuffers.at(currentFrame), Vulkan::getCullMode(cullmode));
+		pipeline = s->getCachedGraphicsPipeline(this, configuration.core);
+	}
 	else
 	{
-		configuration.dynamicState.winding = states.back().winding;
-		configuration.dynamicState.depthState.compare = states.back().depthTest;
-		configuration.dynamicState.depthState.write = states.back().depthWrite;
-		configuration.dynamicState.stencilAction = states.back().stencil.action;
-		configuration.dynamicState.stencilCompare = states.back().stencil.compare;
-		configuration.dynamicState.cullmode = cullmode;
+		configuration.noDynamicState.winding = states.back().winding;
+		configuration.noDynamicState.depthState.compare = states.back().depthTest;
+		configuration.noDynamicState.depthState.write = states.back().depthWrite;
+		configuration.noDynamicState.stencilAction = states.back().stencil.action;
+		configuration.noDynamicState.stencilCompare = states.back().stencil.compare;
+		configuration.noDynamicState.cullmode = cullmode;
+
+		pipeline = s->getCachedGraphicsPipeline(this, configuration);
 	}
 
-	VkPipeline pipeline = s->getCachedGraphicsPipeline(this, configuration);
 	if (pipeline != renderPassState.pipeline)
 	{
 		vkCmdBindPipeline(commandBuffers.at(currentFrame), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
@@ -2696,7 +2702,7 @@ VkSampler Graphics::getCachedSampler(const SamplerState &samplerState)
 	}
 }
 
-VkPipeline Graphics::createGraphicsPipeline(Shader *shader, const GraphicsPipelineConfiguration &configuration)
+VkPipeline Graphics::createGraphicsPipeline(Shader *shader, const GraphicsPipelineConfigurationCore &configuration, const GraphicsPipelineConfigurationNoDynamicState *noDynamicStateConfiguration)
 {
 	VkGraphicsPipelineCreateInfo pipelineInfo{};
 	pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
@@ -2706,7 +2712,10 @@ VkPipeline Graphics::createGraphicsPipeline(Shader *shader, const GraphicsPipeli
 	std::vector<VkVertexInputBindingDescription> bindingDescriptions;
 	std::vector<VkVertexInputAttributeDescription> attributeDescriptions;
 
-	createVulkanVertexFormat(shader, configuration.vertexAttributes, bindingDescriptions, attributeDescriptions);
+	VertexAttributes vertexAttributes;
+	findVertexAttributes(configuration.attributesID, vertexAttributes);
+
+	createVulkanVertexFormat(shader, vertexAttributes, bindingDescriptions, attributeDescriptions);
 
 	VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
 	vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
@@ -2733,8 +2742,8 @@ VkPipeline Graphics::createGraphicsPipeline(Shader *shader, const GraphicsPipeli
 	rasterizer.lineWidth = 1.0f;
 	if (!optionalDeviceExtensions.extendedDynamicState)
 	{
-		rasterizer.cullMode = Vulkan::getCullMode(configuration.dynamicState.cullmode);
-		rasterizer.frontFace = Vulkan::getFrontFace(configuration.dynamicState.winding);
+		rasterizer.cullMode = Vulkan::getCullMode(noDynamicStateConfiguration->cullmode);
+		rasterizer.frontFace = Vulkan::getFrontFace(noDynamicStateConfiguration->winding);
 	}
 
 	rasterizer.depthBiasEnable = VK_FALSE;
@@ -2752,8 +2761,8 @@ VkPipeline Graphics::createGraphicsPipeline(Shader *shader, const GraphicsPipeli
 	depthStencil.depthTestEnable = VK_TRUE;
 	if (!optionalDeviceExtensions.extendedDynamicState)
 	{
-		depthStencil.depthWriteEnable = Vulkan::getBool(configuration.dynamicState.depthState.write);
-		depthStencil.depthCompareOp = Vulkan::getCompareOp(configuration.dynamicState.depthState.compare);
+		depthStencil.depthWriteEnable = Vulkan::getBool(noDynamicStateConfiguration->depthState.write);
+		depthStencil.depthCompareOp = Vulkan::getCompareOp(noDynamicStateConfiguration->depthState.compare);
 	}
 	depthStencil.depthBoundsTestEnable = VK_FALSE;
 	depthStencil.minDepthBounds = 0.0f;
@@ -2764,31 +2773,33 @@ VkPipeline Graphics::createGraphicsPipeline(Shader *shader, const GraphicsPipeli
 	if (!optionalDeviceExtensions.extendedDynamicState)
 	{
 		depthStencil.front.failOp = VK_STENCIL_OP_KEEP;
-		depthStencil.front.passOp = Vulkan::getStencilOp(configuration.dynamicState.stencilAction);
+		depthStencil.front.passOp = Vulkan::getStencilOp(noDynamicStateConfiguration->stencilAction);
 		depthStencil.front.depthFailOp = VK_STENCIL_OP_KEEP;
-		depthStencil.front.compareOp = Vulkan::getCompareOp(getReversedCompareMode(configuration.dynamicState.stencilCompare));
+		depthStencil.front.compareOp = Vulkan::getCompareOp(getReversedCompareMode(noDynamicStateConfiguration->stencilCompare));
 
 		depthStencil.back.failOp = VK_STENCIL_OP_KEEP;
-		depthStencil.back.passOp = Vulkan::getStencilOp(configuration.dynamicState.stencilAction);
+		depthStencil.back.passOp = Vulkan::getStencilOp(noDynamicStateConfiguration->stencilAction);
 		depthStencil.back.depthFailOp = VK_STENCIL_OP_KEEP;
-		depthStencil.back.compareOp = Vulkan::getCompareOp(getReversedCompareMode(configuration.dynamicState.stencilCompare));
+		depthStencil.back.compareOp = Vulkan::getCompareOp(getReversedCompareMode(noDynamicStateConfiguration->stencilCompare));
 	}
 
 	pipelineInfo.pDepthStencilState = &depthStencil;
 
+	BlendState blendState = BlendState::fromKey(configuration.blendStateKey);
+
 	VkPipelineColorBlendAttachmentState colorBlendAttachment{};
 	colorBlendAttachment.colorWriteMask = Vulkan::getColorMask(configuration.colorChannelMask);
-	colorBlendAttachment.blendEnable = Vulkan::getBool(configuration.blendState.enable);
-	colorBlendAttachment.srcColorBlendFactor = Vulkan::getBlendFactor(configuration.blendState.srcFactorRGB);
-	colorBlendAttachment.dstColorBlendFactor = Vulkan::getBlendFactor(configuration.blendState.dstFactorRGB);
-	colorBlendAttachment.colorBlendOp = Vulkan::getBlendOp(configuration.blendState.operationRGB);
-	colorBlendAttachment.srcAlphaBlendFactor = Vulkan::getBlendFactor(configuration.blendState.srcFactorA);
-	colorBlendAttachment.dstAlphaBlendFactor = Vulkan::getBlendFactor(configuration.blendState.dstFactorA);
-	colorBlendAttachment.alphaBlendOp = Vulkan::getBlendOp(configuration.blendState.operationA);
+	colorBlendAttachment.blendEnable = Vulkan::getBool(blendState.enable);
+	colorBlendAttachment.srcColorBlendFactor = Vulkan::getBlendFactor(blendState.srcFactorRGB);
+	colorBlendAttachment.dstColorBlendFactor = Vulkan::getBlendFactor(blendState.dstFactorRGB);
+	colorBlendAttachment.colorBlendOp = Vulkan::getBlendOp(blendState.operationRGB);
+	colorBlendAttachment.srcAlphaBlendFactor = Vulkan::getBlendFactor(blendState.srcFactorA);
+	colorBlendAttachment.dstAlphaBlendFactor = Vulkan::getBlendFactor(blendState.dstFactorA);
+	colorBlendAttachment.alphaBlendOp = Vulkan::getBlendOp(blendState.operationA);
 
 	std::vector<VkPipelineColorBlendAttachmentState> colorBlendAttachments(configuration.numColorAttachments, colorBlendAttachment);
 
-	if (configuration.blendState.enable)
+	if (blendState.enable)
 	{
 		for (uint32 i = 0; i < configuration.numColorAttachments; i++)
 		{

+ 3 - 3
src/modules/graphics/vulkan/Graphics.h

@@ -256,7 +256,7 @@ public:
 	RendererInfo getRendererInfo() const override;
 	void draw(const DrawCommand &cmd) override;
 	void draw(const DrawIndexedCommand &cmd) override;
-	void drawQuads(int start, int count, const VertexAttributes &attributes, const BufferBindings &buffers, graphics::Texture *texture) override;
+	void drawQuads(int start, int count, VertexAttributesID attributesID, const BufferBindings &buffers, graphics::Texture *texture) override;
 
 	// internal functions.
 
@@ -276,7 +276,7 @@ public:
 	int getVsync() const;
 	void mapLocalUniformData(void *data, size_t size, VkDescriptorBufferInfo &bufferInfo);
 
-	VkPipeline createGraphicsPipeline(Shader *shader, const GraphicsPipelineConfiguration &configuration);
+	VkPipeline createGraphicsPipeline(Shader *shader, const GraphicsPipelineConfigurationCore &configuration, const GraphicsPipelineConfigurationNoDynamicState *noDynamicStateConfiguration);
 
 	uint32 getDeviceApiVersion() const { return deviceApiVersion; }
 
@@ -332,7 +332,7 @@ private:
 		std::vector<VkVertexInputBindingDescription> &bindingDescriptions, 
 		std::vector<VkVertexInputAttributeDescription> &attributeDescriptions);
 	void prepareDraw(
-		const VertexAttributes &attributes,
+		VertexAttributesID attributesID,
 		const BufferBindings &buffers, graphics::Texture *texture,
 		PrimitiveType, CullMode);
 	void setRenderPass(const RenderTargets &rts, int pixelw, int pixelh, bool hasSRGBtexture);

+ 35 - 23
src/modules/graphics/vulkan/Shader.cpp

@@ -201,7 +201,8 @@ void Shader::unloadVolatile()
 		return;
 
 	vgfx->queueCleanUp([shaderModules = std::move(shaderModules), device = device, descriptorSetLayout = descriptorSetLayout, pipelineLayout = pipelineLayout,
-		descriptorPools = descriptorPools, computePipeline = computePipeline, graphicsPipelines = std::move(graphicsPipelines)](){
+		descriptorPools = descriptorPools, computePipeline = computePipeline,
+		graphicsPipelinesCore = std::move(graphicsPipelinesDynamicState), graphicsPipelinesFull = std::move(graphicsPipelinesNoDynamicState)]() {
 		for (const auto &pools : descriptorPools)
 		{
 			for (const auto pool : pools)
@@ -213,7 +214,9 @@ void Shader::unloadVolatile()
 		vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
 		if (computePipeline != VK_NULL_HANDLE)
 			vkDestroyPipeline(device, computePipeline, nullptr);
-		for (const auto& kvp : graphicsPipelines)
+		for (const auto &kvp : graphicsPipelinesCore)
+			vkDestroyPipeline(device, kvp.second, nullptr);
+		for (const auto &kvp : graphicsPipelinesFull)
 			vkDestroyPipeline(device, kvp.second, nullptr);
 	});
 
@@ -594,6 +597,7 @@ void Shader::compileShaders()
 
 	BindingMapper bindingMapper(spv::DecorationBinding);
 	BindingMapper ioLocationMapper(spv::DecorationLocation);
+	BindingMapper vertexInputLocationMapper(spv::DecorationLocation);
 
 	for (int i = 0; i < SHADERSTAGE_MAX_ENUM; i++)
 	{
@@ -712,25 +716,21 @@ void Shader::compileShaders()
 
 		if (shaderStage == SHADERSTAGE_VERTEX)
 		{
-			int nextAttributeIndex = ATTRIB_MAX_ENUM;
-
-			// Don't skip unused inputs, vulkan still needs to have valid
-			// bindings for them.
+			// Use the mapper on known used inputs first, so their bindings get
+			// put into the map without being changed.
 			for (const auto &r : shaderResources.stage_inputs)
 			{
-				int index;
-
-				BuiltinVertexAttribute builtinAttribute;
-				if (graphics::getConstant(r.name.c_str(), builtinAttribute))
-					index = (int)builtinAttribute;
-				else
-					index = nextAttributeIndex++;
-
-				uint32_t locationOffset;
-				if (!comp.get_binary_offset_for_decoration(r.id, spv::DecorationLocation, locationOffset))
-					throw love::Exception("could not get binary offset for vertex attribute %s location", r.name.c_str());
+				auto it = reflection.vertexInputs.find(r.name);
+				if (it != reflection.vertexInputs.end() && it->second >= 0)
+					vertexInputLocationMapper(comp, spirv, r.name, 1, r.id);
+			}
 
-				spirv[locationOffset] = (uint32_t)index;
+			for (const auto &r : shaderResources.stage_inputs)
+			{
+				// Don't skip unused inputs, vulkan still needs to have valid
+				// bindings for them. This will also avoid shuffling intentional
+				// used bindings because of the earlier loop.
+				int index = (int)vertexInputLocationMapper(comp, spirv, r.name, 1, r.id);
 
 				DataBaseType basetype = DATA_BASETYPE_FLOAT;
 
@@ -1203,14 +1203,26 @@ VkDescriptorSet Shader::allocateDescriptorSet()
 	}
 }
 
-VkPipeline Shader::getCachedGraphicsPipeline(Graphics *vgfx, const GraphicsPipelineConfiguration &configuration)
+VkPipeline Shader::getCachedGraphicsPipeline(Graphics *vgfx, const GraphicsPipelineConfigurationCore &configuration)
+{
+	auto it = graphicsPipelinesDynamicState.find(configuration);
+	if (it != graphicsPipelinesDynamicState.end())
+		return it->second;
+
+	VkPipeline pipeline = vgfx->createGraphicsPipeline(this, configuration, nullptr);
+	graphicsPipelinesDynamicState.insert({ configuration, pipeline });
+	
+	return pipeline;
+}
+
+VkPipeline Shader::getCachedGraphicsPipeline(Graphics *vgfx, const GraphicsPipelineConfigurationFull &configuration)
 {
-	auto it = graphicsPipelines.find(configuration);
-	if (it != graphicsPipelines.end())
+	auto it = graphicsPipelinesNoDynamicState.find(configuration);
+	if (it != graphicsPipelinesNoDynamicState.end())
 		return it->second;
 
-	VkPipeline pipeline = vgfx->createGraphicsPipeline(this, configuration);
-	graphicsPipelines.insert({ configuration, pipeline });
+	VkPipeline pipeline = vgfx->createGraphicsPipeline(this, configuration.core, &configuration.noDynamicState);
+	graphicsPipelinesNoDynamicState.insert({ configuration, pipeline });
 	
 	return pipeline;
 }

+ 46 - 20
src/modules/graphics/vulkan/Shader.h

@@ -46,43 +46,67 @@ namespace graphics
 namespace vulkan
 {
 
-struct GraphicsPipelineConfiguration
+struct GraphicsPipelineConfigurationCore
 {
 	VkRenderPass renderPass;
-	VertexAttributes vertexAttributes;
+	VertexAttributesID attributesID;
 	bool wireFrame;
-	BlendState blendState;
+	uint32 blendStateKey;
 	ColorChannelMask colorChannelMask;
 	VkSampleCountFlagBits msaaSamples;
 	uint32_t numColorAttachments;
 	PrimitiveType primitiveType;
 	uint64 packedColorAttachmentFormats;
 
-	struct DynamicState
+	GraphicsPipelineConfigurationCore()
 	{
-		CullMode cullmode = CULL_NONE;
-		Winding winding = WINDING_MAX_ENUM;
-		StencilAction stencilAction = STENCIL_MAX_ENUM;
-		CompareMode stencilCompare = COMPARE_MAX_ENUM;
-		DepthState depthState{};
-	} dynamicState;
-
-	GraphicsPipelineConfiguration()
+		memset(this, 0, sizeof(GraphicsPipelineConfigurationCore));
+	}
+
+	bool operator==(const GraphicsPipelineConfigurationCore &other) const
+	{
+		return memcmp(this, &other, sizeof(GraphicsPipelineConfigurationCore)) == 0;
+	}
+};
+
+struct GraphicsPipelineConfigurationCoreHasher
+{
+	size_t operator() (const GraphicsPipelineConfigurationCore &configuration) const
+	{
+		return XXH32(&configuration, sizeof(GraphicsPipelineConfigurationCore), 0);
+	}
+};
+
+struct GraphicsPipelineConfigurationNoDynamicState
+{
+	CullMode cullmode = CULL_NONE;
+	Winding winding = WINDING_MAX_ENUM;
+	StencilAction stencilAction = STENCIL_MAX_ENUM;
+	CompareMode stencilCompare = COMPARE_MAX_ENUM;
+	DepthState depthState{};
+};
+
+struct GraphicsPipelineConfigurationFull
+{
+	GraphicsPipelineConfigurationCore core;
+	GraphicsPipelineConfigurationNoDynamicState noDynamicState;
+
+	GraphicsPipelineConfigurationFull()
 	{
-		memset(this, 0, sizeof(GraphicsPipelineConfiguration));
+		memset(this, 0, sizeof(GraphicsPipelineConfigurationFull));
 	}
 
-	bool operator==(const GraphicsPipelineConfiguration &other) const
+	bool operator==(const GraphicsPipelineConfigurationFull &other) const
 	{
-		return memcmp(this, &other, sizeof(GraphicsPipelineConfiguration)) == 0;
+		return memcmp(this, &other, sizeof(GraphicsPipelineConfigurationFull)) == 0;
 	}
 };
 
-struct GraphicsPipelineConfigurationHasher
+struct GraphicsPipelineConfigurationFullHasher
 {
-	size_t operator() (const GraphicsPipelineConfiguration &configuration) const
+	size_t operator() (const GraphicsPipelineConfigurationFull &configuration) const
 	{
-		return XXH32(&configuration, sizeof(GraphicsPipelineConfiguration), 0);
+		return XXH32(&configuration, sizeof(GraphicsPipelineConfigurationFull), 0);
 	}
 };
 
@@ -136,7 +160,8 @@ public:
 
 	void setMainTex(graphics::Texture *texture);
 
-	VkPipeline getCachedGraphicsPipeline(Graphics *vgfx, const GraphicsPipelineConfiguration &configuration);
+	VkPipeline getCachedGraphicsPipeline(Graphics *vgfx, const GraphicsPipelineConfigurationCore &configuration);
+	VkPipeline getCachedGraphicsPipeline(Graphics *vgfx, const GraphicsPipelineConfigurationFull &configuration);
 
 private:
 	void compileShaders();
@@ -183,7 +208,8 @@ private:
 
 	std::unordered_map<std::string, AttributeInfo> attributes;
 
-	std::unordered_map<GraphicsPipelineConfiguration, VkPipeline, GraphicsPipelineConfigurationHasher> graphicsPipelines;
+	std::unordered_map<GraphicsPipelineConfigurationCore, VkPipeline, GraphicsPipelineConfigurationCoreHasher> graphicsPipelinesDynamicState;
+	std::unordered_map<GraphicsPipelineConfigurationFull, VkPipeline, GraphicsPipelineConfigurationFullHasher> graphicsPipelinesNoDynamicState;
 
 	uint32_t currentFrame = 0;
 	uint32_t currentDescriptorPool = 0;

+ 6 - 0
src/modules/graphics/wrap_Buffer.cpp

@@ -404,9 +404,15 @@ static int w_Buffer_getFormat(lua_State *L)
 		lua_pushinteger(L, member.decl.arrayLength);
 		lua_setfield(L, -2, "arraylength");
 
+		lua_pushinteger(L, member.decl.bindingLocation);
+		lua_setfield(L, -2, "location");
+
 		lua_pushinteger(L, member.offset);
 		lua_setfield(L, -2, "offset");
 
+		lua_pushinteger(L, member.size);
+		lua_setfield(L, -2, "size");
+
 		lua_rawseti(L, -2, i + 1);
 	}
 

+ 78 - 10
src/modules/graphics/wrap_Graphics.cpp

@@ -56,7 +56,7 @@ namespace graphics
 static int luax_checkgraphicscreated(lua_State *L)
 {
 	if (!instance()->isCreated())
-		return luaL_error(L, "love.graphics cannot function without a window!");
+		return luaL_error(L, "love.graphics cannot function without a window.");
 	return 0;
 }
 
@@ -1599,6 +1599,11 @@ int w_newShader(lua_State *L)
 			luax_markdeprecated(L, 1, "texture2D() or textureCube() function calls in shader code", API_CUSTOM, DEPRECATED_REPLACED, "texture() function calls");
 		if (shader->isUsingDeprecatedTextureUniform())
 			luax_markdeprecated(L, 1, "'texture' uniform variable name in shader code", API_CUSTOM, DEPRECATED_NO_REPLACEMENT, "");
+		if (!shader->getUnsetVertexInputLocationsString().empty())
+		{
+			std::string str = "vertex input attribute(s) " + shader->getUnsetVertexInputLocationsString() + " without a 'location' layout qualifier in shader code";
+			luax_markdeprecated(L, 1, str.c_str(), API_CUSTOM, DEPRECATED_REPLACED, "layout(location = #) qualifier for vertex inputs");
+		}
 		luax_pushtype(L, shader);
 		shader->release();
 	}
@@ -1632,6 +1637,11 @@ int w_newComputeShader(lua_State* L)
 			luax_markdeprecated(L, 1, "texture2D() or textureCube() function calls in shader code", API_CUSTOM, DEPRECATED_REPLACED, "texture() function calls");
 		if (shader->isUsingDeprecatedTextureUniform())
 			luax_markdeprecated(L, 1, "'texture' uniform variable name in shader code", API_CUSTOM, DEPRECATED_NO_REPLACEMENT, "");
+		if (!shader->getUnsetVertexInputLocationsString().empty())
+		{
+			std::string str = "vertex input attribute(s) " + shader->getUnsetVertexInputLocationsString() + " without a 'location' layout qualifier in shader code";
+			luax_markdeprecated(L, 1, str.c_str(), API_CUSTOM, DEPRECATED_REPLACED, "layout(location = #) qualifier for vertex inputs");
+		}
 		luax_pushtype(L, shader);
 		shader->release();
 	}
@@ -1709,7 +1719,7 @@ static void luax_optbuffersettings(lua_State *L, int idx, Buffer::Settings &sett
 	lua_pop(L, 1);
 }
 
-static Buffer::DataDeclaration luax_checkdatadeclaration(lua_State* L, int formattableidx, int arrayindex, int declindex, bool requirename)
+static Buffer::DataDeclaration luax_checkdatadeclaration(lua_State* L, int formattableidx, int arrayindex, int declindex, bool requirename, bool requirelocation)
 {
 	Buffer::DataDeclaration decl("", DATAFORMAT_MAX_ENUM);
 
@@ -1744,10 +1754,24 @@ static Buffer::DataDeclaration luax_checkdatadeclaration(lua_State* L, int forma
 
 	decl.arrayLength = luax_intflag(L, declindex, "arraylength", 0);
 
+	lua_getfield(L, declindex, "location");
+	if (requirelocation && lua_type(L, -1) != LUA_TNUMBER)
+	{
+		std::ostringstream ss;
+		ss << "'location' field expected in array element #";
+		ss << arrayindex;
+		ss << " of format table";
+		std::string str = ss.str();
+		luaL_argerror(L, formattableidx, str.c_str());
+	}
+	else if (!lua_isnoneornil(L, -1))
+		decl.bindingLocation = luaL_checkint(L, -1);
+	lua_pop(L, 1);
+
 	return decl;
 }
 
-static void luax_checkbufferformat(lua_State *L, int idx, std::vector<Buffer::DataDeclaration> &format)
+static void luax_checkbufferformat(lua_State *L, int idx, const Buffer::Settings &settings, std::vector<Buffer::DataDeclaration> &format)
 {
 	if (lua_type(L, idx) == LUA_TSTRING)
 	{
@@ -1759,6 +1783,8 @@ static void luax_checkbufferformat(lua_State *L, int idx, std::vector<Buffer::Da
 		return;
 	}
 
+	bool requirelocation = (settings.usageFlags & BUFFERUSAGE_VERTEX) != 0;
+
 	luaL_checktype(L, idx, LUA_TTABLE);
 	int tablelen = luax_objlen(L, idx);
 
@@ -1767,13 +1793,34 @@ static void luax_checkbufferformat(lua_State *L, int idx, std::vector<Buffer::Da
 		lua_rawgeti(L, idx, i);
 		luaL_checktype(L, -1, LUA_TTABLE);
 
-		Buffer::DataDeclaration decl = luax_checkdatadeclaration(L, idx, i, -1, false);
+		Buffer::DataDeclaration decl = luax_checkdatadeclaration(L, idx, i, -1, false, requirelocation);
 
 		format.push_back(decl);
 		lua_pop(L, 1);
 	}
 }
 
+static void luax_validatebuffervertexbindings(lua_State *L, Buffer *buffer)
+{
+	if (buffer->hasLegacyVertexBindings())
+	{
+		std::string names;
+
+		for (const auto &member : buffer->getDataMembers())
+		{
+			if (member.decl.bindingLocation < 0)
+			{
+				if (names.empty())
+					names = member.decl.name;
+				else
+					names += ", " + member.decl.name;
+			}
+		}
+
+		luax_markdeprecated(L, 1, "vertex format 'name' fields in Meshes and Buffers", API_CUSTOM, DEPRECATED_REPLACED, "'location' field containing a binding location number value.");
+	}
+}
+
 static Buffer *luax_newbuffer(lua_State *L, int idx, Buffer::Settings settings, const std::vector<Buffer::DataDeclaration> &format)
 {
 	size_t arraylength = 0;
@@ -1821,6 +1868,8 @@ static Buffer *luax_newbuffer(lua_State *L, int idx, Buffer::Settings settings,
 	Buffer *b = nullptr;
 	luax_catchexcept(L, [&] { b = instance()->newBuffer(settings, format, initialdata, bytesize, arraylength); });
 
+	luax_validatebuffervertexbindings(L, b);
+
 	if (lua_istable(L, idx))
 	{
 		Buffer::Mapper mapper(*b);
@@ -1896,7 +1945,7 @@ int w_newBuffer(lua_State *L)
 	luax_optbuffersettings(L, 3, settings);
 
 	std::vector<Buffer::DataDeclaration> format;
-	luax_checkbufferformat(L, 1, format);
+	luax_checkbufferformat(L, 1, settings, format);
 
 	Buffer *b = luax_newbuffer(L, 2, settings, format);
 
@@ -2005,7 +2054,7 @@ static Mesh *newCustomMesh(lua_State *L)
 		lua_pop(L, 1);
 
 		if (hasformatfield || luax_objlen(L, -1) == 0)
-			decl = luax_checkdatadeclaration(L, 1, i, -1, true);
+			decl = luax_checkdatadeclaration(L, 1, i, -1, false, true);
 		else
 		{
 			// Legacy format arguments: {name, datatype, components}
@@ -2013,7 +2062,7 @@ static Mesh *newCustomMesh(lua_State *L)
 				lua_rawgeti(L, -j, j);
 
 			decl.name = luaL_checkstring(L, -3);
-			const char* tname = luaL_checkstring(L, -2);
+			const char *tname = luaL_checkstring(L, -2);
 			int components = (int)luaL_checkinteger(L, -1);
 
 			// Check deprecated format names.
@@ -2053,7 +2102,7 @@ static Mesh *newCustomMesh(lua_State *L)
 
 			lua_pop(L, 3);
 
-			luax_markdeprecated(L, 1, "vertex format array values in love.graphics.newMesh", API_CUSTOM, DEPRECATED_REPLACED, "named table fields 'format' and 'name'");
+			luax_markdeprecated(L, 1, "vertex format array values in love.graphics.newMesh", API_CUSTOM, DEPRECATED_REPLACED, "named table fields 'format' and 'location'");
 		}
 
 		lua_pop(L, 1);
@@ -2124,6 +2173,9 @@ static Mesh *newCustomMesh(lua_State *L)
 		t->flush();
 	}
 
+	if (t->getVertexBuffer() != nullptr)
+		luax_validatebuffervertexbindings(L, t->getVertexBuffer());
+
 	return t;
 }
 
@@ -2147,7 +2199,7 @@ static bool luax_isbufferattributetable(lua_State* L, int idx)
 
 static Mesh::BufferAttribute luax_checkbufferattributetable(lua_State *L, int idx)
 {
-	Mesh::BufferAttribute attrib = {};
+	Mesh::BufferAttribute attrib;
 
 	attrib.step = STEP_PER_VERTEX;
 	attrib.enabled = true;
@@ -2156,8 +2208,13 @@ static Mesh::BufferAttribute luax_checkbufferattributetable(lua_State *L, int id
 	attrib.buffer = luax_checkbuffer(L, -1);
 	lua_pop(L, 1);
 
+	lua_getfield(L, idx, "location");
+	attrib.bindingLocation = luaL_checkint(L, -1);
+	lua_pop(L, 1);
+
 	lua_getfield(L, idx, "name");
-	attrib.name = luax_checkstring(L, -1);
+	if (!lua_isnoneornil(L, -1))
+		attrib.name = luax_checkstring(L, -1);
 	lua_pop(L, 1);
 
 	lua_getfield(L, idx, "step");
@@ -2169,6 +2226,13 @@ static Mesh::BufferAttribute luax_checkbufferattributetable(lua_State *L, int id
 	}
 	lua_pop(L, 1);
 
+	lua_getfield(L, idx, "locationinbuffer");
+	if (!lua_isnoneornil(L, -1))
+		attrib.bindingLocationInBuffer = luaL_checkint(L, -1);
+	else
+		attrib.bindingLocationInBuffer = attrib.bindingLocation;
+	lua_pop(L, 1);
+
 	lua_getfield(L, idx, "nameinbuffer");	
 	if (!lua_isnoneornil(L, -1))
 		attrib.nameInBuffer = luax_checkstring(L, -1);
@@ -2197,6 +2261,10 @@ static Mesh* newMeshFromBuffers(lua_State *L)
 
 	Mesh *t = nullptr;
 	luax_catchexcept(L, [&]() { t = instance()->newMesh(attributes, drawmode); });
+
+	if (t->getVertexBuffer() != nullptr)
+		luax_validatebuffervertexbindings(L, t->getVertexBuffer());
+
 	return t;
 }
 

+ 53 - 13
src/modules/graphics/wrap_Mesh.cpp

@@ -260,6 +260,9 @@ int w_Mesh_getVertexFormat(lua_State *L)
 		lua_pushstring(L, member.decl.name.c_str());
 		lua_setfield(L, -2, "name");
 
+		lua_pushnumber(L, member.decl.bindingLocation);
+		lua_setfield(L, -2, "location");
+
 		const char *formatstr = "unknown";
 		getConstant(member.decl.format, formatstr);
 		lua_pushstring(L, formatstr);
@@ -280,18 +283,34 @@ int w_Mesh_getVertexFormat(lua_State *L)
 int w_Mesh_setAttributeEnabled(lua_State *L)
 {
 	Mesh *t = luax_checkmesh(L, 1);
-	const char *name = luaL_checkstring(L, 2);
 	bool enable = luax_checkboolean(L, 3);
-	luax_catchexcept(L, [&](){ t->setAttributeEnabled(name, enable); });
+	if (lua_type(L, 2) == LUA_TSTRING)
+	{
+		const char *name = luaL_checkstring(L, 2);
+		luax_catchexcept(L, [&](){ t->setAttributeEnabled(name, enable); });
+	}
+	else
+	{
+		int location = luaL_checkint(L, 2);
+		luax_catchexcept(L, [&](){ t->setAttributeEnabled(location, enable); });
+	}
 	return 0;
 }
 
 int w_Mesh_isAttributeEnabled(lua_State *L)
 {
 	Mesh *t = luax_checkmesh(L, 1);
-	const char *name = luaL_checkstring(L, 2);
 	bool enabled = false;
-	luax_catchexcept(L, [&](){ enabled = t->isAttributeEnabled(name); });
+	if (lua_type(L, 2) == LUA_TSTRING)
+	{
+		const char *name = luaL_checkstring(L, 2);
+		luax_catchexcept(L, [&](){ enabled = t->isAttributeEnabled(name); });
+	}
+	else
+	{
+		int location = luaL_checkint(L, 2);
+		luax_catchexcept(L, [&](){ enabled = t->isAttributeEnabled(location); });
+	}
 	lua_pushboolean(L, enabled);
 	return 1;
 }
@@ -299,7 +318,13 @@ int w_Mesh_isAttributeEnabled(lua_State *L)
 int w_Mesh_attachAttribute(lua_State *L)
 {
 	Mesh *t = luax_checkmesh(L, 1);
-	const char *name = luaL_checkstring(L, 2);
+
+	const char *name = nullptr;
+	int location = -1;
+	if (lua_type(L, 2) == LUA_TSTRING)
+		name = luaL_checkstring(L, 2);
+	else
+		location = luaL_checkint(L, 2);
 
 	Buffer *buffer = nullptr;
 	Mesh *mesh = nullptr;
@@ -320,10 +345,19 @@ int w_Mesh_attachAttribute(lua_State *L)
 	if (stepstr != nullptr && !getConstant(stepstr, step))
 		return luax_enumerror(L, "vertex attribute step", getConstants(step), stepstr);
 
-	const char *attachname = luaL_optstring(L, 5, name);
+	const char *attachname = name;
+	int attachlocation = location;
+	if (name != nullptr)
+		attachname = luaL_optstring(L, 5, name);
+	else
+		attachlocation = luaL_optint(L, 5, location);
+
 	int startindex = (int) luaL_optinteger(L, 6, 1) - 1;
 
-	luax_catchexcept(L, [&](){ t->attachAttribute(name, buffer, mesh, attachname, startindex, step); });
+	if (name != nullptr)
+		luax_catchexcept(L, [&](){ t->attachAttribute(name, buffer, mesh, attachname, startindex, step); });
+	else
+		luax_catchexcept(L, [&]() { t->attachAttribute(location, buffer, mesh, attachlocation, startindex, step); });
 	return 0;
 }
 
@@ -348,26 +382,32 @@ int w_Mesh_getAttachedAttributes(lua_State *L)
 	{
 		const auto &attrib = attributes[i];
 
-		lua_createtable(L, 5, 0);
+		lua_createtable(L, 0, 7);
 
 		luax_pushstring(L, attrib.name);
-		lua_rawseti(L, -1, 1);
+		lua_setfield(L, -2, "name");
+
+		lua_pushnumber(L, attrib.bindingLocation);
+		lua_setfield(L, -2, "location");
 
 		luax_pushtype(L, attrib.buffer.get());
-		lua_rawseti(L, -1, 2);
+		lua_setfield(L, -2, "buffer");
 
 		const char *stepstr = nullptr;
 		if (!getConstant(attrib.step, stepstr))
 			return luaL_error(L, "Invalid vertex attribute step.");
 		lua_pushstring(L, stepstr);
-		lua_rawseti(L, -1, 3);
+		lua_setfield(L, -2, "step");
 
 		const Buffer::DataMember &member = attrib.buffer->getDataMember(attrib.indexInBuffer);
 		luax_pushstring(L, member.decl.name);
-		lua_rawseti(L, -1, 4);
+		lua_setfield(L, -2, "nameinbuffer");
+
+		lua_pushnumber(L, member.decl.bindingLocation);
+		lua_setfield(L, -2, "locationinbuffer");
 
 		lua_pushinteger(L, attrib.startArrayIndex + 1);
-		lua_rawseti(L, -1, 5);
+		lua_setfield(L, -2, "startindex");
 
 		lua_rawseti(L, -1, i + 1);
 	}

+ 10 - 10
testing/tests/graphics.lua

@@ -13,9 +13,9 @@ love.test.graphics.Buffer = function(test)
 
   -- setup vertex data and create some buffers
   local vertexformat = {
-    {name="VertexPosition", format="floatvec2"},
-    {name="VertexTexCoord", format="floatvec2"},
-    {name="VertexColor", format="unorm8vec4"},
+    {name="VertexPosition", format="floatvec2", location=0},
+    {name="VertexTexCoord", format="floatvec2", location=1},
+    {name="VertexColor", format="unorm8vec4", location=2},
   }
   local vertexdata = {
     {0,  0,  0, 0, 1, 0, 1, 1},
@@ -529,11 +529,11 @@ love.test.graphics.Mesh = function(test)
 
   -- check using custom attributes
   local mesh2 = love.graphics.newMesh({
-    { name = 'VertexPosition', format = 'floatvec2'},
-    { name = 'VertexTexCoord', format = 'floatvec2'},
-    { name = 'VertexColor', format = 'floatvec4'},
-    { name = 'CustomValue1', format = 'floatvec2'},
-    { name = 'CustomValue2', format = 'uint16'}
+    { name = 'VertexPosition', format = 'floatvec2', location = 0},
+    { name = 'VertexTexCoord', format = 'floatvec2', location = 1},
+    { name = 'VertexColor', format = 'floatvec4', location = 2},
+    { name = 'CustomValue1', format = 'floatvec2', location = 3},
+    { name = 'CustomValue2', format = 'uint16', location = 4}
   }, {
 		{ 0, 0, 0, 0, 1, 0, 0, 1, 2, 1, 1005 },
 		{ image:getWidth(), 0, 1, 0, 0, 1, 0, 0, 2, 2, 2005 },
@@ -962,8 +962,8 @@ love.test.graphics.Shader = function(test)
       flat varying ivec4 VaryingInt;
 
       #ifdef VERTEX
-      in vec4 VertexPosition;
-      in ivec4 IntAttributeUnused;
+      layout(location = 0) in vec4 VertexPosition;
+      layout(location = 1) in ivec4 IntAttributeUnused;
 
       void vertexmain()
       {