Index: sdl-game.cpp
===================================================================
--- sdl-game.cpp	(revision 5081b9a818ca07c114b3f94c82df8bc279787da6)
+++ sdl-game.cpp	(revision 4a777d2b3a0f0dff81e2a81bb45124f3c448bf47)
@@ -8,4 +8,5 @@
 
 #include "logger.hpp"
+#include "utils.hpp"
 
 #include "gui/imgui/button-imgui.hpp"
@@ -89,4 +90,71 @@
    initImGuiOverlay();
 
+   // TODO: Figure out how much of ubo creation and associated variables should be in the pipeline class
+   // Maybe combine the ubo-related objects into a new class
+
+   initGraphicsPipelines();
+
+   initMatrices();
+
+   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::pos));
+   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::color));
+   modelPipeline.addAttribute(VK_FORMAT_R32G32_SFLOAT, offset_of(&ModelVertex::texCoord));
+   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::normal));
+   modelPipeline.addAttribute(VK_FORMAT_R32_UINT, offset_of(&ModelVertex::objIndex));
+
+   createBufferSet(sizeof(UBO_VP_mats), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
+      uniformBuffers_modelPipeline, uniformBuffersMemory_modelPipeline, uniformBufferInfoList_modelPipeline);
+
+   modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+      VK_SHADER_STAGE_VERTEX_BIT, &uniformBufferInfoList_modelPipeline);
+   modelPipeline.addStorageDescriptor(VK_SHADER_STAGE_VERTEX_BIT);
+   modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+      VK_SHADER_STAGE_FRAGMENT_BIT, &floorTextureImageDescriptor);
+
+   SceneObject<ModelVertex, SSBO_ModelObject>* texturedSquare = nullptr;
+
+   texturedSquare = &addObject(modelObjects, modelPipeline,
+      addObjectIndex<ModelVertex>(modelObjects.size(),
+         addVertexNormals<ModelVertex>({
+            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0}
+         })), {
+            0, 1, 2, 3, 4, 5
+      }, {
+         mat4(1.0f)
+      }, false);
+
+   texturedSquare->model_base =
+      translate(mat4(1.0f), vec3(0.0f, 0.0f, -2.0f));
+   texturedSquare->modified = true;
+
+   texturedSquare = &addObject(modelObjects, modelPipeline,
+      addObjectIndex<ModelVertex>(modelObjects.size(),
+         addVertexNormals<ModelVertex>({
+            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}},
+            {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}},
+            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
+            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
+            {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}},
+            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}}
+         })), {
+            0, 1, 2, 3, 4, 5
+      }, {
+         mat4(1.0f)
+      }, false);
+
+   texturedSquare->model_base =
+      translate(mat4(1.0f), vec3(0.0f, 0.0f, -1.5f));
+   texturedSquare->modified = true;
+
+   modelPipeline.createDescriptorSetLayout();
+   modelPipeline.createPipeline("shaders/model-vert.spv", "shaders/model-frag.spv");
+   modelPipeline.createDescriptorPool(swapChainImages);
+   modelPipeline.createDescriptorSets(swapChainImages);
+
    currentRenderScreenFn = &VulkanGame::renderMainScreen;
 
@@ -182,4 +250,31 @@
    createCommandBuffers();
    createSyncObjects();
+}
+
+void VulkanGame::initGraphicsPipelines() {
+   modelPipeline = GraphicsPipeline_Vulkan<ModelVertex, SSBO_ModelObject>(
+      VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, physicalDevice, device, renderPass,
+      { 0, 0, (int)swapChainExtent.width, (int)swapChainExtent.height }, swapChainImages, 16, 24, 10);
+}
+
+// TODO: Maybe change the name to initScene() or something similar
+void VulkanGame::initMatrices() {
+   cam_pos = vec3(0.0f, 0.0f, 2.0f);
+
+   float cam_yaw = 0.0f;
+   float cam_pitch = -50.0f;
+
+   mat4 yaw_mat = rotate(mat4(1.0f), radians(-cam_yaw), vec3(0.0f, 1.0f, 0.0f));
+   mat4 pitch_mat = rotate(mat4(1.0f), radians(-cam_pitch), vec3(1.0f, 0.0f, 0.0f));
+
+   mat4 R_view = pitch_mat * yaw_mat;
+   mat4 T_view = translate(mat4(1.0f), vec3(-cam_pos.x, -cam_pos.y, -cam_pos.z));
+   viewMat = R_view * T_view;
+
+   projMat = perspective(radians(FOV_ANGLE), (float)swapChainExtent.width / (float)swapChainExtent.height, NEAR_CLIP, FAR_CLIP);
+   projMat[1][1] *= -1; // flip the y-axis so that +y is up
+
+   object_VP_mats.view = viewMat;
+   object_VP_mats.proj = projMat;
 }
 
@@ -241,4 +336,40 @@
                cout << "Window resize event detected" << endl;
                shouldRecreateSwapChain = true;
+               break;
+            case UI_EVENT_KEYDOWN:
+               if (e.key.repeat) {
+                  break;
+               }
+
+               if (e.key.keycode == SDL_SCANCODE_ESCAPE) {
+                  done = true;
+               } else if (e.key.keycode == SDL_SCANCODE_SPACE) {
+                  cout << "Adding a plane" << endl;
+                  float zOffset = -2.0f + (0.5f * modelObjects.size());
+
+                  SceneObject<ModelVertex, SSBO_ModelObject>& texturedSquare =
+                     addObject(modelObjects, modelPipeline,
+                        addObjectIndex<ModelVertex>(modelObjects.size(),
+                           addVertexNormals<ModelVertex>({
+                              {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0}
+                           })), {
+                              0, 1, 2, 3, 4, 5
+                        }, {
+                           mat4(1.0f)
+                        }, true);
+
+                  texturedSquare.model_base =
+                     translate(mat4(1.0f), vec3(0.0f, 0.0f, zOffset));
+                  texturedSquare.modified = true;
+               // START UNREVIEWED SECTION
+               // END UNREVIEWED SECTION
+               } else {
+                  cout << "Key event detected" << endl;
+               }
                break;
             case UI_EVENT_KEYUP:
@@ -277,4 +408,6 @@
       }
 
+      updateScene();
+
       // TODO: Move this into a renderImGuiOverlay() function
       ImGui_ImplVulkan_NewFrame();
@@ -296,4 +429,24 @@
 }
 
+// TODO: The only updates that need to happen once per Vulkan image are the SSBO ones,
+// which are already handled by updateObject(). Move this code to a different place,
+// where it will run just once per frame
+void VulkanGame::updateScene() {
+   for (SceneObject<ModelVertex, SSBO_ModelObject>& model : this->modelObjects) {
+      model.model_transform =
+         translate(mat4(1.0f), vec3(0.0f, -2.0f, -0.0f)) *
+         rotate(mat4(1.0f), curTime * radians(90.0f), vec3(0.0f, 0.0f, 1.0f));
+      model.modified = true;
+   }
+
+   for (size_t i = 0; i < modelObjects.size(); i++) {
+      if (modelObjects[i].modified) {
+         updateObject(modelObjects, modelPipeline, i);
+      }
+   }
+
+   VulkanUtils::copyDataToMemory(device, uniformBuffersMemory_modelPipeline[imageIndex], 0, object_VP_mats);
+}
+
 void VulkanGame::cleanup() {
    // FIXME: We could wait on the Queue if we had the queue in wd-> (otherwise VulkanH functions can't use globals)
@@ -304,4 +457,13 @@
 
    cleanupSwapChain();
+
+   VulkanUtils::destroyVulkanImage(device, floorTextureImage);
+   // START UNREVIEWED SECTION
+
+   vkDestroySampler(device, textureSampler, nullptr);
+
+   modelPipeline.cleanupBuffers();
+
+   // END UNREVIEWED SECTION
 
    vkDestroyCommandPool(device, resourceCommandPool, nullptr);
@@ -638,4 +800,16 @@
    VulkanUtils::createDepthImage(device, physicalDevice, resourceCommandPool, findDepthFormat(), swapChainExtent,
       depthImage, graphicsQueue);
+
+   createTextureSampler();
+
+   // TODO: Move all images/textures somewhere into the assets folder
+
+   VulkanUtils::createVulkanImageFromFile(device, physicalDevice, resourceCommandPool, "textures/texture.jpg",
+      floorTextureImage, graphicsQueue);
+
+   floorTextureImageDescriptor = {};
+   floorTextureImageDescriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+   floorTextureImageDescriptor.imageView = floorTextureImage.imageView;
+   floorTextureImageDescriptor.sampler = textureSampler;
 }
 
@@ -726,4 +900,29 @@
       }
    }
+}
+
+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;
+
+   VKUTIL_CHECK_RESULT(vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler),
+      "failed to create texture sampler!");
 }
 
@@ -881,4 +1080,22 @@
 }
 
+void VulkanGame::createBufferSet(VkDeviceSize bufferSize, VkBufferUsageFlags flags,
+                                 vector<VkBuffer>& buffers, vector<VkDeviceMemory>& buffersMemory,
+                                 vector<VkDescriptorBufferInfo>& bufferInfoList) {
+   buffers.resize(swapChainImageCount);
+   buffersMemory.resize(swapChainImageCount);
+   bufferInfoList.resize(swapChainImageCount);
+
+   for (size_t i = 0; i < swapChainImageCount; i++) {
+      VulkanUtils::createBuffer(device, physicalDevice, bufferSize, flags,
+         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
+         buffers[i], buffersMemory[i]);
+
+      bufferInfoList[i].buffer = buffers[i];
+      bufferInfoList[i].offset = 0; // This is the offset from the start of the buffer, so always 0 for now
+      bufferInfoList[i].range = bufferSize; // Size of the update starting from offset, or VK_WHOLE_SIZE
+   }
+}
+
 void VulkanGame::renderFrame(ImDrawData* draw_data) {
    VkResult result = vkAcquireNextImageKHR(device, swapChain, numeric_limits<uint64_t>::max(),
@@ -926,4 +1143,13 @@
 
    vkCmdBeginRenderPass(commandBuffers[imageIndex], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
+
+   // TODO: Find a more elegant, per-screen solution for this
+   if (currentRenderScreenFn == &VulkanGame::renderGameScreen) {
+      modelPipeline.createRenderCommands(commandBuffers[imageIndex], imageIndex);
+
+
+
+
+   }
 
    ImGui_ImplVulkan_RenderDrawData(draw_data, commandBuffers[imageIndex]);
@@ -999,5 +1225,15 @@
    createSyncObjects();
 
-   // TODO: Update pipelines here
+   // TODO: Move UBO creation/management into GraphicsPipeline_Vulkan, like I did with SSBOs
+   // TODO: Check if the shader stages and maybe some other properties of the pipeline can be re-used
+   // instead of recreated every time
+
+   createBufferSet(sizeof(UBO_VP_mats), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
+      uniformBuffers_modelPipeline, uniformBuffersMemory_modelPipeline, uniformBufferInfoList_modelPipeline);
+
+   modelPipeline.updateRenderPass(renderPass);
+   modelPipeline.createPipeline("shaders/model-vert.spv", "shaders/model-frag.spv");
+   modelPipeline.createDescriptorPool(swapChainImages);
+   modelPipeline.createDescriptorSets(swapChainImages);
 
    imageIndex = 0;
@@ -1014,4 +1250,11 @@
       vkFreeCommandBuffers(device, commandPools[i], 1, &commandBuffers[i]);
       vkDestroyCommandPool(device, commandPools[i], nullptr);
+   }
+
+   modelPipeline.cleanup();
+
+   for (size_t i = 0; i < uniformBuffers_modelPipeline.size(); i++) {
+      vkDestroyBuffer(device, uniformBuffers_modelPipeline[i], nullptr);
+      vkFreeMemory(device, uniformBuffersMemory_modelPipeline[i], nullptr);
    }
 
