Index: shaders/shader.vert
===================================================================
--- shaders/shader.vert	(revision cae7a2c45c3b45a16da4eccb3767ade8c9c58df1)
+++ shaders/shader.vert	(revision de32fdaea94305c3a14e4d64c54d31f4571bd34f)
@@ -1,3 +1,10 @@
 #version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout (binding = 0) uniform UniformBufferObject {
+   mat4 model;
+   mat4 view;
+   mat4 proj;
+} ubo;
 
 layout(location = 0) in vec2 inPosition;
@@ -7,5 +14,5 @@
 
 void main() {
-   gl_Position = vec4(inPosition, 0.0, 1.0);
+   gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
 }
Index: vulkan-game.cpp
===================================================================
--- vulkan-game.cpp	(revision cae7a2c45c3b45a16da4eccb3767ade8c9c58df1)
+++ vulkan-game.cpp	(revision de32fdaea94305c3a14e4d64c54d31f4571bd34f)
@@ -2,5 +2,7 @@
 DESIGN GUIDE
 
-I should store multiple buffers (e.g. vertex and index buffers) in the same VkBuffer and use offsets into it
+-I should store multiple buffers (e.g. vertex and index buffers) in the same VkBuffer and use offsets into it
+-For specifying a separate transform for each model, I can specify a descriptorCount > ` in the ubo layout binding
+-Name class instance variables that are pointers (and possibly other pointer variables as well) like pVarName
 */
 
@@ -12,6 +14,6 @@
 
 #define GLM_FORCE_RADIANS
-#define GLM_FORCE_DEPTH_ZERO_TO_ONE
 #include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
 
 #include <iostream>
@@ -22,4 +24,5 @@
 #include <set>
 #include <optional>
+#include <chrono>
 
 using namespace std;
@@ -88,4 +91,10 @@
       return attributeDescriptions;
    }
+};
+
+struct UniformBufferObject {
+   glm::mat4 model;
+   glm::mat4 view;
+   glm::mat4 proj;
 };
 
@@ -158,4 +167,5 @@
       vector<VkImageView> swapChainImageViews;
       VkRenderPass renderPass;
+      VkDescriptorSetLayout descriptorSetLayout;
       VkPipelineLayout pipelineLayout;
       VkPipeline graphicsPipeline;
@@ -164,6 +174,10 @@
       VkBuffer vertexBuffer;
       VkDeviceMemory vertexBufferMemory;
+
       VkBuffer indexBuffer;
       VkDeviceMemory indexBufferMemory;
+
+      vector<VkBuffer> uniformBuffers;
+      vector<VkDeviceMemory> uniformBuffersMemory;
 
       vector<VkFramebuffer> swapChainFramebuffers;
@@ -203,4 +217,5 @@
          createImageViews();
          createRenderPass();
+         createDescriptorSetLayout();
          createGraphicsPipeline();
          createFramebuffers();
@@ -208,4 +223,5 @@
          createVertexBuffer();
          createIndexBuffer();
+         createUniformBuffers();
          createCommandBuffers();
          createSyncObjects();
@@ -231,4 +247,5 @@
          createGraphicsPipeline();
          createFramebuffers();
+         createUniformBuffers();
          createCommandBuffers();
       }
@@ -250,4 +267,9 @@
 
          vkDestroySwapchainKHR(device, swapChain, nullptr);
+
+         for (size_t i = 0; i < swapChainImages.size(); i++) {
+            vkDestroyBuffer(device, uniformBuffers[i], nullptr);
+            vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
+         }
       }
 
@@ -699,4 +721,22 @@
       }
 
+      void createDescriptorSetLayout() {
+         VkDescriptorSetLayoutBinding uboLayoutBinding = {};
+         uboLayoutBinding.binding = 0;
+         uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+         uboLayoutBinding.descriptorCount = 1;
+         uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
+         uboLayoutBinding.pImmutableSamplers = nullptr;
+
+         VkDescriptorSetLayoutCreateInfo layoutInfo = {};
+         layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+         layoutInfo.bindingCount = 1;
+         layoutInfo.pBindings = &uboLayoutBinding;
+
+         if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
+            throw runtime_error("failed to create descriptor set layout!");
+         }
+      }
+
       void createGraphicsPipeline() {
          auto vertShaderCode = readFile("shaders/vert.spv");
@@ -787,5 +827,6 @@
          VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
          pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
-         pipelineLayoutInfo.setLayoutCount = 0;
+         pipelineLayoutInfo.setLayoutCount = 1;
+         pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
          pipelineLayoutInfo.pushConstantRangeCount = 0;
 
@@ -914,4 +955,17 @@
          vkDestroyBuffer(device, stagingBuffer, nullptr);
          vkFreeMemory(device, stagingBufferMemory, nullptr);
+      }
+
+      void createUniformBuffers() {
+         VkDeviceSize bufferSize = sizeof(UniformBufferObject);
+
+         uniformBuffers.resize(swapChainImages.size());
+         uniformBuffersMemory.resize(swapChainImages.size());
+
+         for (size_t i = 0; i < swapChainImages.size(); i++) {
+            createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
+               VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
+               uniformBuffers[i], uniformBuffersMemory[i]);
+         }
       }
 
@@ -1113,4 +1167,6 @@
          }
 
+         updateUniformBuffer(imageIndex);
+
          VkSubmitInfo submitInfo = {};
          submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
@@ -1160,6 +1216,26 @@
       }
 
+      void updateUniformBuffer(uint32_t currentImage) {
+         static auto startTime = chrono::high_resolution_clock::now();
+
+         auto currentTime = chrono::high_resolution_clock::now();
+         float time = chrono::duration<float, chrono::seconds::period>(currentTime - startTime).count();
+
+         UniformBufferObject ubo = {};
+         ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
+         ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
+         ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f);
+         ubo.proj[1][1] *= -1;
+
+         void* data;
+         vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0, &data);
+         memcpy(data, &ubo, sizeof(ubo));
+         vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
+      }
+
       void cleanup() {
          cleanupSwapChain();
+
+         vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
 
          vkDestroyBuffer(device, indexBuffer, nullptr);
