ughghghgh
This commit is contained in:
parent
2f779d28ac
commit
414e7a8d3a
34
src/config/config.hpp
Normal file
34
src/config/config.hpp
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
|
#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<const char*, 1> validationLayers = { "VK_LAYER_KHRONOS_validation" };
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Required device extensions (platform-specific)
|
||||||
|
#ifdef __APPLE__
|
||||||
|
constexpr std::array<const char*, 2> deviceExtensions = { vk::KHRSwapchainExtensionName,
|
||||||
|
vk::KHRPortabilitySubsetExtensionName };
|
||||||
|
#else
|
||||||
|
constexpr std::array<const char*, 1> deviceExtensions = { vk::KHRSwapchainExtensionName };
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace config
|
95
src/core/device_utils.hpp
Normal file
95
src/core/device_utils.hpp
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
|
#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<vk::ExtensionProperties> availableExtensions = device.enumerateDeviceExtensionProperties();
|
||||||
|
|
||||||
|
// Create a set of required extensions
|
||||||
|
std::unordered_set<std::string> 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
|
144
src/core/queue_structures.hpp
Normal file
144
src/core/queue_structures.hpp
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
|
#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<u32> graphics_family; ///< Index of graphics queue family
|
||||||
|
std::optional<u32> 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<vk::QueueFamilyProperties> 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<vk::SurfaceFormatKHR> formats; ///< Supported surface formats
|
||||||
|
std::vector<vk::PresentModeKHR> 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<vk::SurfaceFormatKHR>& 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<vk::PresentModeKHR>& 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<u32>::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;
|
||||||
|
}
|
||||||
|
};
|
147
src/graphics/camera.hpp
Normal file
147
src/graphics/camera.hpp
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
|
||||||
|
#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<f64>(deltaTime * cameraSpeed));
|
||||||
|
if (window.getKey(vkfw::Key::eA) == vkfw::eTrue)
|
||||||
|
camera.moveLeft(static_cast<f64>(deltaTime * cameraSpeed));
|
||||||
|
if (window.getKey(vkfw::Key::eS) == vkfw::eTrue)
|
||||||
|
camera.moveBackward(static_cast<f64>(deltaTime * cameraSpeed));
|
||||||
|
if (window.getKey(vkfw::Key::eD) == vkfw::eTrue)
|
||||||
|
camera.moveRight(static_cast<f64>(deltaTime * cameraSpeed));
|
||||||
|
if (window.getKey(vkfw::Key::eSpace) == vkfw::eTrue)
|
||||||
|
camera.moveUp(static_cast<f64>(deltaTime * cameraSpeed));
|
||||||
|
if (window.getKey(vkfw::Key::eLeftShift) == vkfw::eTrue)
|
||||||
|
camera.moveDown(static_cast<f64>(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));
|
||||||
|
}
|
||||||
|
};
|
278
src/graphics/image_utils.hpp
Normal file
278
src/graphics/image_utils.hpp
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define VULKAN_HPP_NO_CONSTRUCTORS
|
||||||
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
|
#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<vk::UniqueImage, vk::UniqueDeviceMemory> 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<vk::UniqueImage, vk::UniqueDeviceMemory> {
|
||||||
|
// 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
|
230
src/graphics/shaders.hpp
Normal file
230
src/graphics/shaders.hpp
Normal file
|
@ -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 <filesystem>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <ios>
|
||||||
|
#include <shaderc/shaderc.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define VULKAN_HPP_NO_CONSTRUCTORS
|
||||||
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
|
#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<u32> 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<u32> {
|
||||||
|
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<u32> 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<char>(file)), istreambuf_iterator<char>());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Compile the shader
|
||||||
|
vector<u32> 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<u32>& 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<u32> 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<u32> {
|
||||||
|
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<u32> buffer(static_cast<u32>(fileSize) / sizeof(u32));
|
||||||
|
file.read(std::bit_cast<char*>(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<u32> 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<u32> {
|
||||||
|
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<u32>& spirv, const std::string& cachePath) -> bool {
|
||||||
|
std::ofstream file(cachePath, std::ios::binary);
|
||||||
|
if (!file.is_open())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
file.write(
|
||||||
|
std::bit_cast<const char*>(spirv.data()), static_cast<std::streamsize>(spirv.size() * sizeof(u32))
|
||||||
|
);
|
||||||
|
|
||||||
|
return file.good();
|
||||||
|
}
|
||||||
|
}; // class ShaderCompiler
|
||||||
|
|
||||||
|
} // namespace graphics
|
14
src/graphics/uniform_buffer.hpp
Normal file
14
src/graphics/uniform_buffer.hpp
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
};
|
|
@ -8,11 +8,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#define STB_IMAGE_IMPLEMENTATION
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
#include <stb_image.h>
|
#include <stb_image.h>
|
||||||
|
|
||||||
#include "types.hpp"
|
#include "../core/types.hpp"
|
||||||
|
|
||||||
namespace stb {
|
namespace stb {
|
||||||
/**
|
/**
|
|
@ -15,7 +15,7 @@
|
||||||
#define VULKAN_HPP_NO_CONSTRUCTORS
|
#define VULKAN_HPP_NO_CONSTRUCTORS
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
#include "types.hpp"
|
#include "../core/types.hpp"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Represents a vertex in 3D space with color and texture information.
|
* @brief Represents a vertex in 3D space with color and texture information.
|
||||||
|
@ -46,7 +46,8 @@ struct Vertex {
|
||||||
/**
|
/**
|
||||||
* @brief Provides attribute descriptions for vertex data interpretation.
|
* @brief Provides attribute descriptions for vertex data interpretation.
|
||||||
*
|
*
|
||||||
* @return std::array<vk::VertexInputAttributeDescription, 3> Array of descriptions for position, color, and texture coordinates.
|
* @return std::array<vk::VertexInputAttributeDescription, 3> Array of descriptions for position, color, and
|
||||||
|
* texture coordinates.
|
||||||
*
|
*
|
||||||
* The attribute descriptions specify:
|
* The attribute descriptions specify:
|
||||||
* - Location indices (0 for position, 1 for color, 2 for texture coordinates)
|
* - Location indices (0 for position, 1 for color, 2 for texture coordinates)
|
607
src/main.cpp
607
src/main.cpp
|
@ -22,12 +22,6 @@
|
||||||
// Necessary for dynamic dispatch to work
|
// Necessary for dynamic dispatch to work
|
||||||
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
|
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
|
// ImGui headers for GUI
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <imgui_impl_glfw.h>
|
#include <imgui_impl_glfw.h>
|
||||||
|
@ -37,32 +31,21 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
|
||||||
#define VKFW_NO_STRUCT_CONSTRUCTORS // Use aggregate initialization for GLFW structs
|
#define VKFW_NO_STRUCT_CONSTRUCTORS // Use aggregate initialization for GLFW structs
|
||||||
#include "vkfw.hpp" // Include GLFW C++ bindings
|
#include "vkfw.hpp" // Include GLFW C++ bindings
|
||||||
|
|
||||||
// Constants for window dimensions
|
// Include custom utility headers
|
||||||
constexpr i32 WIDTH = 1920;
|
#include "config/config.hpp" // Configuration constants
|
||||||
constexpr i32 HEIGHT = 1080;
|
#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
|
using namespace config; // For easy access to configuration constants
|
||||||
constexpr f64 CAMERA_SPEED = 1.0;
|
using namespace core; // For core utilities
|
||||||
|
using namespace graphics; // For graphics utilities
|
||||||
// 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<const char*, 1> validationLayers = { "VK_LAYER_KHRONOS_validation" };
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Required device extensions (platform-specific)
|
|
||||||
#ifdef __APPLE__
|
|
||||||
constexpr std::array<const char*, 2> deviceExtensions = { vk::KHRSwapchainExtensionName,
|
|
||||||
vk::KHRPortabilitySubsetExtensionName };
|
|
||||||
#else
|
|
||||||
constexpr std::array<const char*, 1> deviceExtensions = { vk::KHRSwapchainExtensionName };
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The Vulkan application class.
|
* @brief The Vulkan application class.
|
||||||
|
@ -182,154 +165,7 @@ class VulkanApp {
|
||||||
f32 mFieldOfView = 90.0F; ///< Current field of view
|
f32 mFieldOfView = 90.0F; ///< Current field of view
|
||||||
bool mWireframeMode = false; ///< Wireframe rendering mode
|
bool mWireframeMode = false; ///< Wireframe rendering mode
|
||||||
|
|
||||||
/**
|
Camera mCamera; ///< Camera object for the scene
|
||||||
* @brief Struct to store queue family indices.
|
|
||||||
*
|
|
||||||
* This struct contains the indices of the graphics and presentation queue families.
|
|
||||||
*/
|
|
||||||
struct QueueFamilyIndices {
|
|
||||||
std::optional<u32> graphics_family; ///< Index of graphics queue family
|
|
||||||
std::optional<u32> 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<vk::SurfaceFormatKHR> formats; ///< Supported surface formats
|
|
||||||
std::vector<vk::PresentModeKHR> 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<f64>(deltaTime * cameraSpeed));
|
|
||||||
if (window.getKey(vkfw::Key::eA) == vkfw::eTrue)
|
|
||||||
camera.moveLeft(static_cast<f64>(deltaTime * cameraSpeed));
|
|
||||||
if (window.getKey(vkfw::Key::eS) == vkfw::eTrue)
|
|
||||||
camera.moveBackward(static_cast<f64>(deltaTime * cameraSpeed));
|
|
||||||
if (window.getKey(vkfw::Key::eD) == vkfw::eTrue)
|
|
||||||
camera.moveRight(static_cast<f64>(deltaTime * cameraSpeed));
|
|
||||||
if (window.getKey(vkfw::Key::eSpace) == vkfw::eTrue)
|
|
||||||
camera.moveUp(static_cast<f64>(deltaTime * cameraSpeed));
|
|
||||||
if (window.getKey(vkfw::Key::eLeftShift) == vkfw::eTrue)
|
|
||||||
camera.moveDown(static_cast<f64>(deltaTime * cameraSpeed));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initializes the application window using GLFW.
|
* @brief Initializes the application window using GLFW.
|
||||||
|
@ -459,7 +295,13 @@ class VulkanApp {
|
||||||
createDepthResources(); // Create resources for depth testing
|
createDepthResources(); // Create resources for depth testing
|
||||||
createFramebuffers(); // Create framebuffers for rendering
|
createFramebuffers(); // Create framebuffers for rendering
|
||||||
createTextureImage(); // Load and create the texture image
|
createTextureImage(); // Load and create the texture image
|
||||||
createTextureImageView(); // Create an image view for the texture
|
mTextureImageView = graphics::createImageView(
|
||||||
|
mDevice.get(),
|
||||||
|
mTextureImage.get(),
|
||||||
|
vk::Format::eR8G8B8A8Srgb,
|
||||||
|
vk::ImageAspectFlagBits::eColor,
|
||||||
|
mMipLevels
|
||||||
|
);
|
||||||
createTextureSampler(); // Create a sampler for the texture
|
createTextureSampler(); // Create a sampler for the texture
|
||||||
loadModel(); // Load the 3D model
|
loadModel(); // Load the 3D model
|
||||||
createVertexBuffer(); // Create a buffer for vertex data
|
createVertexBuffer(); // Create a buffer for vertex data
|
||||||
|
@ -515,7 +357,8 @@ class VulkanApp {
|
||||||
.Instance = mInstance.get(),
|
.Instance = mInstance.get(),
|
||||||
.PhysicalDevice = mPhysicalDevice,
|
.PhysicalDevice = mPhysicalDevice,
|
||||||
.Device = mDevice.get(),
|
.Device = mDevice.get(),
|
||||||
.QueueFamily = findQueueFamilies(mPhysicalDevice).graphics_family.value(),
|
.QueueFamily =
|
||||||
|
QueueFamilyIndices::findQueueFamilies(mPhysicalDevice, mSurface.get()).graphics_family.value(),
|
||||||
.Queue = mGraphicsQueue,
|
.Queue = mGraphicsQueue,
|
||||||
.DescriptorPool = mImGuiDescriptorPool.get(),
|
.DescriptorPool = mImGuiDescriptorPool.get(),
|
||||||
.RenderPass = mRenderPass.get(),
|
.RenderPass = mRenderPass.get(),
|
||||||
|
@ -551,7 +394,7 @@ class VulkanApp {
|
||||||
deltaTime = currentFrame - lastFrame;
|
deltaTime = currentFrame - lastFrame;
|
||||||
lastFrame = currentFrame;
|
lastFrame = currentFrame;
|
||||||
|
|
||||||
processInput(mWindow.get(), mCamera, static_cast<f32>(deltaTime), mCameraSpeed);
|
Camera::processInput(mWindow.get(), mCamera, static_cast<f32>(deltaTime), mCameraSpeed);
|
||||||
mView = mCamera.getViewMatrix();
|
mView = mCamera.getViewMatrix();
|
||||||
|
|
||||||
if (currentFrame - lastFpsUpdate > 1.0) {
|
if (currentFrame - lastFpsUpdate > 1.0) {
|
||||||
|
@ -741,9 +584,9 @@ class VulkanApp {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Set the first suitable device as the physical device
|
// Set the first suitable device as the physical device
|
||||||
if (isDeviceSuitable(device)) {
|
if (isDeviceSuitable(device, mSurface.get())) {
|
||||||
mPhysicalDevice = device;
|
mPhysicalDevice = device;
|
||||||
mMsaaSamples = getMaxUsableSampleCount();
|
mMsaaSamples = getMaxUsableSampleCount(device);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -761,7 +604,7 @@ class VulkanApp {
|
||||||
*/
|
*/
|
||||||
fn createLogicalDevice() -> void {
|
fn createLogicalDevice() -> void {
|
||||||
// Get the queue families
|
// Get the queue families
|
||||||
QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice);
|
QueueFamilyIndices qfIndices = QueueFamilyIndices::findQueueFamilies(mPhysicalDevice, mSurface.get());
|
||||||
|
|
||||||
std::vector<vk::DeviceQueueCreateInfo> queueCreateInfos;
|
std::vector<vk::DeviceQueueCreateInfo> queueCreateInfos;
|
||||||
|
|
||||||
|
@ -813,11 +656,15 @@ class VulkanApp {
|
||||||
* It determines the format, presentation mode, and extent of the swap chain images.
|
* It determines the format, presentation mode, and extent of the swap chain images.
|
||||||
*/
|
*/
|
||||||
fn createSwapChain() -> void {
|
fn createSwapChain() -> void {
|
||||||
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(mPhysicalDevice);
|
SwapChainSupportDetails swapChainSupport =
|
||||||
|
SwapChainSupportDetails::querySwapChainSupport(mPhysicalDevice, mSurface.get());
|
||||||
|
|
||||||
vk::SurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
|
vk::SurfaceFormatKHR surfaceFormat =
|
||||||
vk::PresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.present_modes);
|
SwapChainSupportDetails::chooseSwapSurfaceFormat(swapChainSupport.formats);
|
||||||
vk::Extent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
|
vk::PresentModeKHR presentMode =
|
||||||
|
SwapChainSupportDetails::chooseSwapPresentMode(swapChainSupport.present_modes);
|
||||||
|
vk::Extent2D extent =
|
||||||
|
SwapChainSupportDetails::chooseSwapExtent(swapChainSupport.capabilities, WIDTH, HEIGHT);
|
||||||
|
|
||||||
u32 imageCount = swapChainSupport.capabilities.minImageCount + 1;
|
u32 imageCount = swapChainSupport.capabilities.minImageCount + 1;
|
||||||
|
|
||||||
|
@ -825,7 +672,7 @@ class VulkanApp {
|
||||||
imageCount > swapChainSupport.capabilities.maxImageCount)
|
imageCount > swapChainSupport.capabilities.maxImageCount)
|
||||||
imageCount = swapChainSupport.capabilities.maxImageCount;
|
imageCount = swapChainSupport.capabilities.maxImageCount;
|
||||||
|
|
||||||
QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice);
|
QueueFamilyIndices qfIndices = QueueFamilyIndices::findQueueFamilies(mPhysicalDevice, mSurface.get());
|
||||||
std::array<u32, 2> queueFamilyIndices = {
|
std::array<u32, 2> queueFamilyIndices = {
|
||||||
qfIndices.graphics_family.value(),
|
qfIndices.graphics_family.value(),
|
||||||
qfIndices.present_family.value(),
|
qfIndices.present_family.value(),
|
||||||
|
@ -869,8 +716,9 @@ class VulkanApp {
|
||||||
mSwapChainImageViews.resize(mSwapChainImages.size());
|
mSwapChainImageViews.resize(mSwapChainImages.size());
|
||||||
|
|
||||||
for (u32 i = 0; i < mSwapChainImages.size(); i++)
|
for (u32 i = 0; i < mSwapChainImages.size(); i++)
|
||||||
mSwapChainImageViews[i] =
|
mSwapChainImageViews[i] = createImageView(
|
||||||
createImageView(mSwapChainImages[i], mSwapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1);
|
mDevice.get(), mSwapChainImages[i], mSwapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1008,12 +856,14 @@ class VulkanApp {
|
||||||
*/
|
*/
|
||||||
fn createGraphicsPipeline() -> void {
|
fn createGraphicsPipeline() -> void {
|
||||||
std::vector<u32> vertShaderCode =
|
std::vector<u32> vertShaderCode =
|
||||||
ShaderCompiler::getCompiledShader(VERTEX_SHADER_PATH, shaderc_shader_kind::shaderc_vertex_shader);
|
ShaderCompiler::getCompiledShader(VERTEX_SHADER_PATH, shaderc_vertex_shader);
|
||||||
std::vector<u32> fragShaderCode =
|
std::vector<u32> 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 vertShaderModule =
|
||||||
vk::UniqueShaderModule fragShaderModule = createShaderModule(fragShaderCode);
|
ShaderCompiler::createShaderModule(mDevice.get(), vertShaderCode);
|
||||||
|
vk::UniqueShaderModule fragShaderModule =
|
||||||
|
ShaderCompiler::createShaderModule(mDevice.get(), fragShaderCode);
|
||||||
|
|
||||||
vk::PipelineShaderStageCreateInfo vertShaderStageInfo {
|
vk::PipelineShaderStageCreateInfo vertShaderStageInfo {
|
||||||
.stage = vk::ShaderStageFlagBits::eVertex,
|
.stage = vk::ShaderStageFlagBits::eVertex,
|
||||||
|
@ -1164,7 +1014,8 @@ class VulkanApp {
|
||||||
* the buffers from which command buffer memory is allocated.
|
* the buffers from which command buffer memory is allocated.
|
||||||
*/
|
*/
|
||||||
fn createCommandPool() -> void {
|
fn createCommandPool() -> void {
|
||||||
QueueFamilyIndices queueFamilyIndices = findQueueFamilies(mPhysicalDevice);
|
QueueFamilyIndices queueFamilyIndices =
|
||||||
|
QueueFamilyIndices::findQueueFamilies(mPhysicalDevice, mSurface.get());
|
||||||
|
|
||||||
vk::CommandPoolCreateInfo poolInfo { .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
|
vk::CommandPoolCreateInfo poolInfo { .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
|
||||||
.queueFamilyIndex = queueFamilyIndices.graphics_family.value() };
|
.queueFamilyIndex = queueFamilyIndices.graphics_family.value() };
|
||||||
|
@ -1181,6 +1032,8 @@ class VulkanApp {
|
||||||
vk::Format colorFormat = mSwapChainImageFormat;
|
vk::Format colorFormat = mSwapChainImageFormat;
|
||||||
|
|
||||||
std::tie(mColorImage, mColorImageMemory) = createImage(
|
std::tie(mColorImage, mColorImageMemory) = createImage(
|
||||||
|
mDevice.get(),
|
||||||
|
mPhysicalDevice,
|
||||||
mSwapChainExtent.width,
|
mSwapChainExtent.width,
|
||||||
mSwapChainExtent.height,
|
mSwapChainExtent.height,
|
||||||
1,
|
1,
|
||||||
|
@ -1191,7 +1044,8 @@ class VulkanApp {
|
||||||
vk::MemoryPropertyFlagBits::eDeviceLocal
|
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();
|
vk::Format depthFormat = findDepthFormat();
|
||||||
|
|
||||||
std::tie(mDepthImage, mDepthImageMemory) = createImage(
|
std::tie(mDepthImage, mDepthImageMemory) = createImage(
|
||||||
|
mDevice.get(),
|
||||||
|
mPhysicalDevice,
|
||||||
mSwapChainExtent.width,
|
mSwapChainExtent.width,
|
||||||
mSwapChainExtent.height,
|
mSwapChainExtent.height,
|
||||||
1,
|
1,
|
||||||
|
@ -1213,7 +1069,8 @@ class VulkanApp {
|
||||||
vk::MemoryPropertyFlagBits::eDeviceLocal
|
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);
|
copyData(stagingBufferMemory.get(), imageSize, pixels);
|
||||||
|
|
||||||
std::tie(mTextureImage, mTextureImageMemory) = createImage(
|
std::tie(mTextureImage, mTextureImageMemory) = createImage(
|
||||||
|
mDevice.get(),
|
||||||
|
mPhysicalDevice,
|
||||||
static_cast<u32>(texWidth),
|
static_cast<u32>(texWidth),
|
||||||
static_cast<u32>(texHeight),
|
static_cast<u32>(texHeight),
|
||||||
mMipLevels,
|
mMipLevels,
|
||||||
|
@ -1316,11 +1175,23 @@ class VulkanApp {
|
||||||
);
|
);
|
||||||
|
|
||||||
transitionImageLayout(
|
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(
|
copyBufferToImage(
|
||||||
stagingBuffer.get(), mTextureImage.get(), static_cast<u32>(texWidth), static_cast<u32>(texHeight)
|
mDevice.get(),
|
||||||
|
mCommandPool.get(),
|
||||||
|
mGraphicsQueue,
|
||||||
|
stagingBuffer.get(),
|
||||||
|
mTextureImage.get(),
|
||||||
|
static_cast<u32>(texWidth),
|
||||||
|
static_cast<u32>(texHeight)
|
||||||
);
|
);
|
||||||
|
|
||||||
generateMipmaps(mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, texWidth, texHeight, mMipLevels);
|
generateMipmaps(mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, texWidth, texHeight, mMipLevels);
|
||||||
|
@ -1450,47 +1321,13 @@ class VulkanApp {
|
||||||
endSingleTimeCommands(commandBuffer);
|
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<vk::SampleCountFlagBits, 7> 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.
|
* @brief Creates the texture image view.
|
||||||
*
|
*
|
||||||
* This function creates an image view for the texture image, which can be used
|
* This function creates an image view for the texture image, which can be used
|
||||||
* to access the texture in shaders.
|
* to access the texture in shaders.
|
||||||
*/
|
*/
|
||||||
fn createTextureImageView() -> void {
|
fn createTextureImageView() -> void {}
|
||||||
mTextureImageView = createImageView(
|
|
||||||
mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mMipLevels
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates the texture sampler.
|
* @brief Creates the texture sampler.
|
||||||
|
@ -1521,167 +1358,6 @@ class VulkanApp {
|
||||||
mTextureSampler = mDevice->createSamplerUnique(samplerInfo);
|
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<vk::UniqueImage, vk::UniqueDeviceMemory> {
|
|
||||||
// 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.
|
* @brief Loads the 3D model.
|
||||||
*
|
*
|
||||||
|
@ -2477,143 +2153,6 @@ class VulkanApp {
|
||||||
return vk::PresentModeKHR::eFifo;
|
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<vk::ExtensionProperties> availableExtensions = device.enumerateDeviceExtensionProperties();
|
|
||||||
|
|
||||||
// Create a set of required extension names
|
|
||||||
std::set<string> 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<vk::QueueFamilyProperties> 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.
|
* @brief Checks if all requested validation layers are available.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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 <filesystem>
|
|
||||||
#include <fmt/format.h>
|
|
||||||
#include <fstream>
|
|
||||||
#include <ios>
|
|
||||||
#include <shaderc/shaderc.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#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<u32> 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<u32> {
|
|
||||||
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<u32> 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<char>(file)), istreambuf_iterator<char>());
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
// Compile the shader
|
|
||||||
vector<u32> 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<u32> 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<u32> {
|
|
||||||
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<u32> buffer(static_cast<u32>(fileSize) / sizeof(u32));
|
|
||||||
file.read(std::bit_cast<char*>(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<u32> 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<u32> {
|
|
||||||
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<u32>& spirv, const std::string& cachePath) -> bool {
|
|
||||||
std::ofstream file(cachePath, std::ios::binary);
|
|
||||||
if (!file.is_open())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
file.write(
|
|
||||||
std::bit_cast<const char*>(spirv.data()), static_cast<std::streamsize>(spirv.size() * sizeof(u32))
|
|
||||||
);
|
|
||||||
|
|
||||||
return file.good();
|
|
||||||
}
|
|
||||||
};
|
|
Loading…
Reference in a new issue