From b6a2d5bf11f65552e5ddb94f93c1c55ab11edfb1 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Sun, 6 Sep 2026 23:40:49 -0700 Subject: [PATCH] Updates --- engine/src/engine.cpp | 82 ----------------- engine/src/engine.hpp | 37 -------- engine/src/model.cpp | 124 +++++++++++++++++++++++++ engine/src/model.hpp | 66 ++++++++++++++ engine/src/render_context.cpp | 165 ++++++++++++++++++++++++++++++++++ engine/src/render_context.hpp | 68 ++++++++++++++ 6 files changed, 423 insertions(+), 119 deletions(-) delete mode 100644 engine/src/engine.cpp delete mode 100644 engine/src/engine.hpp create mode 100644 engine/src/model.cpp create mode 100644 engine/src/model.hpp create mode 100644 engine/src/render_context.cpp create mode 100644 engine/src/render_context.hpp diff --git a/engine/src/engine.cpp b/engine/src/engine.cpp deleted file mode 100644 index 753c467..0000000 --- a/engine/src/engine.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "engine.hpp" - -#include -#include -#include -#include - -namespace sophia { - -Engine::Engine(const Config& config) : config{config} { - if (config.headless) { - this->device = Device::Builder().headless().build(); - } else { - this->window = - std::make_unique(config.width, config.height, config.name); - this->device = Device::Builder().withWindow(*this->window).build(); - } - - // this->imguiDescriptorPool = - // DescriptorPool::Builder(this->device) - // .addPoolSize(vk::DescriptorType::eCombinedImageSampler, - // IMGUI_IMPL_VULKAN_MINIMUM_IMAGE_SAMPLER_POOL_SIZE - // + 5) - // .setPoolFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet) - // .setMaxSets(IMGUI_IMPL_VULKAN_MINIMUM_IMAGE_SAMPLER_POOL_SIZE - // + 5) - // .build(); - - // this->uiManager = - // UIManager::Builder(this->window, this->device, this->renderer, - // imguiDescriptorPool->get()) - // .darkTheme() - // .setDocking(true) - // .setKeyboard(true) - // .build(); -}; - -Engine::~Engine() { this->device->waitIdle(); }; - -void Engine::run() { - auto startTime = std::chrono::high_resolution_clock::now(); - auto currentTime = startTime; - - // EventSystem::get().addListener( - // std::bind(&Application::onEvent, this, std::placeholders::_1)); - - while (this->isRunning) { - if (this->window) { - glfwPollEvents(); - } - - auto newTime = std::chrono::high_resolution_clock::now(); - double deltaTime = - std::chrono::duration(newTime - currentTime).count(); - currentTime = newTime; - - // // Attempt to start a new frame - // vk::CommandBuffer commandBuffer = this->renderer.beginFrame(); - // if (commandBuffer != nullptr) { - // double elapsedTime = - // std::chrono::duration(currentTime - startTime).count(); - - // vk::Extent2D extent = this->renderer.getCurrentFramebufferExtent(); - // glm::vec2 extentVec2(static_cast(extent.width), - // static_cast(extent.height)); - - // FrameInfo frameInfo{this->renderer.getFrameIndex(), elapsedTime, - // deltaTime, extentVec2}; - // } - - this->isRunning = false; - } - - this->device->waitIdle(); - - auto endTime = std::chrono::high_resolution_clock::now(); - double totalRuntime = - std::chrono::duration(endTime - startTime).count(); - log::info("Application ran for", totalRuntime, "s"); -} - -} // namespace sophia diff --git a/engine/src/engine.hpp b/engine/src/engine.hpp deleted file mode 100644 index da893fc..0000000 --- a/engine/src/engine.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include - -#include "device.hpp" -#include "window.hpp" - -namespace sophia { - -class Engine { - public: - struct Config { - int width; - int height; - std::string name; - bool headless = false; - }; - - Engine(const Engine&) = delete; - Engine& operator=(const Engine&) = delete; - - Engine(const Config& config); - ~Engine(); - - void run(); - - private: - Config config; - - std::unique_ptr window; - std::unique_ptr device; - - bool isRunning = true; -}; - -} // namespace sophia diff --git a/engine/src/model.cpp b/engine/src/model.cpp new file mode 100644 index 0000000..b84b70e --- /dev/null +++ b/engine/src/model.cpp @@ -0,0 +1,124 @@ +#include "model.hpp" + +#include +#include + +namespace sophia { + +std::vector +Model::Vertex::getBindingDescriptions() { + std::vector bindingDescriptions(1); + bindingDescriptions[0].binding = 0; + bindingDescriptions[0].stride = sizeof(Vertex); + bindingDescriptions[0].inputRate = vk::VertexInputRate::eVertex; + + return bindingDescriptions; +} + +std::vector +Model::Vertex::getAttributeDescriptions() { + std::vector attributeDescriptions{}; + + attributeDescriptions.push_back( + {0, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, position)}); + attributeDescriptions.push_back( + {1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)}); + // attributeDescriptions.push_back( + // {2, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, normal)}); + // attributeDescriptions.push_back( + // {3, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, uv)}); + + return attributeDescriptions; +} + +Model::Model(Device& device, const Builder& builder) : device{device} { + createVertexBuffers(builder.vertices); + createIndexBuffers(builder.indices); +} + +Model::~Model() {} + +void Model::bind(vk::CommandBuffer commandBuffer) { + vk::Buffer buffers[] = {this->vertexBuffer->getBuffer()}; + vk::DeviceSize offset[] = {0}; + commandBuffer.bindVertexBuffers(0, 1, buffers, offset); + + if (this->hasIndexBuffer) { + commandBuffer.bindIndexBuffer(this->indexBuffer->getBuffer(), 0, + vk::IndexType::eUint32); + } +} + +void Model::draw(vk::CommandBuffer commandBuffer) { + if (this->hasIndexBuffer) { + commandBuffer.drawIndexed(this->indexCount, 1, 0, 0, 0); + } else { + commandBuffer.draw(this->vertexCount, 1, 0, 0); + } +} + +void Model::createVertexBuffers(const std::vector& vertices) { + this->vertexCount = static_cast(vertices.size()); + assert(this->vertexCount >= 3 && "vertex count must be at least 3"); + + vk::DeviceSize bufferSize = sizeof(vertices[0]) * this->vertexCount; + + u32 vertexSize = sizeof(vertices[0]); + + Buffer stagingBuffer{ + this->device, + vertexSize, + vertexCount, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent, + }; + + stagingBuffer.map(); + stagingBuffer.writeToBuffer((void*)vertices.data()); + + vertexBuffer = + std::make_unique(this->device, vertexSize, vertexCount, + vk::BufferUsageFlagBits::eTransferDst | + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + this->device.copyBuffer(stagingBuffer.getBuffer(), + this->vertexBuffer->getBuffer(), bufferSize); +} + +void Model::createIndexBuffers(const std::vector& indicies) { + this->indexCount = static_cast(indicies.size()); + this->hasIndexBuffer = this->indexCount > 0; + + if (!this->hasIndexBuffer) { + return; + } + + vk::DeviceSize bufferSize = sizeof(indicies[0]) * this->indexCount; + + u32 indexSize = sizeof(indicies[0]); + + Buffer stagingBuffer{ + this->device, + indexSize, + indexCount, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent, + }; + + stagingBuffer.map(); + stagingBuffer.writeToBuffer((void*)indicies.data()); + + indexBuffer = + std::make_unique(this->device, indexSize, indexCount, + vk::BufferUsageFlagBits::eTransferDst | + vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + this->device.copyBuffer(stagingBuffer.getBuffer(), + this->indexBuffer->getBuffer(), bufferSize); +} + +} // namespace sophia diff --git a/engine/src/model.hpp b/engine/src/model.hpp new file mode 100644 index 0000000..d3247de --- /dev/null +++ b/engine/src/model.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include + +#include "buffer.hpp" +#include "device.hpp" + +namespace sophia { + +class Model { + public: + struct Vertex { + glm::vec2 position{}; + glm::vec3 color{}; + // glm::vec3 normal{}; + // glm::vec2 uv{}; + + static std::vector + getBindingDescriptions(); + static std::vector + getAttributeDescriptions(); + + // bool operator==(const Vertex& other) const { + // return position == other.position && color == other.color; + // } + }; + + struct Builder { + std::vector vertices{}; + std::vector indices{}; + + // void loadModel(const std::string& filepath); + }; + + Model(Device& device, const Model::Builder& builder); + ~Model(); + + Model(const Model&) = delete; + Model& operator=(const Model&) = delete; + + // static std::unique_ptr createModelFromFile( + // Device& device, const std::string& filepath); + + void bind(vk::CommandBuffer commandBuffer); + void draw(vk::CommandBuffer commandBuffer); + + private: + void createVertexBuffers(const std::vector& vertices); + void createIndexBuffers(const std::vector& indices); + + Device& device; + + std::unique_ptr vertexBuffer; + u32 vertexCount; + + bool hasIndexBuffer = false; + std::unique_ptr indexBuffer; + u32 indexCount; +}; + +} // namespace sophia diff --git a/engine/src/render_context.cpp b/engine/src/render_context.cpp new file mode 100644 index 0000000..25dfd3e --- /dev/null +++ b/engine/src/render_context.cpp @@ -0,0 +1,165 @@ +#include "render_context.hpp" + +#include +#include +#include +#include + +namespace sophia { + +RenderContext::RenderContext(Window& window, Device& device) + : window{window}, device{device} { + createFrames(); + recreateSwapchain(); +} + +RenderContext::~RenderContext() { this->device.waitIdle(); } + +vk::CommandBuffer RenderContext::beginFrame() { + assert(!this->isFrameStarted && + "Can't call beginFrame while already in progress"); + + FrameData& frame = this->frames[this->currentFrameIndex]; + + vk::Fence inFlightFence = frame.inFlight.get(); + vk::Result fenceResult = this->device.get()->waitForFences( + inFlightFence, vk::True, std::numeric_limits::max()); + + if (fenceResult != vk::Result::eSuccess) { + log::error("failed to wait for fence: " + vk::to_string(fenceResult)); + } + + vk::Semaphore imageAvailable = frame.imageAvailable.get(); + vk::Result result = this->swapchain->acquireNextImage( + imageAvailable, this->currentImageIndex); + if (result == vk::Result::eErrorOutOfDateKHR) { + recreateSwapchain(); + return nullptr; + } + + if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) { + log::fatal("failed to acquire swapchain image"); + throw std::runtime_error("failed to acquire swapchain image"); + } + + this->isFrameStarted = true; + + vk::Fence& imageInFlight = this->imagesInFlight.at(this->currentImageIndex); + if (imageInFlight) { + vk::Result fenceResult = this->device.get()->waitForFences( + imageInFlight, vk::True, std::numeric_limits::max()); + + if (fenceResult != vk::Result::eSuccess) { + log::error("failed to wait for fence: " + vk::to_string(fenceResult)); + } + } + + imageInFlight = inFlightFence; + + this->device.get()->resetFences(inFlightFence); + + frame.commandBuffer.reset(); + + try { + frame.commandBuffer.begin(vk::CommandBufferBeginInfo{}); + } catch (const vk::SystemError& err) { + log::fatal("failed to begin recording command buffer"); + throw std::runtime_error("failed to begin recording command buffer"); + } + + return frame.commandBuffer; +} + +void RenderContext::endFrame() { + assert(this->isFrameStarted && + "Can't call endFrame while no frame is in progress"); + + FrameData& frame = this->frames[this->currentFrameIndex]; + frame.commandBuffer.end(); + + vk::Semaphore waitSemaphore = frame.imageAvailable.get(); + vk::Semaphore signalSemaphore = + this->renderFinishedSemaphores.at(this->currentImageIndex).get(); + vk::PipelineStageFlags waitStage = + vk::PipelineStageFlagBits::eColorAttachmentOutput; + vk::CommandBuffer commandBuffer = frame.commandBuffer; + + vk::SubmitInfo submitInfo{}; + submitInfo.waitSemaphoreCount = 1; + submitInfo.pWaitSemaphores = &waitSemaphore; + submitInfo.pWaitDstStageMask = &waitStage; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffer; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &signalSemaphore; + + this->device.getGraphicsQueue().submit(submitInfo, frame.inFlight.get()); + + vk::Result result = this->swapchain->present( + this->device.getPresentQueue(), this->currentImageIndex, signalSemaphore); + if (result == vk::Result::eErrorOutOfDateKHR || + result == vk::Result::eSuboptimalKHR || this->window.wasResized()) { + this->window.resetResizedFlag(); + recreateSwapchain(); + } else if (result != vk::Result::eSuccess) { + log::fatal("failed to present swapchain image"); + throw std::runtime_error("failed to present swapchain image"); + } + + this->isFrameStarted = false; + this->currentFrameIndex = + (this->currentFrameIndex + 1) % MAX_FRAMES_IN_FLIGHT; +} + +void RenderContext::recreateSwapchain() { + vk::Extent2D extent = this->window.getExtent(); + while (extent.width == 0 || extent.height == 0) { + extent = this->window.getExtent(); + glfwWaitEvents(); + } + + this->device.waitIdle(); + + if (this->swapchain == nullptr) { + this->swapchain = std::make_unique(this->device, extent); + } else { + std::shared_ptr oldSwapchain = std::move(this->swapchain); + this->swapchain = + std::make_unique(this->device, extent, oldSwapchain); + + if (!oldSwapchain->compareSwapchainFormats(*this->swapchain)) { + log::fatal("swapchain image format changed on recreation"); + throw std::runtime_error("swapchain image format changed on recreation"); + } + } + + this->renderFinishedSemaphores.clear(); + for (size_t i = 0; i < this->swapchain->imageCount(); i++) { + this->renderFinishedSemaphores.push_back( + this->device.get()->createSemaphoreUnique({})); + } + + // Old fences referred to images owned by the now-replaced swapchain; + // there's nothing valid left to wait on. + this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{}); + + log::trace("recreated swapchain"); +} + +void RenderContext::createFrames() { + vk::CommandBufferAllocateInfo allocInfo{this->device.getCommandPool(), + vk::CommandBufferLevel::ePrimary, + MAX_FRAMES_IN_FLIGHT}; + std::vector commandBuffers = + this->device.get()->allocateCommandBuffers(allocInfo); + + for (u32 i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + this->frames[i].commandBuffer = commandBuffers[i]; + this->frames[i].imageAvailable = + this->device.get()->createSemaphoreUnique({}); + this->frames[i].inFlight = this->device.get()->createFenceUnique( + {vk::FenceCreateFlagBits::eSignaled}); + } +} + +} // namespace sophia diff --git a/engine/src/render_context.hpp b/engine/src/render_context.hpp new file mode 100644 index 0000000..a7be9cb --- /dev/null +++ b/engine/src/render_context.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "device.hpp" +#include "swapchain.hpp" +#include "window.hpp" + +namespace sophia { + +struct FrameData { + vk::CommandBuffer commandBuffer; + vk::UniqueSemaphore imageAvailable; + vk::UniqueFence inFlight; +}; + +class RenderContext { + public: + static constexpr u32 MAX_FRAMES_IN_FLIGHT = 2; + + RenderContext(const RenderContext&) = delete; + RenderContext& operator=(const RenderContext&) = delete; + + RenderContext(Window& window, Device& device); + ~RenderContext(); + + vk::CommandBuffer beginFrame(); + void endFrame(); + + void transitionCurrentImage(vk::CommandBuffer commandBuffer, + vk::ImageLayout newLayout) const { + this->swapchain->getImage(static_cast(this->currentImageIndex)) + .transitionLayout(commandBuffer, newLayout); + } + vk::ImageView getCurrentImageView() const { + return this->swapchain->getImageView( + static_cast(this->currentImageIndex)); + } + + vk::Extent2D getExtent() const { return this->swapchain->getExtent(); } + vk::Format getImageFormat() const { + return this->swapchain->getImageFormat(); + } + + private: + void recreateSwapchain(); + void createFrames(); + + Window& window; + Device& device; + + std::unique_ptr swapchain; + + std::array frames; + + std::vector renderFinishedSemaphores; + std::vector imagesInFlight; + + u32 currentFrameIndex = 0; + u32 currentImageIndex = 0; + bool isFrameStarted = false; +}; + +} // namespace sophia