diff --git a/src/config/config.hpp b/src/config/config.hpp new file mode 100644 index 0000000..7565bfc --- /dev/null +++ b/src/config/config.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "../core/types.hpp" + +namespace config { + + // Window settings + constexpr i32 WIDTH = 1920; + constexpr i32 HEIGHT = 1080; + + // Vulkan settings + constexpr i32 MAX_FRAMES_IN_FLIGHT = 2; + + // Shader paths + constexpr const char* VERTEX_SHADER_PATH = "shaders/vertex.glsl"; + constexpr const char* FRAGMENT_SHADER_PATH = "shaders/fragment.glsl"; + +// Validation layers for debug builds +#ifndef NDEBUG + constexpr std::array validationLayers = { "VK_LAYER_KHRONOS_validation" }; +#endif + +// Required device extensions (platform-specific) +#ifdef __APPLE__ + constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName, + vk::KHRPortabilitySubsetExtensionName }; +#else + constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName }; +#endif + +} // namespace config diff --git a/src/core/device_utils.hpp b/src/core/device_utils.hpp new file mode 100644 index 0000000..e147e5b --- /dev/null +++ b/src/core/device_utils.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +#include "../config/config.hpp" +#include "queue_structures.hpp" +#include "types.hpp" + +namespace core { + + /** + * @brief Gets the maximum usable sample count for MSAA. + * + * @param device Physical device to check + * @return vk::SampleCountFlagBits Maximum sample count supported + */ + static fn getMaxUsableSampleCount(const vk::PhysicalDevice& device) -> vk::SampleCountFlagBits { + vk::PhysicalDeviceProperties physicalDeviceProperties = device.getProperties(); + + vk::SampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & + physicalDeviceProperties.limits.framebufferDepthSampleCounts; + + if (counts & vk::SampleCountFlagBits::e64) + return vk::SampleCountFlagBits::e64; + if (counts & vk::SampleCountFlagBits::e32) + return vk::SampleCountFlagBits::e32; + if (counts & vk::SampleCountFlagBits::e16) + return vk::SampleCountFlagBits::e16; + if (counts & vk::SampleCountFlagBits::e8) + return vk::SampleCountFlagBits::e8; + if (counts & vk::SampleCountFlagBits::e4) + return vk::SampleCountFlagBits::e4; + if (counts & vk::SampleCountFlagBits::e2) + return vk::SampleCountFlagBits::e2; + + return vk::SampleCountFlagBits::e1; + } + + /** + * @brief Checks if a device supports the required extensions. + * + * @param device Physical device to check + * @return bool True if all required extensions are supported + */ + static fn checkDeviceExtensionSupport(const vk::PhysicalDevice& device) -> bool { + // Get the available extensions + std::vector availableExtensions = device.enumerateDeviceExtensionProperties(); + + // Create a set of required extensions + std::unordered_set requiredExtensions( + config::deviceExtensions.begin(), config::deviceExtensions.end() + ); + + // For each available extension, + for (const auto& extension : availableExtensions) + // Remove it from the required extensions set if it's required + requiredExtensions.erase(extension.extensionName); + + // If the set is empty, all required extensions are supported + return requiredExtensions.empty(); + } + + /** + * @brief Checks if a physical device is suitable for the application. + * + * @param device Physical device to check + * @param surface Surface to check presentation support against + * @return bool True if device is suitable, false otherwise + */ + static fn isDeviceSuitable(const vk::PhysicalDevice& device, const vk::SurfaceKHR& surface) -> bool { + // Get the queue families that support the required operations + QueueFamilyIndices qfIndices = QueueFamilyIndices::findQueueFamilies(device, surface); + + // Check if the device supports the required extensions + bool extensionsSupported = checkDeviceExtensionSupport(device); + + bool swapChainAdequate = false; + + if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = + SwapChainSupportDetails::querySwapChainSupport(device, surface); + // Check if the swap chain is adequate (make sure it has + // at least one supported format and presentation mode) + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.present_modes.empty(); + } + + // Check if the device supports the required features + vk::PhysicalDeviceFeatures supportedFeatures = device.getFeatures(); + + return qfIndices.isComplete() && extensionsSupported && swapChainAdequate && + supportedFeatures.samplerAnisotropy; + } + +} // namespace core diff --git a/src/core/queue_structures.hpp b/src/core/queue_structures.hpp new file mode 100644 index 0000000..9f3cd6b --- /dev/null +++ b/src/core/queue_structures.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include + +#include "types.hpp" + +/** + * @brief Struct to store queue family indices. + * + * This struct contains the indices of the graphics and presentation queue families. + */ +struct QueueFamilyIndices { + std::optional graphics_family; ///< Index of graphics queue family + std::optional present_family; ///< Index of presentation queue family + + /** + * @brief Check if all required queue families are found. + * + * @return True if both graphics and presentation families are found, false otherwise. + */ + fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); } + + /** + * @brief Finds queue families that support graphics and presentation. + * + * @param device Physical device to check + * @param surface Surface to check presentation support against + * @return QueueFamilyIndices Struct containing queue family indices + */ + static fn findQueueFamilies(const vk::PhysicalDevice& device, const vk::SurfaceKHR& surface) + -> QueueFamilyIndices { + QueueFamilyIndices indices; + + std::vector queueFamilies = device.getQueueFamilyProperties(); + + for (u32 i = 0; i < queueFamilies.size(); i++) { + const auto& queueFamily = queueFamilies[i]; + + // Check for graphics support + if (queueFamily.queueFlags & vk::QueueFlagBits::eGraphics) + indices.graphics_family = i; + + // Check for presentation support + if (device.getSurfaceSupportKHR(i, surface)) + indices.present_family = i; + + if (indices.isComplete()) + break; + } + + return indices; + } +}; + +/** + * @brief Struct to hold swap chain support details. + * + * This struct contains information about the surface capabilities, + * supported formats, and presentation modes. + */ +struct SwapChainSupportDetails { + vk::SurfaceCapabilitiesKHR capabilities; ///< Surface capabilities + std::vector formats; ///< Supported surface formats + std::vector present_modes; ///< Supported presentation modes + + /** + * @brief Queries swap chain support details from a physical device. + * + * @param device Physical device to query + * @param surface Surface to check against + * @return SwapChainSupportDetails Struct containing surface capabilities and supported formats + */ + static fn querySwapChainSupport(const vk::PhysicalDevice& device, const vk::SurfaceKHR& surface) + -> SwapChainSupportDetails { + SwapChainSupportDetails details; + + details.capabilities = device.getSurfaceCapabilitiesKHR(surface); + details.formats = device.getSurfaceFormatsKHR(surface); + details.present_modes = device.getSurfacePresentModesKHR(surface); + + return details; + } + + /** + * @brief Chooses the best surface format for the swap chain. + * + * @param availableFormats List of available surface formats + * @return vk::SurfaceFormatKHR The chosen surface format + */ + static fn chooseSwapSurfaceFormat(const std::vector& availableFormats + ) -> vk::SurfaceFormatKHR { + // Prefer SRGB with nonlinear color space + for (const auto& availableFormat : availableFormats) + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && + availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) + return availableFormat; + + // If preferred format not found, use first available + return availableFormats[0]; + } + + /** + * @brief Chooses the best presentation mode for the swap chain. + * + * @param availablePresentModes List of available presentation modes + * @return vk::PresentModeKHR The chosen presentation mode + */ + static fn chooseSwapPresentMode(const std::vector& availablePresentModes + ) -> vk::PresentModeKHR { + // Prefer mailbox mode (triple buffering) + for (const auto& availablePresentMode : availablePresentModes) + if (availablePresentMode == vk::PresentModeKHR::eMailbox) + return availablePresentMode; + + // Fallback to FIFO (vsync) + return vk::PresentModeKHR::eFifo; + } + + /** + * @brief Chooses the swap extent (resolution) for the swap chain. + * + * @param capabilities Surface capabilities + * @param width Desired width + * @param height Desired height + * @return vk::Extent2D The chosen swap extent + */ + static fn chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities, u32 width, u32 height) + -> vk::Extent2D { + if (capabilities.currentExtent.width != std::numeric_limits::max()) + return capabilities.currentExtent; + + vk::Extent2D actualExtent = { width, height }; + + actualExtent.width = + std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + + actualExtent.height = + std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + + return actualExtent; + } +}; diff --git a/src/util/types.hpp b/src/core/types.hpp similarity index 100% rename from src/util/types.hpp rename to src/core/types.hpp diff --git a/src/graphics/camera.hpp b/src/graphics/camera.hpp new file mode 100644 index 0000000..d30e944 --- /dev/null +++ b/src/graphics/camera.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include + +#include "../core/types.hpp" +#include "vkfw.hpp" + +// Camera speed constant from main.cpp +constexpr f64 CAMERA_SPEED = 1.0; + +/** + * @brief Represents a 3D camera in the scene. + */ +struct Camera { + glm::dvec3 position; ///< Camera's position in 3D space + glm::dvec3 front; ///< Direction the camera is facing + glm::dvec3 up; ///< Camera's up vector + glm::dvec3 right; ///< Camera's right vector + f64 yaw; ///< Yaw angle (rotation around vertical axis) + f64 pitch; ///< Pitch angle (rotation around horizontal axis) + + /** + * @brief Constructs a Camera with default settings. + */ + Camera() + : position(2.0, 2.0, 2.0), + front(glm::normalize(glm::dvec3(-2.0, -2.0, -2.0))), + up(0.0, 0.0, 1.0), + right(glm::normalize(glm::cross(front, up))), + yaw(-135.0), + pitch(-35.26) { + updateCameraVectors(); + } + + /** + * @brief Gets the camera's current position. + * @return The camera's position as a 3D vector. + */ + [[nodiscard]] fn getPosition() const -> glm::dvec3 { return position; } + + /** + * @brief Calculates and returns the view matrix for the camera. + * @return The view matrix as a 4x4 matrix. + */ + [[nodiscard]] fn getViewMatrix() const -> glm::mat4 { return glm::lookAt(position, position + front, up); } + + /** + * @brief Moves the camera forward. + * @param deltaTime Time elapsed since last frame. + */ + fn moveForward(f64 deltaTime) -> void { + glm::dvec3 horizontalFront = glm::normalize(glm::dvec3(front.x, front.y, 0.0)); + position += horizontalFront * CAMERA_SPEED * deltaTime; + } + + /** + * @brief Moves the camera backward. + * @param deltaTime Time elapsed since last frame. + */ + fn moveBackward(f64 deltaTime) -> void { + glm::dvec3 horizontalFront = glm::normalize(glm::dvec3(front.x, front.y, 0.0)); + position -= horizontalFront * CAMERA_SPEED * deltaTime; + } + + /** + * @brief Moves the camera to the left. + * @param deltaTime Time elapsed since last frame. + */ + fn moveLeft(f64 deltaTime) -> void { + glm::dvec3 horizontalRight = glm::normalize(glm::dvec3(right.x, right.y, 0.0)); + position -= horizontalRight * CAMERA_SPEED * deltaTime; + } + + /** + * @brief Moves the camera to the right. + * @param deltaTime Time elapsed since last frame. + */ + fn moveRight(f64 deltaTime) -> void { + glm::dvec3 horizontalRight = glm::normalize(glm::dvec3(right.x, right.y, 0.0)); + position += horizontalRight * CAMERA_SPEED * deltaTime; + } + + /** + * @brief Moves the camera upward. + * @param deltaTime Time elapsed since last frame. + */ + fn moveUp(f64 deltaTime) -> void { position += glm::dvec3(0.0, 0.0, 1.0) * CAMERA_SPEED * deltaTime; } + + /** + * @brief Moves the camera downward. + * @param deltaTime Time elapsed since last frame. + */ + fn moveDown(f64 deltaTime) -> void { position -= glm::dvec3(0.0, 0.0, 1.0) * CAMERA_SPEED * deltaTime; } + + /** + * @brief Rotates the camera based on mouse movement. + * @param xoffset Horizontal mouse movement. + * @param yoffset Vertical mouse movement. + */ + fn rotate(f64 xoffset, f64 yoffset) -> void { + const f64 sensitivity = 0.1; + yaw += xoffset * sensitivity; + pitch += yoffset * sensitivity; + + pitch = glm::clamp(pitch, -89.0, 89.0); + + updateCameraVectors(); + } + + /** + * @brief Processes input for camera movement and rotation. + * @param window The GLFW window. + * @param camera The camera to be controlled. + * @param deltaTime Time elapsed since last frame. + * @param cameraSpeed Speed multiplier for camera movement. + */ + static fn processInput(vkfw::Window& window, Camera& camera, const f32& deltaTime, const f32& cameraSpeed) + -> void { + if (window.getKey(vkfw::Key::eW) == vkfw::eTrue) + camera.moveForward(static_cast(deltaTime * cameraSpeed)); + if (window.getKey(vkfw::Key::eA) == vkfw::eTrue) + camera.moveLeft(static_cast(deltaTime * cameraSpeed)); + if (window.getKey(vkfw::Key::eS) == vkfw::eTrue) + camera.moveBackward(static_cast(deltaTime * cameraSpeed)); + if (window.getKey(vkfw::Key::eD) == vkfw::eTrue) + camera.moveRight(static_cast(deltaTime * cameraSpeed)); + if (window.getKey(vkfw::Key::eSpace) == vkfw::eTrue) + camera.moveUp(static_cast(deltaTime * cameraSpeed)); + if (window.getKey(vkfw::Key::eLeftShift) == vkfw::eTrue) + camera.moveDown(static_cast(deltaTime * cameraSpeed)); + } + + private: + /** + * @brief Updates the camera's orientation vectors based on yaw and pitch. + */ + fn updateCameraVectors() -> void { + front = glm::normalize(glm::dvec3( + cos(glm::radians(yaw)) * cos(glm::radians(pitch)), + sin(glm::radians(yaw)) * cos(glm::radians(pitch)), + sin(glm::radians(pitch)) + )); + right = glm::normalize(glm::cross(front, glm::dvec3(0.0, 0.0, 1.0))); + up = glm::normalize(glm::cross(right, front)); + } +}; diff --git a/src/graphics/image_utils.hpp b/src/graphics/image_utils.hpp new file mode 100644 index 0000000..8721462 --- /dev/null +++ b/src/graphics/image_utils.hpp @@ -0,0 +1,278 @@ +#pragma once + +#define VULKAN_HPP_NO_CONSTRUCTORS +#include + +#include "../core/types.hpp" + +namespace graphics { + + /** + * @brief Creates a Vulkan image view. + * + * This function creates and returns a unique Vulkan image view using the provided parameters. + * + * @param device The logical device to create the view on + * @param image The Vulkan image for which to create the view + * @param format The format of the image + * @param aspectFlags The aspect flags for the image view + * @param mipLevels The number of mip levels for the image view + * @return vk::UniqueImageView A unique handle to the created Vulkan image view + * + * @details + * The function creates an image view with the following properties: + * - 2D view type + * - Subresource range starting from base mip level 0 + * - Single array layer starting from base array layer 0 + */ + static fn createImageView( + const vk::Device& device, + const vk::Image& image, + const vk::Format& format, + const vk::ImageAspectFlags& aspectFlags, + const u32& mipLevels + ) -> vk::UniqueImageView { + return device.createImageViewUnique({ + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { + .aspectMask = aspectFlags, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + } + + /** + * @brief Finds a suitable memory type for allocation. + * + * @param physicalDevice The physical device to check + * @param typeFilter Filter for memory types + * @param properties Required memory properties + * @return u32 Index of the suitable memory type + */ + static fn findMemoryType( + const vk::PhysicalDevice& physicalDevice, + const u32& typeFilter, + const vk::MemoryPropertyFlags& properties + ) -> u32 { + vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + for (u32 i = 0; i < memProperties.memoryTypeCount; i++) + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) + return i; + + throw std::runtime_error("Failed to find suitable memory type!"); + } + + /** + * @brief Creates a Vulkan image and allocates memory for it. + * + * @param device The logical device to create the image on + * @param width Width of the image + * @param height Height of the image + * @param mipLevels Number of mip levels + * @param numSamples Number of samples for multisampling + * @param format Format of the image + * @param tiling Tiling mode of the image + * @param usage Usage flags for the image + * @param properties Memory property flags + * @return std::pair A pair containing the image and its memory + */ + static fn createImage( + const vk::Device& device, + const vk::PhysicalDevice& physicalDevice, + const u32& width, + const u32& height, + const u32& mipLevels, + const vk::SampleCountFlagBits& numSamples, + const vk::Format& format, + const vk::ImageTiling& tiling, + const vk::ImageUsageFlags& usage, + const vk::MemoryPropertyFlags& properties + ) -> std::pair { + // Define the image creation info + vk::ImageCreateInfo imageInfo { + .imageType = vk::ImageType::e2D, + .format = format, + .extent = { .width = width, .height = height, .depth = 1 }, + .mipLevels = mipLevels, + .arrayLayers = 1, + .samples = numSamples, + .tiling = tiling, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined, + }; + + // Create the image + vk::UniqueImage image = device.createImageUnique(imageInfo); + + // Get the memory requirements for the image + vk::MemoryRequirements memRequirements = device.getImageMemoryRequirements(image.get()); + + // Memory allocation info + vk::MemoryAllocateInfo allocInfo { + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties), + }; + + // Allocate memory + vk::UniqueDeviceMemory imageMemory = device.allocateMemoryUnique(allocInfo); + + // Bind the allocated memory to the image + device.bindImageMemory(image.get(), imageMemory.get(), 0); + + return { std::move(image), std::move(imageMemory) }; + } + + /** + * @brief Transitions an image between layouts. + * + * @param device The logical device + * @param commandPool The command pool to allocate command buffers from + * @param queue The queue to submit commands to + * @param image The image to transition + * @param oldLayout The old layout + * @param newLayout The new layout + * @param mipLevels Number of mip levels + */ + static fn transitionImageLayout( + const vk::Device& device, + const vk::CommandPool& commandPool, + const vk::Queue& queue, + const vk::Image& image, + const vk::ImageLayout& oldLayout, + const vk::ImageLayout& newLayout, + const u32& mipLevels + ) -> void { + // Create a command buffer + vk::CommandBufferAllocateInfo allocInfo { + .commandPool = commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1, + }; + + vk::UniqueCommandBuffer commandBuffer = std::move(device.allocateCommandBuffersUnique(allocInfo)[0]); + + // Begin recording + commandBuffer->begin({ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }); + + // Define the image memory barrier + vk::ImageMemoryBarrier barrier { + .oldLayout = oldLayout, + .newLayout = newLayout, + .srcQueueFamilyIndex = vk::QueueFamilyIgnored, + .dstQueueFamilyIndex = vk::QueueFamilyIgnored, + .image = image, + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + + // Define the source and destination stages + vk::PipelineStageFlags sourceStage; + vk::PipelineStageFlags destinationStage; + + // Define the access masks + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier.srcAccessMask = {}; + barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; + + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; + destinationStage = vk::PipelineStageFlagBits::eTransfer; + } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && + newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + sourceStage = vk::PipelineStageFlagBits::eTransfer; + destinationStage = vk::PipelineStageFlagBits::eFragmentShader; + } else { + // Ensure that the layout transition is supported + throw std::invalid_argument("Unsupported layout transition!"); + } + + // Record the pipeline barrier + commandBuffer->pipelineBarrier(sourceStage, destinationStage, {}, {}, {}, barrier); + + // End recording + commandBuffer->end(); + + // Submit the command buffer + vk::SubmitInfo submitInfo { + .commandBufferCount = 1, + .pCommandBuffers = &commandBuffer.get(), + }; + + queue.submit(submitInfo); + queue.waitIdle(); + } + + /** + * @brief Copies data from a buffer to an image. + * + * @param device The logical device + * @param commandPool The command pool to allocate command buffers from + * @param queue The queue to submit commands to + * @param buffer Source buffer + * @param image Destination image + * @param width Image width + * @param height Image height + */ + static fn copyBufferToImage( + const vk::Device& device, + const vk::CommandPool& commandPool, + const vk::Queue& queue, + const vk::Buffer& buffer, + const vk::Image& image, + const u32& width, + const u32& height + ) -> void { + // Create a command buffer + vk::CommandBufferAllocateInfo allocInfo { + .commandPool = commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1, + }; + + vk::UniqueCommandBuffer commandBuffer = std::move(device.allocateCommandBuffersUnique(allocInfo)[0]); + + // Begin recording + commandBuffer->begin({ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }); + + vk::BufferImageCopy region { + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 }, + .imageOffset = { .x = 0, .y = 0, .z = 0 }, + .imageExtent = { .width = width, .height = height, .depth = 1 }, + }; + + commandBuffer->copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, 1, ®ion); + + // End recording + commandBuffer->end(); + + // Submit the command buffer + vk::SubmitInfo submitInfo { + .commandBufferCount = 1, + .pCommandBuffers = &commandBuffer.get(), + }; + + queue.submit(submitInfo); + queue.waitIdle(); + } + +} // namespace graphics diff --git a/src/graphics/shaders.hpp b/src/graphics/shaders.hpp new file mode 100644 index 0000000..0f47f32 --- /dev/null +++ b/src/graphics/shaders.hpp @@ -0,0 +1,230 @@ +/** + * @file shaders.hpp + * @brief SPIR-V shader compilation and caching system. + * + * This file provides functionality for compiling GLSL shaders to SPIR-V and + * managing a shader cache system. It supports automatic recompilation when + * source files are modified and efficient caching of compiled shaders. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#define VULKAN_HPP_NO_CONSTRUCTORS +#include + +#include "../core/types.hpp" + +namespace graphics { + + /** + * @brief Handles shader compilation and caching operations. + * + * This class provides static methods for compiling GLSL shaders to SPIR-V + * and managing a cache system. It automatically detects when shaders need + * to be recompiled based on file timestamps and provides efficient caching + * of compiled shader binaries. + */ + class ShaderCompiler { + public: + ShaderCompiler() = default; + + /** + * @brief Compiles or retrieves a cached SPIR-V shader. + * + * @param shaderPath Path to the GLSL shader source file + * @param kind Type of shader (vertex, fragment, compute, etc.) + * @return std::vector Compiled SPIR-V binary code + * @throws std::runtime_error If shader compilation fails or file is not found + * + * This function performs the following steps: + * 1. Checks if a cached version exists and is up-to-date + * 2. Loads from cache if available and valid + * 3. Otherwise, compiles the shader from source + * 4. Caches the newly compiled shader for future use + * 5. Returns the SPIR-V binary code + */ + static fn getCompiledShader(const std::filesystem::path& shaderPath, const shaderc_shader_kind& kind) + -> std::vector { + using namespace std; + + // Convert to absolute path if relative + filesystem::path absPath = filesystem::absolute(shaderPath); + + if (!filesystem::exists(absPath)) + throw runtime_error("Shader file not found: " + absPath.string()); + + const string shaderName = absPath.stem().string(); + const filesystem::path cacheFile = getCacheFilePath(shaderName); + + // Check if we need to recompile by comparing timestamps + if (filesystem::exists(cacheFile)) { + const auto sourceTime = filesystem::last_write_time(absPath); + const auto cacheTime = filesystem::last_write_time(cacheFile); + + if (cacheTime >= sourceTime) { + // Cache is up to date, load it + vector spirvCode = loadCachedShader(cacheFile); + if (!spirvCode.empty()) { + fmt::println("Loaded shader from cache: {}", cacheFile.string()); + return spirvCode; + } + } + } + + // Need to compile the shader + fmt::println("Compiling shader: {}", absPath.string()); + + // Read shader source + ifstream file(absPath); + if (!file) + throw runtime_error("Failed to open shader file: " + absPath.string()); + + string shaderSource((istreambuf_iterator(file)), istreambuf_iterator()); + file.close(); + + // Compile the shader + vector spirvCode = compileShader(shaderSource.c_str(), kind); + + if (spirvCode.empty()) + throw runtime_error("Shader compilation failed for: " + absPath.string()); + + // Cache the compiled SPIR-V binary + saveCompiledShader(spirvCode, cacheFile.string()); + return spirvCode; + } + + /** + * @brief Creates a shader module from SPIR-V code. + * + * @param device Logical device to create the shader module on + * @param code SPIR-V binary code + * @return vk::UniqueShaderModule Unique handle to the created shader module + */ + static fn createShaderModule(const vk::Device& device, const std::vector& code) + -> vk::UniqueShaderModule { + vk::ShaderModuleCreateInfo createInfo { .codeSize = code.size() * sizeof(u32), .pCode = code.data() }; + + return device.createShaderModuleUnique(createInfo); + } + + private: + /** + * @brief Determines the platform-specific shader cache directory. + * + * @param shaderName Base name of the shader file + * @return std::filesystem::path Full path to the cache file + * + * Cache locations: + * - Windows: %LOCALAPPDATA%/VulkanApp/Shaders/ + * - macOS: ~/Library/Application Support/VulkanApp/Shaders/ + * - Linux: ~/.local/share/VulkanApp/Shaders/ + * + * The directory is created if it doesn't exist. + */ + static fn getCacheFilePath(const string& shaderName) -> std::filesystem::path { + using namespace std::filesystem; + +#ifdef _WIN32 + path cacheDir = path(getenv("LOCALAPPDATA")) / "VulkanApp" / "Shaders"; +#elif defined(__APPLE__) + path cacheDir = path(getenv("HOME")) / "Library" / "Application Support" / "VulkanApp" / "Shaders"; +#else // Assume Linux or other UNIX-like systems + path cacheDir = path(getenv("HOME")) / ".local" / "share" / "VulkanApp" / "Shaders"; +#endif + + if (!exists(cacheDir)) + create_directories(cacheDir); + + return cacheDir / (shaderName + ".spv"); + } + + /** + * @brief Loads a cached SPIR-V shader from disk. + * + * @param cachePath Path to the cached shader file + * @return std::vector SPIR-V binary code, empty if loading fails + * + * Reads the binary SPIR-V data from the cache file. Returns an empty + * vector if the file cannot be opened or read properly. + */ + static fn loadCachedShader(const std::filesystem::path& cachePath) -> std::vector { + std::ifstream file(cachePath, std::ios::binary); + if (!file.is_open()) + return {}; + + // Read file size + file.seekg(0, std::ios::end); + const std::streamoff fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + // Allocate buffer and read data + std::vector buffer(static_cast(fileSize) / sizeof(u32)); + file.read(std::bit_cast(buffer.data()), fileSize); + + return buffer; + } + + /** + * @brief Compiles GLSL source code to SPIR-V. + * + * @param source GLSL shader source code + * @param kind Type of shader being compiled + * @return std::vector Compiled SPIR-V binary code + * + * Uses the shaderc library to compile GLSL to SPIR-V. The compilation + * is performed with optimization level set to performance and generates + * debug information in debug builds. + */ + static fn compileShader(const char* source, shaderc_shader_kind kind) -> std::vector { + shaderc::Compiler compiler; + shaderc::CompileOptions options; + + // Set compilation options +#ifdef NDEBUG + options.SetOptimizationLevel(shaderc_optimization_level_performance); +#else + options.SetOptimizationLevel(shaderc_optimization_level_zero); + options.SetGenerateDebugInfo(); +#endif + + // Compile the shader + shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, kind, "shader", options); + + if (module.GetCompilationStatus() != shaderc_compilation_status_success) + return {}; + + return { module.cbegin(), module.cend() }; + } + + /** + * @brief Saves compiled SPIR-V code to the cache. + * + * @param spirv Compiled SPIR-V binary code + * @param cachePath Path where the cache file should be saved + * @return bool True if save was successful, false otherwise + * + * Writes the SPIR-V binary to disk for future use. Creates any + * necessary parent directories if they don't exist. + */ + static fn saveCompiledShader(const std::vector& spirv, const std::string& cachePath) -> bool { + std::ofstream file(cachePath, std::ios::binary); + if (!file.is_open()) + return false; + + file.write( + std::bit_cast(spirv.data()), static_cast(spirv.size() * sizeof(u32)) + ); + + return file.good(); + } + }; // class ShaderCompiler + +} // namespace graphics diff --git a/src/graphics/uniform_buffer.hpp b/src/graphics/uniform_buffer.hpp new file mode 100644 index 0000000..55d92df --- /dev/null +++ b/src/graphics/uniform_buffer.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +/** + * @brief Struct representing a uniform buffer object. + * + * This struct holds the model, view, and projection matrices for use in shaders. + */ +struct UniformBufferObject { + alignas(16) glm::mat4 model; ///< Model transformation matrix + alignas(16) glm::mat4 view; ///< View transformation matrix + alignas(16) glm::mat4 proj; ///< Projection matrix +}; diff --git a/src/util/unique_image.hpp b/src/graphics/unique_image.hpp similarity index 91% rename from src/util/unique_image.hpp rename to src/graphics/unique_image.hpp index 01ea809..101da07 100644 --- a/src/util/unique_image.hpp +++ b/src/graphics/unique_image.hpp @@ -1,23 +1,24 @@ /** * @file unique_image.hpp * @brief Provides RAII wrapper for image loading and management. - * + * * This file implements a RAII-compliant image handling class that uses stb_image * for loading various image formats. It ensures proper resource management and * provides a safe interface for image data access. */ #include +#include #define STB_IMAGE_IMPLEMENTATION #include -#include "types.hpp" +#include "../core/types.hpp" namespace stb { /** * @brief RAII wrapper for image data loaded via stb_image. - * + * * This class provides safe resource management for loaded images, ensuring proper * cleanup of image data. It supports move semantics but prevents copying to maintain * single ownership of image resources. @@ -26,41 +27,41 @@ namespace stb { public: /** * @brief Constructs a UniqueImage by loading from file. - * + * * @param path Filesystem path to the image file. * @throws std::runtime_error If image loading fails. - * + * * Automatically loads the image data from the specified file using stb_image. * The image data is stored in RGBA format with 8 bits per channel. */ UniqueImage(const std::filesystem::path& path) { load(path.string().c_str()); } // Prevent copying to maintain single ownership - UniqueImage(const UniqueImage&) = delete; - fn operator=(const UniqueImage&) -> UniqueImage& = delete; + UniqueImage(const UniqueImage&) = delete; + fn operator=(const UniqueImage&)->UniqueImage& = delete; /** * @brief Move constructor for transferring image ownership. - * + * * @param other Source UniqueImage to move from. - * + * * Transfers ownership of image data from another UniqueImage instance, * leaving the source in a valid but empty state. */ UniqueImage(UniqueImage&& other) noexcept - : mData(other.mData), - mWidth(static_cast(other.mWidth)), - mHeight(static_cast(other.mHeight)), + : mData(other.mData), + mWidth(static_cast(other.mWidth)), + mHeight(static_cast(other.mHeight)), mChannels(static_cast(other.mChannels)) { other.mData = nullptr; } /** * @brief Move assignment operator for transferring image ownership. - * + * * @param other Source UniqueImage to move from. * @return Reference to this object. - * + * * Safely transfers ownership of image data, ensuring proper cleanup of * existing resources before the transfer. */ @@ -80,7 +81,7 @@ namespace stb { /** * @brief Destructor that ensures proper cleanup of image resources. - * + * * Automatically frees the image data using stb_image_free when the * object goes out of scope. */ @@ -92,7 +93,7 @@ namespace stb { /** * @brief Get raw pointer to image data. * @return Pointer to the raw image data in memory. - * + * * The data is stored in row-major order with either RGB or RGBA format, * depending on the source image. */ @@ -119,10 +120,10 @@ namespace stb { private: /** * @brief Internal helper function to load image data. - * + * * @param path Path to the image file. * @throws std::runtime_error If image loading fails. - * + * * Uses stb_image to load the image data, automatically detecting the * format and number of channels from the file. */ diff --git a/src/util/vertex.hpp b/src/graphics/vertex.hpp similarity index 87% rename from src/util/vertex.hpp rename to src/graphics/vertex.hpp index 0e2ba69..30980a6 100644 --- a/src/util/vertex.hpp +++ b/src/graphics/vertex.hpp @@ -1,7 +1,7 @@ /** * @file vertex.hpp * @brief Defines the vertex structure and its associated utilities for 3D rendering. - * + * * This file contains the Vertex structure used for 3D model representation in the Vulkan * graphics pipeline. It includes position, color, and texture coordinate data, along with * Vulkan-specific descriptors for vertex input handling. @@ -15,11 +15,11 @@ #define VULKAN_HPP_NO_CONSTRUCTORS #include -#include "types.hpp" +#include "../core/types.hpp" /** * @brief Represents a vertex in 3D space with color and texture information. - * + * * This structure defines a vertex with all its attributes required for rendering, * including position in 3D space, RGB color, and texture coordinates. It also * provides methods for Vulkan vertex input configuration. @@ -31,9 +31,9 @@ struct Vertex { /** * @brief Provides the vertex binding description for Vulkan. - * + * * @return vk::VertexInputBindingDescription Describes how to bind vertex data to GPU memory. - * + * * The binding description specifies: * - Binding index (0) * - Stride (size of one vertex) @@ -45,9 +45,10 @@ struct Vertex { /** * @brief Provides attribute descriptions for vertex data interpretation. - * - * @return std::array Array of descriptions for position, color, and texture coordinates. - * + * + * @return std::array Array of descriptions for position, color, and + * texture coordinates. + * * The attribute descriptions specify: * - Location indices (0 for position, 1 for color, 2 for texture coordinates) * - Binding point (0) @@ -56,19 +57,19 @@ struct Vertex { */ static fn getAttributeDescriptions() -> std::array { return { - vk::VertexInputAttributeDescription { 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) }, - vk::VertexInputAttributeDescription { 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) }, - vk::VertexInputAttributeDescription { 2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, tex_coord) } + vk::VertexInputAttributeDescription { 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) }, + vk::VertexInputAttributeDescription { 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) }, + vk::VertexInputAttributeDescription { 2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, tex_coord) } }; } /** * @brief Compares two vertices for equality. - * + * * @param other The vertex to compare with. * @return bool True if vertices are identical in position, color, and texture coordinates. */ - fn operator==(const Vertex& other) const -> bool { + fn operator==(const Vertex& other) const->bool { return pos == other.pos && color == other.color && tex_coord == other.tex_coord; } }; @@ -76,7 +77,7 @@ struct Vertex { namespace std { /** * @brief Hash function specialization for Vertex type. - * + * * This specialization allows Vertex objects to be used as keys in unordered containers. * The hash combines position, color, and texture coordinate data using bit operations * to create a unique hash value. @@ -85,11 +86,11 @@ namespace std { struct hash { /** * @brief Computes hash value for a vertex. - * + * * @param vertex The vertex to hash. * @return size_t Hash value combining all vertex attributes. */ - fn operator()(Vertex const& vertex) const -> size_t { + fn operator()(Vertex const& vertex) const->size_t { return ((hash()(vertex.pos) ^ (hash()(vertex.color) << 1)) >> 1) ^ (hash()(vertex.tex_coord) << 1); } diff --git a/src/main.cpp b/src/main.cpp index f3cee8f..b6e4221 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,12 +22,6 @@ // Necessary for dynamic dispatch to work VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE -// Include custom utility headers -#include "util/shaders.hpp" // Compiled shader code -#include "util/types.hpp" // Custom type definitions -#include "util/unique_image.hpp" // Custom image handling utilities -#include "util/vertex.hpp" // Custom vertex structure definition - // ImGui headers for GUI #include #include @@ -37,32 +31,21 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE #define VKFW_NO_STRUCT_CONSTRUCTORS // Use aggregate initialization for GLFW structs #include "vkfw.hpp" // Include GLFW C++ bindings -// Constants for window dimensions -constexpr i32 WIDTH = 1920; -constexpr i32 HEIGHT = 1080; +// Include custom utility headers +#include "config/config.hpp" // Configuration constants +#include "core/device_utils.hpp" // Device-related utilities +#include "core/queue_structures.hpp" // Queue-related structures +#include "core/types.hpp" // Custom type definitions +#include "graphics/camera.hpp" // Camera implementation +#include "graphics/image_utils.hpp" // Image-related utilities +#include "graphics/shaders.hpp" // Custom shader code +#include "graphics/uniform_buffer.hpp" // Uniform buffer object +#include "graphics/unique_image.hpp" // Custom image handling utilities +#include "graphics/vertex.hpp" // Custom vertex structure definition -// CAMERA_SPEED of camera movement -constexpr f64 CAMERA_SPEED = 1.0; - -// Maximum number of frames that can be processed concurrently -constexpr i32 MAX_FRAMES_IN_FLIGHT = 2; - -// Shader file paths -constexpr const char* VERTEX_SHADER_PATH = "shaders/vertex.glsl"; -constexpr const char* FRAGMENT_SHADER_PATH = "shaders/fragment.glsl"; - -// Validation layers for debug builds -#ifndef NDEBUG -constexpr std::array validationLayers = { "VK_LAYER_KHRONOS_validation" }; -#endif - -// Required device extensions (platform-specific) -#ifdef __APPLE__ -constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName, - vk::KHRPortabilitySubsetExtensionName }; -#else -constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName }; -#endif +using namespace config; // For easy access to configuration constants +using namespace core; // For core utilities +using namespace graphics; // For graphics utilities /** * @brief The Vulkan application class. @@ -182,154 +165,7 @@ class VulkanApp { f32 mFieldOfView = 90.0F; ///< Current field of view bool mWireframeMode = false; ///< Wireframe rendering mode - /** - * @brief Struct to store queue family indices. - * - * This struct contains the indices of the graphics and presentation queue families. - */ - struct QueueFamilyIndices { - std::optional graphics_family; ///< Index of graphics queue family - std::optional present_family; ///< Index of presentation queue family - - /** - * @brief Check if all required queue families are found. - * - * @return True if both graphics and presentation families are found, false otherwise. - */ - fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); } - }; - - /** - * @brief Struct to hold swap chain support details. - * - * This struct contains information about the surface capabilities, - * supported formats, and presentation modes. - */ - struct SwapChainSupportDetails { - vk::SurfaceCapabilitiesKHR capabilities; ///< Surface capabilities - std::vector formats; ///< Supported surface formats - std::vector present_modes; ///< Supported presentation modes - }; - - /** - * @brief Struct representing a uniform buffer object. - * - * This struct holds the model, view, and projection matrices for use in shaders. - */ - struct UniformBufferObject { - alignas(16) glm::mat4 model; ///< Model transformation matrix - alignas(16) glm::mat4 view; ///< View transformation matrix - alignas(16) glm::mat4 proj; ///< Projection matrix - }; - - struct Camera { - glm::dvec3 position; - glm::dvec3 front; - glm::dvec3 up; - glm::dvec3 right; - f64 yaw; - f64 pitch; - - Camera() - : position(2.0, 2.0, 2.0), - front(glm::normalize(glm::dvec3(-2.0, -2.0, -2.0))), - up(0.0, 0.0, 1.0), - right(glm::normalize(glm::cross(front, up))), - yaw(-135.0) // -135 degrees to match the initial front vector - , - pitch(-35.26) // -35.26 degrees to match the initial front vector - { - updateCameraVectors(); - } - - [[nodiscard]] fn getPosition() const -> glm::dvec3 { return position; } - - [[nodiscard]] fn getViewMatrix() const -> glm::mat4 { - return glm::lookAt(position, position + front, up); - } - - fn moveForward(f64 deltaTime) -> void { - // Project front vector onto horizontal plane by zeroing Z component - glm::dvec3 horizontalFront = front; - horizontalFront.z = 0.0; - horizontalFront = glm::normalize(horizontalFront); - position += horizontalFront * CAMERA_SPEED * deltaTime; - } - - fn moveBackward(f64 deltaTime) -> void { - // Project front vector onto horizontal plane by zeroing Z component - glm::dvec3 horizontalFront = front; - horizontalFront.z = 0.0; - horizontalFront = glm::normalize(horizontalFront); - position -= horizontalFront * CAMERA_SPEED * deltaTime; - } - - fn moveLeft(f64 deltaTime) -> void { - // Project right vector onto horizontal plane by zeroing Z component - glm::dvec3 horizontalRight = right; - horizontalRight.z = 0.0; - horizontalRight = glm::normalize(horizontalRight); - position -= horizontalRight * CAMERA_SPEED * deltaTime; - } - - fn moveRight(f64 deltaTime) -> void { - // Project right vector onto horizontal plane by zeroing Z component - glm::dvec3 horizontalRight = right; - horizontalRight.z = 0.0; - horizontalRight = glm::normalize(horizontalRight); - position += horizontalRight * CAMERA_SPEED * deltaTime; - } - - fn moveUp(f64 deltaTime) -> void { position += glm::dvec3(0.0, 0.0, 1.0) * CAMERA_SPEED * deltaTime; } - - fn moveDown(f64 deltaTime) -> void { position -= glm::dvec3(0.0, 0.0, 1.0) * CAMERA_SPEED * deltaTime; } - - fn rotate(f64 xoffset, f64 yoffset) -> void { - const f64 sensitivity = 0.1; - yaw += xoffset * sensitivity; - pitch += yoffset * sensitivity; - - // Constrain pitch to avoid camera flipping - if (pitch > 89.0) - pitch = 89.0; - if (pitch < -89.0) - pitch = -89.0; - - updateCameraVectors(); - } - - private: - fn updateCameraVectors() -> void { - // Calculate new front vector - glm::dvec3 newFront; - newFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); - newFront.y = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); - newFront.z = sin(glm::radians(pitch)); - - front = glm::normalize(newFront); - // Recalculate right and up vectors - right = glm::normalize(glm::cross(front, glm::dvec3(0.0, 0.0, 1.0))); - up = glm::normalize(glm::cross(right, front)); - } - }; - - Camera mCamera; ///< Camera object - - static fn processInput(vkfw::Window& window, Camera& camera, const f32& deltaTime, const f32& cameraSpeed) - -> void { - if (window.getKey(vkfw::Key::eW) == vkfw::eTrue) - camera.moveForward(static_cast(deltaTime * cameraSpeed)); - if (window.getKey(vkfw::Key::eA) == vkfw::eTrue) - camera.moveLeft(static_cast(deltaTime * cameraSpeed)); - if (window.getKey(vkfw::Key::eS) == vkfw::eTrue) - camera.moveBackward(static_cast(deltaTime * cameraSpeed)); - if (window.getKey(vkfw::Key::eD) == vkfw::eTrue) - camera.moveRight(static_cast(deltaTime * cameraSpeed)); - if (window.getKey(vkfw::Key::eSpace) == vkfw::eTrue) - camera.moveUp(static_cast(deltaTime * cameraSpeed)); - if (window.getKey(vkfw::Key::eLeftShift) == vkfw::eTrue) - camera.moveDown(static_cast(deltaTime * cameraSpeed)); - } + Camera mCamera; ///< Camera object for the scene /** * @brief Initializes the application window using GLFW. @@ -459,17 +295,23 @@ class VulkanApp { createDepthResources(); // Create resources for depth testing createFramebuffers(); // Create framebuffers for rendering createTextureImage(); // Load and create the texture image - createTextureImageView(); // Create an image view for the texture - createTextureSampler(); // Create a sampler for the texture - loadModel(); // Load the 3D model - createVertexBuffer(); // Create a buffer for vertex data - createIndexBuffer(); // Create a buffer for index data - createUniformBuffers(); // Create uniform buffers for shader parameters - createDescriptorPool(); // Create a descriptor pool - createDescriptorSets(); // Allocate and update descriptor sets - createCommandBuffers(); // Create command buffers for rendering commands - createSyncObjects(); // Create synchronization objects (semaphores and fences) - initImGui(); // Initialize Dear ImGui for GUI rendering + mTextureImageView = graphics::createImageView( + mDevice.get(), + mTextureImage.get(), + vk::Format::eR8G8B8A8Srgb, + vk::ImageAspectFlagBits::eColor, + mMipLevels + ); + createTextureSampler(); // Create a sampler for the texture + loadModel(); // Load the 3D model + createVertexBuffer(); // Create a buffer for vertex data + createIndexBuffer(); // Create a buffer for index data + createUniformBuffers(); // Create uniform buffers for shader parameters + createDescriptorPool(); // Create a descriptor pool + createDescriptorSets(); // Allocate and update descriptor sets + createCommandBuffers(); // Create command buffers for rendering commands + createSyncObjects(); // Create synchronization objects (semaphores and fences) + initImGui(); // Initialize Dear ImGui for GUI rendering } /** @@ -512,10 +354,11 @@ class VulkanApp { mImGuiDescriptorPool = mDevice->createDescriptorPoolUnique(poolInfo); ImGui_ImplVulkan_InitInfo initInfo = { - .Instance = mInstance.get(), - .PhysicalDevice = mPhysicalDevice, - .Device = mDevice.get(), - .QueueFamily = findQueueFamilies(mPhysicalDevice).graphics_family.value(), + .Instance = mInstance.get(), + .PhysicalDevice = mPhysicalDevice, + .Device = mDevice.get(), + .QueueFamily = + QueueFamilyIndices::findQueueFamilies(mPhysicalDevice, mSurface.get()).graphics_family.value(), .Queue = mGraphicsQueue, .DescriptorPool = mImGuiDescriptorPool.get(), .RenderPass = mRenderPass.get(), @@ -551,7 +394,7 @@ class VulkanApp { deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; - processInput(mWindow.get(), mCamera, static_cast(deltaTime), mCameraSpeed); + Camera::processInput(mWindow.get(), mCamera, static_cast(deltaTime), mCameraSpeed); mView = mCamera.getViewMatrix(); if (currentFrame - lastFpsUpdate > 1.0) { @@ -741,9 +584,9 @@ class VulkanApp { #endif // Set the first suitable device as the physical device - if (isDeviceSuitable(device)) { + if (isDeviceSuitable(device, mSurface.get())) { mPhysicalDevice = device; - mMsaaSamples = getMaxUsableSampleCount(); + mMsaaSamples = getMaxUsableSampleCount(device); break; } } @@ -761,7 +604,7 @@ class VulkanApp { */ fn createLogicalDevice() -> void { // Get the queue families - QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice); + QueueFamilyIndices qfIndices = QueueFamilyIndices::findQueueFamilies(mPhysicalDevice, mSurface.get()); std::vector queueCreateInfos; @@ -813,11 +656,15 @@ class VulkanApp { * It determines the format, presentation mode, and extent of the swap chain images. */ fn createSwapChain() -> void { - SwapChainSupportDetails swapChainSupport = querySwapChainSupport(mPhysicalDevice); + SwapChainSupportDetails swapChainSupport = + SwapChainSupportDetails::querySwapChainSupport(mPhysicalDevice, mSurface.get()); - vk::SurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); - vk::PresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.present_modes); - vk::Extent2D extent = chooseSwapExtent(swapChainSupport.capabilities); + vk::SurfaceFormatKHR surfaceFormat = + SwapChainSupportDetails::chooseSwapSurfaceFormat(swapChainSupport.formats); + vk::PresentModeKHR presentMode = + SwapChainSupportDetails::chooseSwapPresentMode(swapChainSupport.present_modes); + vk::Extent2D extent = + SwapChainSupportDetails::chooseSwapExtent(swapChainSupport.capabilities, WIDTH, HEIGHT); u32 imageCount = swapChainSupport.capabilities.minImageCount + 1; @@ -825,7 +672,7 @@ class VulkanApp { imageCount > swapChainSupport.capabilities.maxImageCount) imageCount = swapChainSupport.capabilities.maxImageCount; - QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice); + QueueFamilyIndices qfIndices = QueueFamilyIndices::findQueueFamilies(mPhysicalDevice, mSurface.get()); std::array queueFamilyIndices = { qfIndices.graphics_family.value(), qfIndices.present_family.value(), @@ -869,8 +716,9 @@ class VulkanApp { mSwapChainImageViews.resize(mSwapChainImages.size()); for (u32 i = 0; i < mSwapChainImages.size(); i++) - mSwapChainImageViews[i] = - createImageView(mSwapChainImages[i], mSwapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1); + mSwapChainImageViews[i] = createImageView( + mDevice.get(), mSwapChainImages[i], mSwapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1 + ); } /** @@ -1008,12 +856,14 @@ class VulkanApp { */ fn createGraphicsPipeline() -> void { std::vector vertShaderCode = - ShaderCompiler::getCompiledShader(VERTEX_SHADER_PATH, shaderc_shader_kind::shaderc_vertex_shader); + ShaderCompiler::getCompiledShader(VERTEX_SHADER_PATH, shaderc_vertex_shader); std::vector fragShaderCode = - ShaderCompiler::getCompiledShader(FRAGMENT_SHADER_PATH, shaderc_shader_kind::shaderc_fragment_shader); + ShaderCompiler::getCompiledShader(FRAGMENT_SHADER_PATH, shaderc_fragment_shader); - vk::UniqueShaderModule vertShaderModule = createShaderModule(vertShaderCode); - vk::UniqueShaderModule fragShaderModule = createShaderModule(fragShaderCode); + vk::UniqueShaderModule vertShaderModule = + ShaderCompiler::createShaderModule(mDevice.get(), vertShaderCode); + vk::UniqueShaderModule fragShaderModule = + ShaderCompiler::createShaderModule(mDevice.get(), fragShaderCode); vk::PipelineShaderStageCreateInfo vertShaderStageInfo { .stage = vk::ShaderStageFlagBits::eVertex, @@ -1164,7 +1014,8 @@ class VulkanApp { * the buffers from which command buffer memory is allocated. */ fn createCommandPool() -> void { - QueueFamilyIndices queueFamilyIndices = findQueueFamilies(mPhysicalDevice); + QueueFamilyIndices queueFamilyIndices = + QueueFamilyIndices::findQueueFamilies(mPhysicalDevice, mSurface.get()); vk::CommandPoolCreateInfo poolInfo { .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = queueFamilyIndices.graphics_family.value() }; @@ -1181,6 +1032,8 @@ class VulkanApp { vk::Format colorFormat = mSwapChainImageFormat; std::tie(mColorImage, mColorImageMemory) = createImage( + mDevice.get(), + mPhysicalDevice, mSwapChainExtent.width, mSwapChainExtent.height, 1, @@ -1191,7 +1044,8 @@ class VulkanApp { vk::MemoryPropertyFlagBits::eDeviceLocal ); - mColorImageView = createImageView(mColorImage.get(), colorFormat, vk::ImageAspectFlagBits::eColor, 1); + mColorImageView = + createImageView(mDevice.get(), mColorImage.get(), colorFormat, vk::ImageAspectFlagBits::eColor, 1); } /** @@ -1203,6 +1057,8 @@ class VulkanApp { vk::Format depthFormat = findDepthFormat(); std::tie(mDepthImage, mDepthImageMemory) = createImage( + mDevice.get(), + mPhysicalDevice, mSwapChainExtent.width, mSwapChainExtent.height, 1, @@ -1213,7 +1069,8 @@ class VulkanApp { vk::MemoryPropertyFlagBits::eDeviceLocal ); - mDepthImageView = createImageView(mDepthImage.get(), depthFormat, vk::ImageAspectFlagBits::eDepth, 1); + mDepthImageView = + createImageView(mDevice.get(), mDepthImage.get(), depthFormat, vk::ImageAspectFlagBits::eDepth, 1); } /** @@ -1304,6 +1161,8 @@ class VulkanApp { copyData(stagingBufferMemory.get(), imageSize, pixels); std::tie(mTextureImage, mTextureImageMemory) = createImage( + mDevice.get(), + mPhysicalDevice, static_cast(texWidth), static_cast(texHeight), mMipLevels, @@ -1316,11 +1175,23 @@ class VulkanApp { ); transitionImageLayout( - mTextureImage.get(), vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, mMipLevels + mDevice.get(), + mCommandPool.get(), + mGraphicsQueue, + mTextureImage.get(), + vk::ImageLayout::eUndefined, + vk::ImageLayout::eTransferDstOptimal, + mMipLevels ); copyBufferToImage( - stagingBuffer.get(), mTextureImage.get(), static_cast(texWidth), static_cast(texHeight) + mDevice.get(), + mCommandPool.get(), + mGraphicsQueue, + stagingBuffer.get(), + mTextureImage.get(), + static_cast(texWidth), + static_cast(texHeight) ); generateMipmaps(mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, texWidth, texHeight, mMipLevels); @@ -1450,47 +1321,13 @@ class VulkanApp { endSingleTimeCommands(commandBuffer); } - /** - * @brief Gets the maximum usable sample count for multisampling. - * - * @return The maximum sample count supported by the device for both color and depth. - * - * This function determines the highest sample count that is supported by the device - * for both color and depth attachments. - */ - fn getMaxUsableSampleCount() -> vk::SampleCountFlagBits { - vk::PhysicalDeviceProperties physicalDeviceProperties = mPhysicalDevice.getProperties(); - - vk::SampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & - physicalDeviceProperties.limits.framebufferDepthSampleCounts; - - // Define an array of sample counts in descending order - const std::array sampleCounts = { - vk::SampleCountFlagBits::e64, vk::SampleCountFlagBits::e32, vk::SampleCountFlagBits::e16, - vk::SampleCountFlagBits::e8, vk::SampleCountFlagBits::e4, vk::SampleCountFlagBits::e2, - vk::SampleCountFlagBits::e1, - }; - - // Loop through the array and return the first supported sample count - for (const vk::SampleCountFlagBits& count : sampleCounts) - if (counts & count) - return count; - - // Return e1 if no other sample count is supported - return vk::SampleCountFlagBits::e1; - } - /** * @brief Creates the texture image view. * * This function creates an image view for the texture image, which can be used * to access the texture in shaders. */ - fn createTextureImageView() -> void { - mTextureImageView = createImageView( - mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mMipLevels - ); - } + fn createTextureImageView() -> void {} /** * @brief Creates the texture sampler. @@ -1521,167 +1358,6 @@ class VulkanApp { mTextureSampler = mDevice->createSamplerUnique(samplerInfo); } - /** - * @brief Creates a Vulkan image view. - * - * This function creates and returns a unique Vulkan image view using the provided parameters. - * - * @param image The Vulkan image for which to create the view. - * @param format The format of the image. - * @param aspectFlags The aspect flags for the image view. - * @param mipLevels The number of mip levels for the image view. - * - * @return vk::UniqueImageView A unique handle to the created Vulkan image view. - * - * @details - * The function creates an image view with the following properties: - * - 2D view type - * - Subresource range starting from base mip level 0 - * - Single array layer starting from base array layer 0 - */ - fn createImageView( - const vk::Image& image, - const vk::Format& format, - const vk::ImageAspectFlags& aspectFlags, - const u32& mipLevels - ) -> vk::UniqueImageView { - return mDevice->createImageViewUnique({ - .image = image, - .viewType = vk::ImageViewType::e2D, - .format = format, - .subresourceRange = { - .aspectMask = aspectFlags, - .baseMipLevel = 0, - .levelCount = mipLevels, - .baseArrayLayer = 0, - .layerCount = 1, - }, - }); - } - - fn createImage( - const u32& width, - const u32& height, - const u32& mipLevels, - const vk::SampleCountFlagBits& numSamples, - const vk::Format& format, - const vk::ImageTiling& tiling, - const vk::ImageUsageFlags& usage, - const vk::MemoryPropertyFlags& properties - ) -> std::pair { - // Define the image creation info - vk::ImageCreateInfo imageInfo { - .imageType = vk::ImageType::e2D, - .format = format, - .extent = { .width = width, .height = height, .depth = 1 }, - .mipLevels = mipLevels, - .arrayLayers = 1, - .samples = numSamples, - .tiling = tiling, - .usage = usage, - .sharingMode = vk::SharingMode::eExclusive, - .initialLayout = vk::ImageLayout::eUndefined, - }; - - // Create the image - vk::UniqueImage image = mDevice->createImageUnique(imageInfo); - - // Get the memory requirements for the image - vk::MemoryRequirements memRequirements = mDevice->getImageMemoryRequirements(image.get()); - - // Memory allocation info - vk::MemoryAllocateInfo allocInfo { - .allocationSize = memRequirements.size, - .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties), - }; - - // Allocate memory - vk::UniqueDeviceMemory imageMemory = mDevice->allocateMemoryUnique(allocInfo); - - // Bind the allocated memory to the image - mDevice->bindImageMemory(image.get(), imageMemory.get(), 0); - - // Return the unique image - return { std::move(image), std::move(imageMemory) }; - } - - // Transition image between layouts - fn transitionImageLayout( - const vk::Image& image, - const vk::ImageLayout& oldLayout, - const vk::ImageLayout& newLayout, - const u32& mipLevels - ) -> void { - // Create a command buffer - vk::CommandBuffer commandBuffer = beginSingleTimeCommands(); - - // Define the image memory barrier - vk::ImageMemoryBarrier barrier { - .oldLayout = oldLayout, - .newLayout = newLayout, - .srcQueueFamilyIndex = vk::QueueFamilyIgnored, - .dstQueueFamilyIndex = vk::QueueFamilyIgnored, - .image = image, - .subresourceRange = { - .aspectMask = vk::ImageAspectFlagBits::eColor, - .baseMipLevel = 0, - .levelCount = mipLevels, - .baseArrayLayer = 0, - .layerCount = 1, - }, - }; - - // Define the source and destination stages - vk::PipelineStageFlags sourceStage; - vk::PipelineStageFlags destinationStage; - - // Define the access masks - if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; - - sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; - destinationStage = vk::PipelineStageFlagBits::eTransfer; - } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && - newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { - barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; - - sourceStage = vk::PipelineStageFlagBits::eTransfer; - destinationStage = vk::PipelineStageFlagBits::eFragmentShader; - } else { - // Ensure that the layout transition is supported - throw std::invalid_argument("Unsupported layout transition!"); - } - - // Record the pipeline barrier - commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, {}, {}, barrier); - - // End the command buffer - endSingleTimeCommands(commandBuffer); - } - - fn copyBufferToImage(const vk::Buffer& buffer, const vk::Image& image, const u32& width, const u32& height) - -> void { - vk::CommandBuffer commandBuffer = beginSingleTimeCommands(); - - vk::BufferImageCopy region { - .bufferOffset = 0, - .bufferRowLength = 0, - .bufferImageHeight = 0, - .imageSubresource = { .aspectMask = vk::ImageAspectFlagBits::eColor, - .mipLevel = 0, - .baseArrayLayer = 0, - .layerCount = 1 }, - .imageOffset = { .x = 0, .y = 0, .z = 0 }, - .imageExtent = { .width = width, .height = height, .depth = 1 }, - }; - - commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, 1, ®ion); - - endSingleTimeCommands(commandBuffer); - } - /** * @brief Loads the 3D model. * @@ -2477,143 +2153,6 @@ class VulkanApp { return vk::PresentModeKHR::eFifo; } - /** - * @brief Chooses the swap extent (resolution) for the swap chain. - * - * @param capabilities The surface capabilities of the device. - * @return The chosen swap extent. - * - * This function determines the resolution of the swap chain images, - * taking into account the current window size and device limitations. - */ - fn chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) -> vk::Extent2D { - // If the resolution is not UINT32_MAX, return it - // Otherwise, we need to set the resolution manually - if (capabilities.currentExtent.width != UINT32_MAX) - return capabilities.currentExtent; - - // Get the window's resolution - u32 width = 0, height = 0; - std::tie(width, height) = mWindow->getFramebufferSize(); - - // Return the resolution clamped to the supported range - return { - .width = std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), - .height = std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height), - }; - } - - /** - * @brief Queries the swap chain support details for a physical device. - * - * @param device The physical device to query. - * @return A SwapChainSupportDetails struct containing the support information. - * - * This function retrieves information about the swap chain support, - * including surface capabilities, formats, and presentation modes. - */ - fn querySwapChainSupport(const vk::PhysicalDevice& device) -> SwapChainSupportDetails { - return { - .capabilities = device.getSurfaceCapabilitiesKHR(mSurface.get()), - .formats = device.getSurfaceFormatsKHR(mSurface.get()), - .present_modes = device.getSurfacePresentModesKHR(mSurface.get()), - }; - } - - /** - * @brief Checks if a physical device is suitable for the application. - * - * @param device The physical device to check. - * @return True if the device is suitable, false otherwise. - * - * This function checks if a physical device meets all the requirements - * of the application, including queue families, extensions, and features. - */ - fn isDeviceSuitable(const vk::PhysicalDevice& device) -> bool { - // Get the queue families that support the required operations - QueueFamilyIndices qfIndices = findQueueFamilies(device); - - // Check if the device supports the required extensions - bool extensionsSupported = checkDeviceExtensionSupport(device); - - bool swapChainAdequate = false; - - if (extensionsSupported) { - SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); - // Check if the swap chain is adequate (make sure it has - // at least one supported format and presentation mode) - swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.present_modes.empty(); - } - - // Check if the device supports the required features - vk::PhysicalDeviceFeatures supportedFeatures = device.getFeatures(); - - // If the device supports everything required, return true - return qfIndices.isComplete() && extensionsSupported && swapChainAdequate && - supportedFeatures.samplerAnisotropy; - } - - /** - * @brief Checks if a device supports all required extensions. - * - * @param device The physical device to check. - * @return True if all required extensions are supported, false otherwise. - * - * This function verifies that a physical device supports all the - * extensions required by the application. - */ - static fn checkDeviceExtensionSupport(const vk::PhysicalDevice& device) -> bool { - // Get the available extensions - std::vector availableExtensions = device.enumerateDeviceExtensionProperties(); - - // Create a set of required extension names - std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end()); - - // Remove each required extension from the set of available extensions - for (const vk::ExtensionProperties& extension : availableExtensions) - requiredExtensions.erase(extension.extensionName); - - // If the set is empty, all required extensions are supported - return requiredExtensions.empty(); - } - - /** - * @brief Finds queue families that support required operations. - * - * @param device The physical device to check. - * @return A QueueFamilyIndices struct with the found queue family indices. - * - * This function finds queue families that support graphics operations - * and presentation to the window surface. - */ - fn findQueueFamilies(const vk::PhysicalDevice& device) -> QueueFamilyIndices { - // Create a struct to store the queue family indices - QueueFamilyIndices qfIndices; - - // Get the queue family properties - std::vector queueFamilies = device.getQueueFamilyProperties(); - - // For every queue family, - for (u32 i = 0; i < queueFamilies.size(); i++) { - // Check if the queue family supports the required operations - if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics) - qfIndices.graphics_family = i; - - // Check if the queue family supports presentation - vk::Bool32 queuePresentSupport = device.getSurfaceSupportKHR(i, mSurface.get()); - - // If the queue family supports presentation, set the present family index - if (queuePresentSupport) - qfIndices.present_family = i; - - // If the queue family supports both operations, we're done - if (qfIndices.isComplete()) - break; - } - - return qfIndices; - } - /** * @brief Checks if all requested validation layers are available. * diff --git a/src/util/shaders.hpp b/src/util/shaders.hpp deleted file mode 100644 index b6d43a4..0000000 --- a/src/util/shaders.hpp +++ /dev/null @@ -1,209 +0,0 @@ -/** - * @file shaders.hpp - * @brief SPIR-V shader compilation and caching system. - * - * This file provides functionality for compiling GLSL shaders to SPIR-V and - * managing a shader cache system. It supports automatic recompilation when - * source files are modified and efficient caching of compiled shaders. - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "types.hpp" - -/** - * @brief Handles shader compilation and caching operations. - * - * This class provides static methods for compiling GLSL shaders to SPIR-V - * and managing a cache system. It automatically detects when shaders need - * to be recompiled based on file timestamps and provides efficient caching - * of compiled shader binaries. - */ -class ShaderCompiler { - public: - ShaderCompiler() = default; - - /** - * @brief Compiles or retrieves a cached SPIR-V shader. - * - * @param shaderPath Path to the GLSL shader source file - * @param kind Type of shader (vertex, fragment, compute, etc.) - * @return std::vector Compiled SPIR-V binary code - * @throws std::runtime_error If shader compilation fails or file is not found - * - * This function performs the following steps: - * 1. Checks if a cached version exists and is up-to-date - * 2. Loads from cache if available and valid - * 3. Otherwise, compiles the shader from source - * 4. Caches the newly compiled shader for future use - * 5. Returns the SPIR-V binary code - */ - static fn getCompiledShader(const std::filesystem::path& shaderPath, const shaderc_shader_kind& kind) - -> std::vector { - using namespace std; - - // Convert to absolute path if relative - filesystem::path absPath = filesystem::absolute(shaderPath); - - if (!filesystem::exists(absPath)) - throw runtime_error("Shader file not found: " + absPath.string()); - - const string shaderName = absPath.stem().string(); - const filesystem::path cacheFile = getCacheFilePath(shaderName); - - // Check if we need to recompile by comparing timestamps - if (filesystem::exists(cacheFile)) { - const auto sourceTime = filesystem::last_write_time(absPath); - const auto cacheTime = filesystem::last_write_time(cacheFile); - - if (cacheTime >= sourceTime) { - // Cache is up to date, load it - vector spirvCode = loadCachedShader(cacheFile); - if (!spirvCode.empty()) { - fmt::println("Loaded shader from cache: {}", cacheFile.string()); - return spirvCode; - } - } - } - - // Need to compile the shader - fmt::println("Compiling shader: {}", absPath.string()); - - // Read shader source - ifstream file(absPath); - if (!file) - throw runtime_error("Failed to open shader file: " + absPath.string()); - - string shaderSource((istreambuf_iterator(file)), istreambuf_iterator()); - file.close(); - - // Compile the shader - vector spirvCode = compileShader(shaderSource.c_str(), kind); - - if (spirvCode.empty()) - throw runtime_error("Shader compilation failed for: " + absPath.string()); - - // Cache the compiled SPIR-V binary - saveCompiledShader(spirvCode, cacheFile.string()); - return spirvCode; - } - - private: - /** - * @brief Determines the platform-specific shader cache directory. - * - * @param shaderName Base name of the shader file - * @return std::filesystem::path Full path to the cache file - * - * Cache locations: - * - Windows: %LOCALAPPDATA%/VulkanApp/Shaders/ - * - macOS: ~/Library/Application Support/VulkanApp/Shaders/ - * - Linux: ~/.local/share/VulkanApp/Shaders/ - * - * The directory is created if it doesn't exist. - */ - static fn getCacheFilePath(const string& shaderName) -> std::filesystem::path { - using namespace std::filesystem; - -#ifdef _WIN32 - path cacheDir = path(getenv("LOCALAPPDATA")) / "VulkanApp" / "Shaders"; -#elif defined(__APPLE__) - path cacheDir = path(getenv("HOME")) / "Library" / "Application Support" / "VulkanApp" / "Shaders"; -#else // Assume Linux or other UNIX-like systems - path cacheDir = path(getenv("HOME")) / ".local" / "share" / "VulkanApp" / "Shaders"; -#endif - - if (!exists(cacheDir)) - create_directories(cacheDir); - - return cacheDir / (shaderName + ".spv"); - } - - /** - * @brief Loads a cached SPIR-V shader from disk. - * - * @param cachePath Path to the cached shader file - * @return std::vector SPIR-V binary code, empty if loading fails - * - * Reads the binary SPIR-V data from the cache file. Returns an empty - * vector if the file cannot be opened or read properly. - */ - static fn loadCachedShader(const std::filesystem::path& cachePath) -> std::vector { - std::ifstream file(cachePath, std::ios::binary); - if (!file.is_open()) - return {}; - - // Read file size - file.seekg(0, std::ios::end); - const std::streamoff fileSize = file.tellg(); - file.seekg(0, std::ios::beg); - - // Allocate buffer and read data - std::vector buffer(static_cast(fileSize) / sizeof(u32)); - file.read(std::bit_cast(buffer.data()), fileSize); - - return buffer; - } - - /** - * @brief Compiles GLSL source code to SPIR-V. - * - * @param source GLSL shader source code - * @param kind Type of shader being compiled - * @return std::vector Compiled SPIR-V binary code - * - * Uses the shaderc library to compile GLSL to SPIR-V. The compilation - * is performed with optimization level set to performance and generates - * debug information in debug builds. - */ - static fn compileShader(const char* source, shaderc_shader_kind kind) -> std::vector { - shaderc::Compiler compiler; - shaderc::CompileOptions options; - - // Set compilation options -#ifdef NDEBUG - options.SetOptimizationLevel(shaderc_optimization_level_performance); -#else - options.SetOptimizationLevel(shaderc_optimization_level_zero); - options.SetGenerateDebugInfo(); -#endif - - // Compile the shader - shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, kind, "shader", options); - - if (module.GetCompilationStatus() != shaderc_compilation_status_success) - return {}; - - return { module.cbegin(), module.cend() }; - } - - /** - * @brief Saves compiled SPIR-V code to the cache. - * - * @param spirv Compiled SPIR-V binary code - * @param cachePath Path where the cache file should be saved - * @return bool True if save was successful, false otherwise - * - * Writes the SPIR-V binary to disk for future use. Creates any - * necessary parent directories if they don't exist. - */ - static fn saveCompiledShader(const std::vector& spirv, const std::string& cachePath) -> bool { - std::ofstream file(cachePath, std::ios::binary); - if (!file.is_open()) - return false; - - file.write( - std::bit_cast(spirv.data()), static_cast(spirv.size() * sizeof(u32)) - ); - - return file.good(); - } -};