Avoid redundant resource bindings.

When new descriptor sets are bound, it may be the case that many of the
bindings are not changed from their previous state. Detect this case and avoid
binding the same resources in the Metal comand buffer repeatedly. If possible,
change only the offset. This saves some encoding time.
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
index c6e7280..0def2be 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
@@ -373,7 +373,7 @@
     // Template function that marks both the vector and all binding elements in the vector as dirty.
     template<class T>
     void markDirty(T& bindings, bool& bindingsDirtyFlag) {
-        for (auto& b : bindings) { b.isDirty = true; }
+        for (auto& b : bindings) { b.markDirty(); }
         bindingsDirtyFlag = true;
     }
 
@@ -384,18 +384,18 @@
 
         if ( !b.mtlResource ) { return; }
 
-        T db = b;   // Copy that can be marked dirty
         MVKCommandEncoderState::markDirty();
         bindingsDirtyFlag = true;
-        db.isDirty = true;
 
         for (auto iter = bindings.begin(), end = bindings.end(); iter != end; ++iter) {
-            if( iter->index == db.index ) {
-                *iter = db;
+            if (iter->index == b.index) {
+                iter->update(b);
                 return;
             }
         }
-        bindings.push_back(db);
+
+        bindings.push_back(b);
+        bindings.back().markDirty();
     }
 
 	// For texture bindings, we also keep track of whether any bindings need a texture swizzle
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
index dfb8c1c..031541f 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
@@ -752,6 +752,10 @@
                                                            b.mtlBytes,
                                                            b.size,
                                                            b.index);
+                           else if (b.justOffset)
+                               [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl)
+                                            setBufferOffset: b.offset
+                                            atIndex: b.index];
                            else
                                [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBuffer: b.mtlBuffer
                                                                                                              offset: b.offset
@@ -785,9 +789,14 @@
                                                               b.size,
                                                               b.index);
                                } else {
-                                   [cmdEncoder->_mtlRenderEncoder setVertexBuffer: b.mtlBuffer
-                                                                           offset: b.offset
-                                                                          atIndex: b.index];
+                                   if (b.justOffset) {
+                                       [cmdEncoder->_mtlRenderEncoder setVertexBufferOffset: b.offset
+                                                                                    atIndex: b.index];
+                                   } else {
+                                       [cmdEncoder->_mtlRenderEncoder setVertexBuffer: b.mtlBuffer
+                                                                               offset: b.offset
+                                                                              atIndex: b.index];
+                                   }
 
                                    // Add any translated vertex bindings for this binding
                                    auto xltdVtxBindings = pipeline->getTranslatedVertexBindings();
@@ -828,6 +837,9 @@
                                                            b.mtlBytes,
                                                            b.size,
                                                            b.index);
+                           else if (b.justOffset)
+                               [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBufferOffset: b.offset
+                                                                                                                  atIndex: b.index];
                            else
                                [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBuffer: b.mtlBuffer
                                                                                                              offset: b.offset
@@ -858,6 +870,9 @@
                                                           b.mtlBytes,
                                                           b.size,
                                                           b.index);
+                           else if (b.justOffset)
+                               [cmdEncoder->_mtlRenderEncoder setVertexBufferOffset: b.offset
+                                                                            atIndex: b.index];
                            else
                                [cmdEncoder->_mtlRenderEncoder setVertexBuffer: b.mtlBuffer
                                                                        offset: b.offset
@@ -888,6 +903,9 @@
                                                             b.mtlBytes,
                                                             b.size,
                                                             b.index);
+                           else if (b.justOffset)
+                               [cmdEncoder->_mtlRenderEncoder setFragmentBufferOffset: b.offset
+                                                                              atIndex: b.index];
                            else
                                [cmdEncoder->_mtlRenderEncoder setFragmentBuffer: b.mtlBuffer
                                                                          offset: b.offset
@@ -1027,7 +1045,12 @@
 										b.mtlBytes,
 										b.size,
 										b.index);
-		} else {
+        } else if (b.justOffset) {
+            [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseDispatch)
+                        setBufferOffset: b.offset
+                                atIndex: b.index];
+
+        } else {
 			[cmdEncoder->getMTLComputeEncoder(kMVKCommandUseDispatch) setBuffer: b.mtlBuffer
 																		 offset: b.offset
 																		atIndex: b.index];
diff --git a/MoltenVK/MoltenVK/Commands/MVKMTLResourceBindings.h b/MoltenVK/MoltenVK/Commands/MVKMTLResourceBindings.h
index 012600a..a0f71d0 100644
--- a/MoltenVK/MoltenVK/Commands/MVKMTLResourceBindings.h
+++ b/MoltenVK/MoltenVK/Commands/MVKMTLResourceBindings.h
@@ -34,6 +34,16 @@
     uint32_t swizzle = 0;
 	uint16_t index = 0;
     bool isDirty = true;
+
+    inline void markDirty() { isDirty = true; }
+
+    inline void update(const MVKMTLTextureBinding &other) {
+        if (mtlTexture != other.mtlTexture || swizzle != other.swizzle) {
+            mtlTexture = other.mtlTexture;
+            swizzle = other.swizzle;
+            markDirty();
+        }
+    }
 } MVKMTLTextureBinding;
 
 /** Describes a MTLSamplerState resource binding. */
@@ -41,6 +51,15 @@
     union { id<MTLSamplerState> mtlSamplerState = nil; id<MTLSamplerState> mtlResource; }; // aliases
     uint16_t index = 0;
     bool isDirty = true;
+
+    inline void markDirty() { isDirty = true; }
+
+    inline void update(const MVKMTLSamplerStateBinding &other) {
+        if (mtlSamplerState != other.mtlSamplerState) {
+            mtlSamplerState = other.mtlSamplerState;
+            markDirty();
+        }
+    }
 } MVKMTLSamplerStateBinding;
 
 /** Describes a MTLBuffer resource binding. */
@@ -49,8 +68,27 @@
     VkDeviceSize offset = 0;
     uint32_t size = 0;
 	uint16_t index = 0;
+    bool justOffset = false;
     bool isDirty = true;
     bool isInline = false;
+
+    inline void markDirty() { justOffset = false; isDirty = true; }
+
+    inline void update(const MVKMTLBufferBinding &other) {
+        if (mtlBuffer != other.mtlBuffer || size != other.size || isInline != other.isInline) {
+            mtlBuffer = other.mtlBuffer;
+            size = other.size;
+            isInline = other.isInline;
+            offset = other.offset;
+
+            justOffset = false;
+            isDirty = true;
+        } else if (offset != other.offset) {
+            offset = other.offset;
+            justOffset = !isDirty || justOffset;
+            isDirty = true;
+        }
+    }
 } MVKMTLBufferBinding;
 
 /** Describes a MTLBuffer resource binding as used for an index buffer. */