Index: graphics-pipeline_vulkan.cpp
===================================================================
--- graphics-pipeline_vulkan.cpp	(revision 771b33a137aed1be0c097e401feb8e3727bb1166)
+++ graphics-pipeline_vulkan.cpp	(revision b79417880394af692ba165be4054f0843fdb1c91)
@@ -3,7 +3,14 @@
 #include <fstream>
 #include <stdexcept>
-
-GraphicsPipeline_Vulkan::GraphicsPipeline_Vulkan(VkDevice device, Viewport viewport, int vertexSize) {
+#include <iostream>
+
+using namespace std;
+
+// TODO: Remove any instances of cout and instead throw exceptions
+
+GraphicsPipeline_Vulkan::GraphicsPipeline_Vulkan(VkDevice device, VkRenderPass renderPass, Viewport viewport,
+      int vertexSize) {
    this->device = device;
+   this->renderPass = renderPass;
    this->viewport = viewport;
 
@@ -25,4 +32,12 @@
 
    this->attributeDescriptions.push_back(attributeDesc);
+}
+
+void GraphicsPipeline_Vulkan::addDescriptorInfo(VkDescriptorType type, VkShaderStageFlags stageFlags, vector<VkDescriptorBufferInfo>* bufferData) {
+   this->descriptorInfoList.push_back({ type, stageFlags, bufferData, nullptr });
+}
+
+void GraphicsPipeline_Vulkan::addDescriptorInfo(VkDescriptorType type, VkShaderStageFlags stageFlags, VkDescriptorImageInfo* imageData) {
+   this->descriptorInfoList.push_back({ type, stageFlags, nullptr, imageData });
 }
 
@@ -131,7 +146,119 @@
    pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    pipelineLayoutInfo.setLayoutCount = 1;
-
-   vkDestroyShaderModule(device, vertShaderModule, nullptr);
-   vkDestroyShaderModule(device, fragShaderModule, nullptr);
+   pipelineLayoutInfo.pSetLayouts = &this->descriptorSetLayout;
+   pipelineLayoutInfo.pushConstantRangeCount = 0;
+
+   if (vkCreatePipelineLayout(this->device, &pipelineLayoutInfo, nullptr, &this->pipelineLayout) != VK_SUCCESS) {
+      throw runtime_error("failed to create pipeline layout!");
+   }
+
+   VkGraphicsPipelineCreateInfo pipelineInfo = {};
+   pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
+   pipelineInfo.stageCount = 2;
+   pipelineInfo.pStages = shaderStages;
+   pipelineInfo.pVertexInputState = &vertexInputInfo;
+   pipelineInfo.pInputAssemblyState = &inputAssembly;
+   pipelineInfo.pViewportState = &viewportState;
+   pipelineInfo.pRasterizationState = &rasterizer;
+   pipelineInfo.pMultisampleState = &multisampling;
+   pipelineInfo.pDepthStencilState = &depthStencil;
+   pipelineInfo.pColorBlendState = &colorBlending;
+   pipelineInfo.pDynamicState = nullptr;
+   pipelineInfo.layout = this->pipelineLayout;
+   pipelineInfo.renderPass = this->renderPass;
+   pipelineInfo.subpass = 0;
+   pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
+   pipelineInfo.basePipelineIndex = -1;
+
+   if (vkCreateGraphicsPipelines(this->device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &this->pipeline) != VK_SUCCESS) {
+      throw runtime_error("failed to create graphics pipeline!");
+   }
+
+   vkDestroyShaderModule(this->device, vertShaderModule, nullptr);
+   vkDestroyShaderModule(this->device, fragShaderModule, nullptr);
+}
+
+void GraphicsPipeline_Vulkan::createDescriptorSetLayout() {
+   vector<VkDescriptorSetLayoutBinding> bindings(this->descriptorInfoList.size());
+
+   for (size_t i = 0; i < bindings.size(); i++) {
+      bindings[i].binding = i;
+      bindings[i].descriptorCount = 1;
+      bindings[i].descriptorType = this->descriptorInfoList[i].type;
+      bindings[i].stageFlags = this->descriptorInfoList[i].stageFlags;
+      bindings[i].pImmutableSamplers = nullptr;
+   }
+
+   VkDescriptorSetLayoutCreateInfo layoutInfo = {};
+   layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+   layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
+   layoutInfo.pBindings = bindings.data();
+
+   if (vkCreateDescriptorSetLayout(this->device, &layoutInfo, nullptr, &this->descriptorSetLayout) != VK_SUCCESS) {
+      throw runtime_error("failed to create descriptor set layout!");
+   }
+}
+
+void GraphicsPipeline_Vulkan::createDescriptorPool(vector<VkImage>& swapChainImages) {
+   vector<VkDescriptorPoolSize> poolSizes(this->descriptorInfoList.size());
+
+   for (size_t i = 0; i < poolSizes.size(); i++) {
+      poolSizes[i].type = this->descriptorInfoList[i].type;
+      poolSizes[i].descriptorCount = static_cast<uint32_t>(swapChainImages.size());
+   }
+
+   VkDescriptorPoolCreateInfo poolInfo = {};
+   poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
+   poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
+   poolInfo.pPoolSizes = poolSizes.data();
+   poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());
+
+   if (vkCreateDescriptorPool(this->device, &poolInfo, nullptr, &this->descriptorPool) != VK_SUCCESS) {
+      throw runtime_error("failed to create descriptor pool!");
+   }
+}
+
+void GraphicsPipeline_Vulkan::createDescriptorSets(vector<VkImage>& swapChainImages) {
+   vector<VkDescriptorSetLayout> layouts(swapChainImages.size(), this->descriptorSetLayout);
+
+   VkDescriptorSetAllocateInfo allocInfo = {};
+   allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+   allocInfo.descriptorPool = this->descriptorPool;
+   allocInfo.descriptorSetCount = static_cast<uint32_t>(swapChainImages.size());
+   allocInfo.pSetLayouts = layouts.data();
+
+   this->descriptorSets.resize(swapChainImages.size());
+   if (vkAllocateDescriptorSets(device, &allocInfo, this->descriptorSets.data()) != VK_SUCCESS) {
+      throw runtime_error("failed to allocate descriptor sets!");
+   }
+
+   for (size_t i = 0; i < swapChainImages.size(); i++) {
+      vector<VkWriteDescriptorSet> descriptorWrites(this->descriptorInfoList.size());
+
+      for (size_t j = 0; j < descriptorWrites.size(); j++) {
+         descriptorWrites[j].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+         descriptorWrites[j].dstSet = this->descriptorSets[i];
+         descriptorWrites[j].dstBinding = j;
+         descriptorWrites[j].dstArrayElement = 0;
+         descriptorWrites[j].descriptorType = this->descriptorInfoList[j].type;
+         descriptorWrites[j].descriptorCount = 1;
+         descriptorWrites[j].pBufferInfo = nullptr;
+         descriptorWrites[j].pImageInfo = nullptr;
+         descriptorWrites[j].pTexelBufferView = nullptr;
+
+         switch (descriptorWrites[j].descriptorType) {
+            case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER:
+               descriptorWrites[j].pBufferInfo = &(*this->descriptorInfoList[j].bufferDataList)[i];
+               break;
+            case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER:
+               descriptorWrites[j].pImageInfo = this->descriptorInfoList[j].imageData;
+               break;
+            default:
+               cout << "Unknown descriptor type: " << descriptorWrites[j].descriptorType << endl;
+         }
+      }
+
+      vkUpdateDescriptorSets(this->device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
+   }
 }
 
@@ -143,5 +270,5 @@
 
    VkShaderModule shaderModule;
-   if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
+   if (vkCreateShaderModule(this->device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
       throw runtime_error("failed to create shader module!");
    }
@@ -167,2 +294,12 @@
    return buffer;
 }
+
+void GraphicsPipeline_Vulkan::cleanup() {
+   vkDestroyPipeline(this->device, this->pipeline, nullptr);
+   vkDestroyDescriptorPool(this->device, this->descriptorPool, nullptr);
+   vkDestroyPipelineLayout(this->device, this->pipelineLayout, nullptr);
+}
+
+void GraphicsPipeline_Vulkan::cleanupBuffers() {
+   vkDestroyDescriptorSetLayout(this->device, this->descriptorSetLayout, nullptr);
+}
Index: graphics-pipeline_vulkan.hpp
===================================================================
--- graphics-pipeline_vulkan.hpp	(revision 771b33a137aed1be0c097e401feb8e3727bb1166)
+++ graphics-pipeline_vulkan.hpp	(revision b79417880394af692ba165be4054f0843fdb1c91)
@@ -8,19 +8,52 @@
 #include <vulkan/vulkan.h>
 
+// TODO: Maybe change the name of this struct so I can call the list something other than descriptorInfoList
+struct DescriptorInfo {
+   VkDescriptorType type;
+   VkShaderStageFlags stageFlags;
+
+   // Only one of the below properties should be set
+   vector<VkDescriptorBufferInfo>* bufferDataList;
+   VkDescriptorImageInfo* imageData;
+};
+
 class GraphicsPipeline_Vulkan : public GraphicsPipeline {
    public:
-      GraphicsPipeline_Vulkan(VkDevice device, Viewport viewport, int vertexSize);
+      GraphicsPipeline_Vulkan(VkDevice device, VkRenderPass renderPass, Viewport viewport, int vertexSize);
       ~GraphicsPipeline_Vulkan();
 
+      // Maybe I should rename these to addVertexAttribute (addVaryingAttribute) and addUniformAttribute
+
       void addAttribute(VkFormat format, size_t offset);
+
+      void addDescriptorInfo(VkDescriptorType type, VkShaderStageFlags stageFlags, vector<VkDescriptorBufferInfo>* bufferData);
+      void addDescriptorInfo(VkDescriptorType type, VkShaderStageFlags stageFlags, VkDescriptorImageInfo* imageData);
+
       void createPipeline(string vertShaderFile, string fragShaderFile);
+      void createDescriptorSetLayout();
+      void createDescriptorPool(vector<VkImage>& swapChainImages);
+      void createDescriptorSets(vector<VkImage>& swapChainImages);
+
+      void cleanup();
+      void cleanupBuffers();
    
    private:
-      VkDevice device;
-      VkVertexInputBindingDescription bindingDescription;
-      vector<VkVertexInputAttributeDescription> attributeDescriptions;
-
       VkShaderModule createShaderModule(const vector<char>& code);
       vector<char> readFile(const string& filename);
+
+      VkDevice device;
+      VkRenderPass renderPass;
+
+      VkPipeline pipeline;
+      VkPipelineLayout pipelineLayout;
+
+      VkVertexInputBindingDescription bindingDescription;
+
+      vector<VkVertexInputAttributeDescription> attributeDescriptions;
+      vector<DescriptorInfo> descriptorInfoList;
+
+      VkDescriptorSetLayout descriptorSetLayout;
+      VkDescriptorPool descriptorPool;
+      vector<VkDescriptorSet> descriptorSets;
 };
 
Index: vulkan-game.cpp
===================================================================
--- vulkan-game.cpp	(revision 771b33a137aed1be0c097e401feb8e3727bb1166)
+++ vulkan-game.cpp	(revision b79417880394af692ba165be4054f0843fdb1c91)
@@ -9,7 +9,12 @@
 
 #include "utils.hpp"
-#include "vulkan-utils.hpp"
 
 using namespace std;
+
+struct UniformBufferObject {
+   alignas(16) mat4 model;
+   alignas(16) mat4 view;
+   alignas(16) mat4 proj;
+};
 
 VulkanGame::VulkanGame() {
@@ -84,4 +89,25 @@
       cout << gui->getError() << endl;
       return RTWO_ERROR;
+   }
+
+   SDL_VERSION(&sdlVersion);
+
+   // In SDL 2.0.10 (currently, the latest), SDL_TEXTUREACCESS_TARGET is required to get a transparent overlay working
+   // However, the latest SDL version available through homebrew on Mac is 2.0.9, which requires SDL_TEXTUREACCESS_STREAMING
+   // I tried building sdl 2.0.10 (and sdl_image and sdl_ttf) from source on Mac, but had some issues, so this is easier
+   // until the homebrew recipe is updated
+   if (sdlVersion.major == 2 && sdlVersion.minor == 0 && sdlVersion.patch == 9) {
+      uiOverlay = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING,
+         gui->getWindowWidth(), gui->getWindowHeight());
+   } else {
+      uiOverlay = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET,
+         gui->getWindowWidth(), gui->getWindowHeight());
+   }
+
+   if (uiOverlay == nullptr) {
+      cout << "Unable to create blank texture! SDL Error: " << SDL_GetError() << endl;
+   }
+   if (SDL_SetTextureBlendMode(uiOverlay, SDL_BLENDMODE_BLEND) != 0) {
+      cout << "Unable to set texture blend mode! SDL Error: " << SDL_GetError() << endl;
    }
 
@@ -107,5 +133,7 @@
    createCommandPool();
 
-   graphicsPipelines.push_back(GraphicsPipeline_Vulkan(device, viewport, sizeof(Vertex)));
+   createVulkanResources();
+
+   graphicsPipelines.push_back(GraphicsPipeline_Vulkan(device, renderPass, viewport, sizeof(Vertex)));
 
    graphicsPipelines.back().addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&Vertex::pos));
@@ -113,12 +141,26 @@
    graphicsPipelines.back().addAttribute(VK_FORMAT_R32G32_SFLOAT, offset_of(&Vertex::texCoord));
 
+   graphicsPipelines.back().addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+      VK_SHADER_STAGE_VERTEX_BIT, &uniformBufferInfoList);
+   graphicsPipelines.back().addDescriptorInfo(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+      VK_SHADER_STAGE_FRAGMENT_BIT, &floorTextureImageDescriptor);
+
+   graphicsPipelines.back().createDescriptorSetLayout();
    graphicsPipelines.back().createPipeline("shaders/scene-vert.spv", "shaders/scene-frag.spv");
-
-   graphicsPipelines.push_back(GraphicsPipeline_Vulkan(device, viewport, sizeof(OverlayVertex)));
+   graphicsPipelines.back().createDescriptorPool(swapChainImages);
+   graphicsPipelines.back().createDescriptorSets(swapChainImages);
+
+   graphicsPipelines.push_back(GraphicsPipeline_Vulkan(device, renderPass, viewport, sizeof(OverlayVertex)));
 
    graphicsPipelines.back().addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&OverlayVertex::pos));
    graphicsPipelines.back().addAttribute(VK_FORMAT_R32G32_SFLOAT, offset_of(&OverlayVertex::texCoord));
 
+   graphicsPipelines.back().addDescriptorInfo(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+      VK_SHADER_STAGE_FRAGMENT_BIT, &sdlOverlayImageDescriptor);
+
+   graphicsPipelines.back().createDescriptorSetLayout();
    graphicsPipelines.back().createPipeline("shaders/overlay-vert.spv", "shaders/overlay-frag.spv");
+   graphicsPipelines.back().createDescriptorPool(swapChainImages);
+   graphicsPipelines.back().createDescriptorSets(swapChainImages);
 
    cout << "Created " << graphicsPipelines.size() << " graphics pipelines" << endl;
@@ -189,4 +231,8 @@
    cleanupSwapChain();
 
+   for (GraphicsPipeline_Vulkan pipeline : graphicsPipelines) {
+      pipeline.cleanupBuffers();
+   }
+
    vkDestroyCommandPool(device, commandPool, nullptr);
    vkDestroyDevice(device, nullptr);
@@ -198,4 +244,12 @@
 
    vkDestroyInstance(instance, nullptr);
+
+   // TODO: Check if any of these functions accept null parameters
+   // If they do, I don't need to check for that
+
+   if (uiOverlay != nullptr) {
+      SDL_DestroyTexture(uiOverlay);
+      uiOverlay = nullptr;
+   }
 
    SDL_DestroyRenderer(renderer);
@@ -343,5 +397,5 @@
    QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, surface);
 
-   vector<VkDeviceQueueCreateInfo> queueCreateInfos;
+   vector<VkDeviceQueueCreateInfo> queueCreateInfoList;
    set<uint32_t> uniqueQueueFamilies = { indices.graphicsFamily.value(), indices.presentFamily.value() };
 
@@ -354,5 +408,5 @@
       queueCreateInfo.pQueuePriorities = &queuePriority;
 
-      queueCreateInfos.push_back(queueCreateInfo);
+      queueCreateInfoList.push_back(queueCreateInfo);
    }
 
@@ -362,6 +416,6 @@
    VkDeviceCreateInfo createInfo = {};
    createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
-   createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
-   createInfo.pQueueCreateInfos = queueCreateInfos.data();
+   createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfoList.size());
+   createInfo.pQueueCreateInfos = queueCreateInfoList.data();
 
    createInfo.pEnabledFeatures = &deviceFeatures;
@@ -529,8 +583,77 @@
 }
 
+void VulkanGame::createVulkanResources() {
+   createTextureSampler();
+   createUniformBuffers();
+
+   // TODO: Make sure that Vulkan complains about these images not being destroyed and then destroy them
+
+   VulkanUtils::createVulkanImageFromFile(device, physicalDevice, commandPool, "textures/texture.jpg",
+      floorTextureImage, graphicsQueue);
+   VulkanUtils::createVulkanImageFromSDLTexture(device, physicalDevice, uiOverlay, sdlOverlayImage);
+
+   floorTextureImageDescriptor = {};
+   floorTextureImageDescriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+   floorTextureImageDescriptor.imageView = floorTextureImage.imageView;
+   floorTextureImageDescriptor.sampler = textureSampler;
+
+   sdlOverlayImageDescriptor = {};
+   sdlOverlayImageDescriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+   sdlOverlayImageDescriptor.imageView = sdlOverlayImage.imageView;
+   sdlOverlayImageDescriptor.sampler = textureSampler;
+}
+
+void VulkanGame::createTextureSampler() {
+   VkSamplerCreateInfo samplerInfo = {};
+   samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
+   samplerInfo.magFilter = VK_FILTER_LINEAR;
+   samplerInfo.minFilter = VK_FILTER_LINEAR;
+
+   samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+   samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+   samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+
+   samplerInfo.anisotropyEnable = VK_TRUE;
+   samplerInfo.maxAnisotropy = 16;
+   samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
+   samplerInfo.unnormalizedCoordinates = VK_FALSE;
+   samplerInfo.compareEnable = VK_FALSE;
+   samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
+   samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
+   samplerInfo.mipLodBias = 0.0f;
+   samplerInfo.minLod = 0.0f;
+   samplerInfo.maxLod = 0.0f;
+
+   if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
+      throw runtime_error("failed to create texture sampler!");
+   }
+}
+
+void VulkanGame::createUniformBuffers() {
+   VkDeviceSize bufferSize = sizeof(UniformBufferObject);
+
+   uniformBuffers.resize(swapChainImages.size());
+   uniformBuffersMemory.resize(swapChainImages.size());
+   uniformBufferInfoList.resize(swapChainImages.size());
+
+   for (size_t i = 0; i < swapChainImages.size(); i++) {
+      VulkanUtils::createBuffer(device, physicalDevice, bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
+         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
+         uniformBuffers[i], uniformBuffersMemory[i]);
+
+      uniformBufferInfoList[i].buffer = uniformBuffers[i];
+      uniformBufferInfoList[i].offset = 0;
+      uniformBufferInfoList[i].range = sizeof(UniformBufferObject);
+   }
+}
+
 void VulkanGame::cleanupSwapChain() {
+   for (GraphicsPipeline_Vulkan pipeline : graphicsPipelines) {
+      pipeline.cleanup();
+   }
+
    vkDestroyRenderPass(device, renderPass, nullptr);
 
-   for (auto imageView : swapChainImageViews) {
+   for (VkImageView imageView : swapChainImageViews) {
       vkDestroyImageView(device, imageView, nullptr);
    }
Index: vulkan-game.hpp
===================================================================
--- vulkan-game.hpp	(revision 771b33a137aed1be0c097e401feb8e3727bb1166)
+++ vulkan-game.hpp	(revision b79417880394af692ba165be4054f0843fdb1c91)
@@ -6,4 +6,6 @@
 #include "game-gui-sdl.hpp"
 #include "graphics-pipeline_vulkan.hpp"
+
+#include "vulkan-utils.hpp"
 
 #ifdef NDEBUG
@@ -40,6 +42,8 @@
 
       SDL_version sdlVersion;
-      SDL_Window* window;
-      SDL_Renderer* renderer;
+      SDL_Window* window = nullptr;
+      SDL_Renderer* renderer = nullptr;
+
+      SDL_Texture* uiOverlay = nullptr;
 
       VkInstance instance;
@@ -59,4 +63,26 @@
       VkRenderPass renderPass;
       VkCommandPool commandPool;
+
+      // TODO: Create these (and wrap them inside a VulkanImage)
+      VkImage depthImage;
+      VkDeviceMemory depthImageMemory;
+      VkImageView depthImageView;
+
+      VkSampler textureSampler;
+
+      vector<VkDescriptorBufferInfo> uniformBufferInfoList;
+
+      // These are currently to store the MVP matrix
+      // I should figure out if it makes sense to use them for other uniforms in the future
+      // If not, I should rename them to better indicate their purpose.
+      // I should also decide if I can use these for all shaders, or if I need a separapte set of buffers for each one
+      vector<VkBuffer> uniformBuffers;
+      vector<VkDeviceMemory> uniformBuffersMemory;
+
+      VulkanImage floorTextureImage;
+      VkDescriptorImageInfo floorTextureImageDescriptor;
+
+      VulkanImage sdlOverlayImage;
+      VkDescriptorImageInfo sdlOverlayImageDescriptor;
 
       bool framebufferResized = false;
@@ -83,4 +109,7 @@
       VkFormat findDepthFormat();
       void createCommandPool();
+      void createVulkanResources();
+      void createTextureSampler();
+      void createUniformBuffers();
 
       void cleanupSwapChain();
Index: vulkan-ref.cpp
===================================================================
--- vulkan-ref.cpp	(revision 771b33a137aed1be0c097e401feb8e3727bb1166)
+++ vulkan-ref.cpp	(revision b79417880394af692ba165be4054f0843fdb1c91)
@@ -80,4 +80,5 @@
 };
 
+/*** START OF REFACTORED CODE ***/
 struct DescriptorInfo {
    VkDescriptorType type;
@@ -87,6 +88,8 @@
    VkDescriptorImageInfo* imageData;
 };
+/*** END OF REFACTORED CODE ***/
 
 struct GraphicsPipelineInfo {
+/*** START OF REFACTORED CODE ***/
    VkPipelineLayout pipelineLayout;
    VkPipeline pipeline;
@@ -100,4 +103,5 @@
    VkDescriptorSetLayout descriptorSetLayout;
    vector<VkDescriptorSet> descriptorSets;
+/*** END OF REFACTORED CODE ***/
 
    size_t numVertices; // Currently unused
@@ -153,6 +157,6 @@
       SDL_Window* window = nullptr;
       SDL_Renderer* gRenderer = nullptr;
-/*** END OF REFACTORED CODE ***/
       SDL_Texture* uiOverlay = nullptr;
+/*** END OF REFACTORED CODE ***/
 
       TTF_Font* gFont = nullptr;
@@ -176,5 +180,6 @@
 /*** END OF REFACTORED CODE ***/
       // (This was taken out of vulkan-game for now and replaced with Viewport)
-      // It will definitely be needed when creating render passes and I could use it in a few other places
+      // It will definitely be needed when creating command buffers and I could use it in a few other places
+      // TODO: Check above ^
       VkExtent2D swapChainExtent;
 /*** START OF REFACTORED CODE ***/
@@ -190,4 +195,5 @@
       vector<VkCommandBuffer> commandBuffers;
 
+/*** START OF REFACTORED CODE ***/
       // The images and the sampler are used to store data for specific attributes. I probably
       // want to keep them separate from the GraphicsPipelineInfo objects and start passing
@@ -197,4 +203,5 @@
       VkDeviceMemory depthImageMemory;
       VkImageView depthImageView;
+/*** END OF REFACTORED CODE ***/
 
       VkImage textureImage;
@@ -202,25 +209,25 @@
       VkImageView textureImageView;
 
-      VkImage overlayImage;
-      VkDeviceMemory overlayImageMemory;
-      VkImageView overlayImageView;
-
       VkImage sdlOverlayImage;
       VkDeviceMemory sdlOverlayImageMemory;
       VkImageView sdlOverlayImageView;
 
+/*** START OF REFACTORED CODE ***/
       VkSampler textureSampler;
 
       // These are currently to store the MVP matrix
       // I should figure out if it makes sense to use them for other uniforms in the future
-      // If not, I should rename them to better indicate their puprose.
+      // If not, I should rename them to better indicate their purpose.
       // I should also decide if I can use these for all shaders, or if I need a separapte set of buffers for each one
       vector<VkBuffer> uniformBuffers;
       vector<VkDeviceMemory> uniformBuffersMemory;
+/*** END OF REFACTORED CODE ***/
 
       VkDescriptorImageInfo sceneImageInfo;
       VkDescriptorImageInfo overlayImageInfo;
 
+/*** START OF REFACTORED CODE ***/
       vector<VkDescriptorBufferInfo> uniformBufferInfoList;
+/*** END OF REFACTORED CODE ***/
 
       GraphicsPipelineInfo scenePipeline;
@@ -257,5 +264,4 @@
             return RTWO_ERROR;
          }
-/*** END OF REFACTORED CODE ***/
 
          SDL_VERSION(&sdlVersion);
@@ -277,4 +283,5 @@
             cout << "Unable to set texture blend mode! SDL Error: " << SDL_GetError() << endl;
          }
+/*** END OF REFACTORED CODE ***/
 
          gFont = TTF_OpenFont("fonts/lazy.ttf", 28);
@@ -334,8 +341,6 @@
 /*** END OF REFACTORED CODE ***/
 
-         // THIS SECTION IS WHERE TEXTURES ARE CREATED, MAYBE SPLIT IT OFF INTO A SEPARATE FUNCTION
-         // MAY WANT TO CREATE A STRUCT TO HOLD SIMILAR VARIABLES< LIKE THOSE FOR A TEXTURE
-
          createImageResources("textures/texture.jpg", textureImage, textureImageMemory, textureImageView);
+/*** START OF REFACTORED CODE ***/
          createImageResourcesFromSDLTexture(uiOverlay, sdlOverlayImage, sdlOverlayImageMemory, sdlOverlayImageView);
          createTextureSampler();
@@ -350,4 +355,5 @@
          overlayImageInfo.imageView = sdlOverlayImageView;
          overlayImageInfo.sampler = textureSampler;
+/*** END OF REFACTORED CODE ***/
 
          // SHADER-SPECIFIC STUFF STARTS HERE
@@ -377,5 +383,4 @@
          addAttributeDescription(scenePipeline, VK_FORMAT_R32G32B32_SFLOAT, offset_of(&Vertex::color));
          addAttributeDescription(scenePipeline, VK_FORMAT_R32G32_SFLOAT, offset_of(&Vertex::texCoord));
-/*** END OF REFACTORED CODE ***/
 
          addDescriptorInfo(scenePipeline, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, &uniformBufferInfoList, nullptr);
@@ -383,4 +388,5 @@
 
          createDescriptorSetLayout(scenePipeline);
+/*** END OF REFACTORED CODE ***/
 
          numPlanes = 2;
@@ -403,9 +409,9 @@
          addAttributeDescription(overlayPipeline, VK_FORMAT_R32G32B32_SFLOAT, offset_of(&OverlayVertex::pos));
          addAttributeDescription(overlayPipeline, VK_FORMAT_R32G32_SFLOAT, offset_of(&OverlayVertex::texCoord));
-/*** END OF REFACTORED CODE ***/
 
          addDescriptorInfo(overlayPipeline, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr, &overlayImageInfo);
 
          createDescriptorSetLayout(overlayPipeline);
+/*** END OF REFACTORED CODE ***/
 
          createBufferResources();
@@ -847,4 +853,5 @@
       }
 
+/*** START OF REFACTORED CODE ***/
       void addDescriptorInfo(GraphicsPipelineInfo& info, VkDescriptorType type, VkShaderStageFlags stageFlags, vector<VkDescriptorBufferInfo>* bufferData, VkDescriptorImageInfo* imageData) {
          info.descriptorInfoList.push_back({ type, stageFlags, bufferData, imageData });
@@ -873,5 +880,4 @@
 
       void createGraphicsPipeline(string vertShaderFile, string fragShaderFile, GraphicsPipelineInfo& info) {
-/*** START OF REFACTORED CODE ***/
          vector<char> vertShaderCode = readFile(vertShaderFile);
          vector<char> fragShaderCode = readFile(fragShaderFile);
@@ -977,5 +983,4 @@
          pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
          pipelineLayoutInfo.setLayoutCount = 1;
-/*** END OF REFACTORED CODE ***/
          pipelineLayoutInfo.pSetLayouts = &info.descriptorSetLayout;
          pipelineLayoutInfo.pushConstantRangeCount = 0;
@@ -1007,9 +1012,8 @@
          }
 
-/*** START OF REFACTORED CODE ***/
          vkDestroyShaderModule(device, vertShaderModule, nullptr);
          vkDestroyShaderModule(device, fragShaderModule, nullptr);
-/*** END OF REFACTORED CODE ***/
-      }
+      }
+/*** END OF REFACTORED CODE ***/
 
       VkShaderModule createShaderModule(const vector<char>& code) {
@@ -1134,5 +1138,4 @@
          throw runtime_error("failed to find supported format!");
       }
-/*** END OF REFACTORED CODE ***/
 
       bool hasStencilComponent(VkFormat format) {
@@ -1191,4 +1194,5 @@
          view = createImageView(image, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
       }
+/*** END OF REFACTORED CODE ***/
 
       void populateImageFromSDLTexture(SDL_Texture* texture, VkImage& image) {
@@ -1386,5 +1390,4 @@
          return imageView;
       }
-/*** END OF REFACTORED CODE ***/
 
       void createTextureSampler() {
@@ -1413,4 +1416,5 @@
          }
       }
+/*** END OF REFACTORED CODE ***/
 
       void createVertexBuffer(GraphicsPipelineInfo& info, const void* vertexData, int vertexSize) {
@@ -1462,4 +1466,5 @@
       }
 
+/*** START OF REFACTORED CODE ***/
       void createUniformBuffers() {
          VkDeviceSize bufferSize = sizeof(UniformBufferObject);
@@ -1505,4 +1510,5 @@
          vkBindBufferMemory(device, buffer, bufferMemory, 0);
       }
+/*** END OF REFACTORED CODE ***/
 
       void copyDataToBuffer(const void* srcData, VkBuffer dst, VkDeviceSize dstOffset, VkDeviceSize dataSize) {
@@ -1567,4 +1573,5 @@
       }
 
+/*** START OF REFACTORED CODE ***/
       uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {
          VkPhysicalDeviceMemoryProperties memProperties;
@@ -1642,4 +1649,5 @@
          }
       }
+/*** END OF REFACTORED CODE ***/
 
       void createCommandBuffers() {
@@ -1948,14 +1956,20 @@
 
       void createBufferResources() {
+         // TODO: The three functions below will be called in vulkan-game following the
+         // pipeline creation (createDescriptorSets()), and before createCommandBuffers()
          createDepthResources();
          createFramebuffers();
          createUniformBuffers();
 
+/*** START OF REFACTORED CODE ***/
          createGraphicsPipeline("shaders/scene-vert.spv", "shaders/scene-frag.spv", scenePipeline);
          createDescriptorPool(scenePipeline);
+/*** END OF REFACTORED CODE ***/
          createDescriptorSets(scenePipeline);
 
+/*** START OF REFACTORED CODE ***/
          createGraphicsPipeline("shaders/overlay-vert.spv", "shaders/overlay-frag.spv", overlayPipeline);
          createDescriptorPool(overlayPipeline);
+/*** END OF REFACTORED CODE ***/
          createDescriptorSets(overlayPipeline);
 
@@ -1963,6 +1977,6 @@
       }
 
-/*** START OF REFACTORED CODE ***/
       void cleanup() {
+/*** START OF REFACTORED CODE ***/
          cleanupSwapChain();
 /*** END OF REFACTORED CODE ***/
@@ -1974,14 +1988,12 @@
          vkFreeMemory(device, textureImageMemory, nullptr);
 
-         vkDestroyImageView(device, overlayImageView, nullptr);
-         vkDestroyImage(device, overlayImage, nullptr);
-         vkFreeMemory(device, overlayImageMemory, nullptr);
-
          vkDestroyImageView(device, sdlOverlayImageView, nullptr);
          vkDestroyImage(device, sdlOverlayImage, nullptr);
          vkFreeMemory(device, sdlOverlayImageMemory, nullptr);
 
+/*** START OF REFACTORED CODE ***/
          cleanupPipelineBuffers(scenePipeline);
          cleanupPipelineBuffers(overlayPipeline);
+ /*** END OF REFACTORED CODE ***/
 
          for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
@@ -2001,5 +2013,4 @@
 
          vkDestroyInstance(instance, nullptr);
-/*** END OF REFACTORED CODE ***/
 
          // TODO: Check if any of these functions accept null parameters
@@ -2010,4 +2021,5 @@
             uiOverlay = nullptr;
          }
+/*** END OF REFACTORED CODE ***/
 
          TTF_CloseFont(gFont);
@@ -2045,8 +2057,8 @@
          vkFreeCommandBuffers(device, commandPool, static_cast<uint32_t>(commandBuffers.size()), commandBuffers.data());
 
+/*** START OF REFACTORED CODE ***/
          cleanupPipeline(scenePipeline);
          cleanupPipeline(overlayPipeline);
 
-/*** START OF REFACTORED CODE ***/
          vkDestroyRenderPass(device, renderPass, nullptr);
 
@@ -2062,8 +2074,7 @@
             vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
          }
-/*** START OF REFACTORED CODE ***/
-      }
-/*** END OF REFACTORED CODE ***/
-
+      }
+
+/*** START OF REFACTORED CODE ***/
       void cleanupPipeline(GraphicsPipelineInfo& pipeline) {
          vkDestroyPipeline(device, pipeline.pipeline, nullptr);
@@ -2071,7 +2082,10 @@
          vkDestroyPipelineLayout(device, pipeline.pipelineLayout, nullptr);
       }
+/*** END OF REFACTORED CODE ***/
 
       void cleanupPipelineBuffers(GraphicsPipelineInfo& pipeline) {
+/*** START OF REFACTORED CODE ***/
          vkDestroyDescriptorSetLayout(device, pipeline.descriptorSetLayout, nullptr);
+/*** END OF REFACTORED CODE ***/
 
          vkDestroyBuffer(device, pipeline.vertexBuffer, nullptr);
Index: vulkan-utils.cpp
===================================================================
--- vulkan-utils.cpp	(revision 771b33a137aed1be0c097e401feb8e3727bb1166)
+++ vulkan-utils.cpp	(revision b79417880394af692ba165be4054f0843fdb1c91)
@@ -4,5 +4,9 @@
 #include <set>
 #include <stdexcept>
-#include <string>
+
+#define STB_IMAGE_IMPLEMENTATION
+#include "stb_image.h" // TODO: Probably switch to SDL_image
+
+// TODO: Remove all instances of auto
 
 bool VulkanUtils::checkValidationLayerSupport(const vector<const char*> &validationLayers) {
@@ -210,2 +214,263 @@
    throw runtime_error("failed to find supported format!");
 }
+
+void VulkanUtils::createBuffer(VkDevice device, VkPhysicalDevice physicalDevice, VkDeviceSize size, VkBufferUsageFlags usage,
+      VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) {
+   VkBufferCreateInfo bufferInfo = {};
+   bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
+   bufferInfo.size = size;
+   bufferInfo.usage = usage;
+   bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+
+   if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
+      throw runtime_error("failed to create buffer!");
+   }
+
+   VkMemoryRequirements memRequirements;
+   vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
+
+   VkMemoryAllocateInfo allocInfo = {};
+   allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+   allocInfo.allocationSize = memRequirements.size;
+   allocInfo.memoryTypeIndex = findMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);
+
+   if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
+      throw runtime_error("failed to allocate buffer memory!");
+   }
+
+   vkBindBufferMemory(device, buffer, bufferMemory, 0);
+}
+
+uint32_t VulkanUtils::findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties) {
+   VkPhysicalDeviceMemoryProperties memProperties;
+   vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
+
+   for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
+      if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
+         return i;
+      }
+   }
+
+   throw runtime_error("failed to find suitable memory type!");
+}
+
+void VulkanUtils::createVulkanImageFromFile(VkDevice device, VkPhysicalDevice physicalDevice,
+      VkCommandPool commandPool, string filename, VulkanImage& image, VkQueue graphicsQueue) {
+   int texWidth, texHeight, texChannels;
+
+   stbi_uc* pixels = stbi_load(filename.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
+   VkDeviceSize imageSize = texWidth * texHeight * 4;
+
+   if (!pixels) {
+      throw runtime_error("failed to load texture image!");
+   }
+
+   VkBuffer stagingBuffer;
+   VkDeviceMemory stagingBufferMemory;
+
+   createBuffer(device, physicalDevice, imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
+      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
+      stagingBuffer, stagingBufferMemory);
+
+   void* data;
+
+   vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
+   memcpy(data, pixels, static_cast<size_t>(imageSize));
+   vkUnmapMemory(device, stagingBufferMemory);
+
+   stbi_image_free(pixels);
+
+   createImage(device, physicalDevice, texWidth, texHeight, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL,
+      VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, image);
+
+   transitionImageLayout(device, commandPool, image.image, VK_FORMAT_R8G8B8A8_UNORM,
+      VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, graphicsQueue);
+   copyBufferToImage(device, commandPool, stagingBuffer, image.image,
+      static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), graphicsQueue);
+   transitionImageLayout(device, commandPool, image.image, VK_FORMAT_R8G8B8A8_UNORM,
+      VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, graphicsQueue);
+
+   vkDestroyBuffer(device, stagingBuffer, nullptr);
+   vkFreeMemory(device, stagingBufferMemory, nullptr);
+
+   image.imageView = createImageView(device, image.image, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
+}
+
+void VulkanUtils::createVulkanImageFromSDLTexture(VkDevice device, VkPhysicalDevice physicalDevice,
+      SDL_Texture* texture, VulkanImage& image) {
+   int a, w, h;
+
+   // I only need this here for the width and height, which are constants, so just use those instead
+   SDL_QueryTexture(texture, nullptr, &a, &w, &h);
+
+   createImage(device, physicalDevice, w, h, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL,
+      VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, image);
+
+   image.imageView = createImageView(device, image.image, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
+}
+
+void VulkanUtils::createImage(VkDevice device, VkPhysicalDevice physicalDevice, uint32_t width, uint32_t height,
+      VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties,
+      VulkanImage& image) {
+   VkImageCreateInfo imageInfo = {};
+   imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
+   imageInfo.imageType = VK_IMAGE_TYPE_2D;
+   imageInfo.extent.width = width;
+   imageInfo.extent.height = height;
+   imageInfo.extent.depth = 1;
+   imageInfo.mipLevels = 1;
+   imageInfo.arrayLayers = 1;
+   imageInfo.format = format;
+   imageInfo.tiling = tiling;
+   imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+   imageInfo.usage = usage;
+   imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
+   imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+
+   if (vkCreateImage(device, &imageInfo, nullptr, &image.image) != VK_SUCCESS) {
+      throw runtime_error("failed to create image!");
+   }
+
+   VkMemoryRequirements memRequirements;
+   vkGetImageMemoryRequirements(device, image.image, &memRequirements);
+
+   VkMemoryAllocateInfo allocInfo = {};
+   allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+   allocInfo.allocationSize = memRequirements.size;
+   allocInfo.memoryTypeIndex = findMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);
+
+   if (vkAllocateMemory(device, &allocInfo, nullptr, &image.imageMemory) != VK_SUCCESS) {
+      throw runtime_error("failed to allocate image memory!");
+   }
+
+   vkBindImageMemory(device, image.image, image.imageMemory, 0);
+}
+
+void VulkanUtils::transitionImageLayout(VkDevice device, VkCommandPool commandPool, VkImage image,
+      VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout, VkQueue graphicsQueue) {
+   VkCommandBuffer commandBuffer = beginSingleTimeCommands(device, commandPool);
+
+   VkImageMemoryBarrier barrier = {};
+   barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
+   barrier.oldLayout = oldLayout;
+   barrier.newLayout = newLayout;
+   barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+   barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+   barrier.image = image;
+
+   if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
+      barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
+
+      if (hasStencilComponent(format)) {
+         barrier.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
+      }
+   } else {
+      barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+   }
+
+   barrier.subresourceRange.baseMipLevel = 0;
+   barrier.subresourceRange.levelCount = 1;
+   barrier.subresourceRange.baseArrayLayer = 0;
+   barrier.subresourceRange.layerCount = 1;
+
+   VkPipelineStageFlags sourceStage;
+   VkPipelineStageFlags destinationStage;
+
+   if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
+      barrier.srcAccessMask = 0;
+      barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+
+      sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
+      destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
+   } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
+      barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+      barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+
+      sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
+      destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
+   } else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
+      barrier.srcAccessMask = 0;
+      barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+
+      sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
+      destinationStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
+   } else {
+      throw invalid_argument("unsupported layout transition!");
+   }
+
+   vkCmdPipelineBarrier(
+      commandBuffer,
+      sourceStage, destinationStage,
+      0,
+      0, nullptr,
+      0, nullptr,
+      1, &barrier
+   );
+
+   endSingleTimeCommands(device, commandPool, commandBuffer, graphicsQueue);
+}
+
+VkCommandBuffer VulkanUtils::beginSingleTimeCommands(VkDevice device, VkCommandPool commandPool) {
+   VkCommandBufferAllocateInfo allocInfo = {};
+   allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
+   allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
+   allocInfo.commandPool = commandPool;
+   allocInfo.commandBufferCount = 1;
+
+   VkCommandBuffer commandBuffer;
+   vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
+
+   VkCommandBufferBeginInfo beginInfo = {};
+   beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+   beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
+
+   vkBeginCommandBuffer(commandBuffer, &beginInfo);
+
+   return commandBuffer;
+}
+
+void VulkanUtils::endSingleTimeCommands(VkDevice device, VkCommandPool commandPool,
+      VkCommandBuffer commandBuffer, VkQueue graphicsQueue) {
+   vkEndCommandBuffer(commandBuffer);
+
+   VkSubmitInfo submitInfo = {};
+   submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+   submitInfo.commandBufferCount = 1;
+   submitInfo.pCommandBuffers = &commandBuffer;
+
+   vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
+   vkQueueWaitIdle(graphicsQueue);
+
+   vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
+}
+
+void VulkanUtils::copyBufferToImage(VkDevice device, VkCommandPool commandPool, VkBuffer buffer,
+      VkImage image, uint32_t width, uint32_t height, VkQueue graphicsQueue) {
+   VkCommandBuffer commandBuffer = beginSingleTimeCommands(device, commandPool);
+
+   VkBufferImageCopy region = {};
+   region.bufferOffset = 0;
+   region.bufferRowLength = 0;
+   region.bufferImageHeight = 0;
+   region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+   region.imageSubresource.mipLevel = 0;
+   region.imageSubresource.baseArrayLayer = 0;
+   region.imageSubresource.layerCount = 1;
+   region.imageOffset = { 0, 0, 0 };
+   region.imageExtent = { width, height, 1 };
+
+   vkCmdCopyBufferToImage(
+      commandBuffer,
+      buffer,
+      image,
+      VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+      1,
+      &region
+   );
+
+   endSingleTimeCommands(device, commandPool, commandBuffer, graphicsQueue);
+}
+
+bool VulkanUtils::hasStencilComponent(VkFormat format) {
+   return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT;
+}
Index: vulkan-utils.hpp
===================================================================
--- vulkan-utils.hpp	(revision 771b33a137aed1be0c097e401feb8e3727bb1166)
+++ vulkan-utils.hpp	(revision b79417880394af692ba165be4054f0843fdb1c91)
@@ -3,7 +3,12 @@
 
 #include <optional>
+#include <string>
 #include <vector>
 
 #include <vulkan/vulkan.h>
+
+#include <SDL2/SDL.h>
+#include <SDL2/SDL_image.h>
+#include <SDL2/SDL_vulkan.h>
 
 using namespace std;
@@ -24,4 +29,10 @@
 };
 
+struct VulkanImage {
+   VkImage image;
+   VkDeviceMemory imageMemory;
+   VkImageView imageView;
+};
+
 class VulkanUtils {
    public:
@@ -38,12 +49,38 @@
 
       static QueueFamilyIndices findQueueFamilies(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
-      static bool checkDeviceExtensionSupport(VkPhysicalDevice physicalDevice, const vector<const char*>& deviceExtensions);
-      static SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
+      static bool checkDeviceExtensionSupport(VkPhysicalDevice physicalDevice,
+            const vector<const char*>& deviceExtensions);
+      static SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice physicalDevice,
+            VkSurfaceKHR surface);
       static VkSurfaceFormatKHR chooseSwapSurfaceFormat(const vector<VkSurfaceFormatKHR>& availableFormats);
       static VkPresentModeKHR chooseSwapPresentMode(const vector<VkPresentModeKHR>& availablePresentModes);
       static VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities, int width, int height);
-      static VkImageView createImageView(VkDevice device, VkImage image, VkFormat format, VkImageAspectFlags aspectFlags);
+      static VkImageView createImageView(VkDevice device, VkImage image, VkFormat format,
+            VkImageAspectFlags aspectFlags);
       static VkFormat findSupportedFormat(VkPhysicalDevice physicalDevice, const vector<VkFormat>& candidates,
             VkImageTiling tiling, VkFormatFeatureFlags features);
+      static void createBuffer(VkDevice device, VkPhysicalDevice physicalDevice, VkDeviceSize size,
+            VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer,
+            VkDeviceMemory& bufferMemory);
+      static uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter,
+            VkMemoryPropertyFlags properties);
+
+      static void createVulkanImageFromFile(VkDevice device, VkPhysicalDevice physicalDevice,
+            VkCommandPool commandPool, string filename, VulkanImage& image, VkQueue graphicsQueue);
+      static void createVulkanImageFromSDLTexture(VkDevice device, VkPhysicalDevice physicalDevice,
+            SDL_Texture* texture, VulkanImage& image);
+      static void createImage(VkDevice device, VkPhysicalDevice physicalDevice, uint32_t width, uint32_t height,
+            VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties,
+            VulkanImage& image);
+
+      static void transitionImageLayout(VkDevice device, VkCommandPool commandPool, VkImage image,
+            VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout, VkQueue graphicsQueue);
+      static VkCommandBuffer beginSingleTimeCommands(VkDevice device, VkCommandPool commandPool);
+      static void endSingleTimeCommands(VkDevice device, VkCommandPool commandPool,
+            VkCommandBuffer commandBuffer, VkQueue graphicsQueue);
+      static void copyBufferToImage(VkDevice device, VkCommandPool commandPool, VkBuffer buffer, VkImage image,
+            uint32_t width, uint32_t height, VkQueue graphicsQueue);
+
+      static bool hasStencilComponent(VkFormat format);
 };
 
