817 lines
32 KiB
C++
817 lines
32 KiB
C++
// Copyright 2025 Steven Le Rouzic
|
|
//
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
#include "hk21/game/gpu.hpp"
|
|
|
|
#include <asl/base/numeric.hpp>
|
|
#include <asl/containers/buffer.hpp>
|
|
#include <asl/containers/intrusive_list.hpp>
|
|
#include <asl/formatting/format.hpp>
|
|
#include <asl/types/option.hpp>
|
|
#include <asl/logging/logging.hpp>
|
|
|
|
#include <SDL3/SDL_vulkan.h>
|
|
|
|
#include "hk21/vulkan/loader/loader.hpp"
|
|
#include "hk21/vulkan/sync/sync.hpp"
|
|
|
|
// @Todo Make fences recyclable
|
|
// @Todo Make command pool recyclable
|
|
// @Todo Make frame structure recyclable
|
|
|
|
#define VK_ALLOCATOR nullptr
|
|
|
|
[[maybe_unused]] static void AslFormat(asl::Formatter& formatter, VkResult res)
|
|
{
|
|
switch (res) // NOLINT
|
|
{
|
|
case VK_SUCCESS: formatter.write("VK_SUCCESS"); break;
|
|
case VK_NOT_READY: formatter.write("VK_NOT_READY"); break;
|
|
case VK_TIMEOUT: formatter.write("VK_TIMEOUT"); break;
|
|
case VK_EVENT_SET: formatter.write("VK_EVENT_SET"); break;
|
|
case VK_EVENT_RESET: formatter.write("VK_EVENT_RESET"); break;
|
|
case VK_INCOMPLETE: formatter.write("VK_INCOMPLETE"); break;
|
|
case VK_ERROR_OUT_OF_HOST_MEMORY: formatter.write("VK_ERROR_OUT_OF_HOST_MEMORY"); break;
|
|
case VK_ERROR_OUT_OF_DEVICE_MEMORY: formatter.write("VK_ERROR_OUT_OF_DEVICE_MEMORY"); break;
|
|
case VK_ERROR_INITIALIZATION_FAILED: formatter.write("VK_ERROR_INITIALIZATION_FAILED"); break;
|
|
case VK_ERROR_DEVICE_LOST: formatter.write("VK_ERROR_DEVICE_LOST"); break;
|
|
case VK_ERROR_MEMORY_MAP_FAILED: formatter.write("VK_ERROR_MEMORY_MAP_FAILED"); break;
|
|
case VK_ERROR_LAYER_NOT_PRESENT: formatter.write("VK_ERROR_LAYER_NOT_PRESENT"); break;
|
|
case VK_ERROR_EXTENSION_NOT_PRESENT: formatter.write("VK_ERROR_EXTENSION_NOT_PRESENT"); break;
|
|
case VK_ERROR_FEATURE_NOT_PRESENT: formatter.write("VK_ERROR_FEATURE_NOT_PRESENT"); break;
|
|
case VK_ERROR_INCOMPATIBLE_DRIVER: formatter.write("VK_ERROR_INCOMPATIBLE_DRIVER"); break;
|
|
case VK_ERROR_TOO_MANY_OBJECTS: formatter.write("VK_ERROR_TOO_MANY_OBJECTS"); break;
|
|
case VK_ERROR_FORMAT_NOT_SUPPORTED: formatter.write("VK_ERROR_FORMAT_NOT_SUPPORTED"); break;
|
|
case VK_ERROR_FRAGMENTED_POOL: formatter.write("VK_ERROR_FRAGMENTED_POOL"); break;
|
|
case VK_ERROR_UNKNOWN: formatter.write("VK_ERROR_UNKNOWN"); break;
|
|
case VK_ERROR_OUT_OF_POOL_MEMORY: formatter.write("VK_ERROR_OUT_OF_POOL_MEMORY"); break;
|
|
case VK_ERROR_INVALID_EXTERNAL_HANDLE: formatter.write("VK_ERROR_INVALID_EXTERNAL_HANDLE"); break;
|
|
case VK_ERROR_FRAGMENTATION: formatter.write("VK_ERROR_FRAGMENTATION"); break;
|
|
case VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS: formatter.write("VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS"); break;
|
|
case VK_PIPELINE_COMPILE_REQUIRED: formatter.write("VK_PIPELINE_COMPILE_REQUIRED"); break;
|
|
case VK_ERROR_NOT_PERMITTED: formatter.write("VK_ERROR_NOT_PERMITTED"); break;
|
|
case VK_ERROR_SURFACE_LOST_KHR: formatter.write("VK_ERROR_SURFACE_LOST_KHR"); break;
|
|
case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: formatter.write("VK_ERROR_NATIVE_WINDOW_IN_USE_KHR"); break;
|
|
case VK_SUBOPTIMAL_KHR: formatter.write("VK_SUBOPTIMAL_KHR"); break;
|
|
case VK_ERROR_OUT_OF_DATE_KHR: formatter.write("VK_ERROR_OUT_OF_DATE_KHR"); break;
|
|
case VK_ERROR_INCOMPATIBLE_DISPLAY_KHR: formatter.write("VK_ERROR_INCOMPATIBLE_DISPLAY_KHR"); break;
|
|
case VK_ERROR_VALIDATION_FAILED_EXT: formatter.write("VK_ERROR_VALIDATION_FAILED_EXT"); break;
|
|
case VK_ERROR_INVALID_SHADER_NV: formatter.write("VK_ERROR_INVALID_SHADER_NV"); break;
|
|
case VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR: formatter.write("VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR"); break;
|
|
case VK_ERROR_VIDEO_PICTURE_LAYOUT_NOT_SUPPORTED_KHR: formatter.write("VK_ERROR_VIDEO_PICTURE_LAYOUT_NOT_SUPPORTED_KHR"); break;
|
|
case VK_ERROR_VIDEO_PROFILE_OPERATION_NOT_SUPPORTED_KHR: formatter.write("VK_ERROR_VIDEO_PROFILE_OPERATION_NOT_SUPPORTED_KHR"); break;
|
|
case VK_ERROR_VIDEO_PROFILE_FORMAT_NOT_SUPPORTED_KHR: formatter.write("VK_ERROR_VIDEO_PROFILE_FORMAT_NOT_SUPPORTED_KHR"); break;
|
|
case VK_ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR: formatter.write("VK_ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR"); break;
|
|
case VK_ERROR_VIDEO_STD_VERSION_NOT_SUPPORTED_KHR: formatter.write("VK_ERROR_VIDEO_STD_VERSION_NOT_SUPPORTED_KHR"); break;
|
|
case VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT: formatter.write("VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT"); break;
|
|
case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT: formatter.write("VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT"); break;
|
|
case VK_THREAD_IDLE_KHR: formatter.write("VK_THREAD_IDLE_KHR"); break;
|
|
case VK_THREAD_DONE_KHR: formatter.write("VK_THREAD_DONE_KHR"); break;
|
|
case VK_OPERATION_DEFERRED_KHR: formatter.write("VK_OPERATION_DEFERRED_KHR"); break;
|
|
case VK_OPERATION_NOT_DEFERRED_KHR: formatter.write("VK_OPERATION_NOT_DEFERRED_KHR"); break;
|
|
case VK_ERROR_INVALID_VIDEO_STD_PARAMETERS_KHR: formatter.write("VK_ERROR_INVALID_VIDEO_STD_PARAMETERS_KHR"); break;
|
|
case VK_ERROR_COMPRESSION_EXHAUSTED_EXT: formatter.write("VK_ERROR_COMPRESSION_EXHAUSTED_EXT"); break;
|
|
case VK_INCOMPATIBLE_SHADER_BINARY_EXT: formatter.write("VK_INCOMPATIBLE_SHADER_BINARY_EXT"); break;
|
|
case VK_PIPELINE_BINARY_MISSING_KHR: formatter.write("VK_PIPELINE_BINARY_MISSING_KHR"); break;
|
|
case VK_ERROR_NOT_ENOUGH_SPACE_KHR: formatter.write("VK_ERROR_NOT_ENOUGH_SPACE_KHR"); break;
|
|
default: formatter.write("Unknown Vulkan error"); break;
|
|
}
|
|
}
|
|
|
|
namespace gpu
|
|
{
|
|
|
|
static uint32_t kTargetVersionMajor = 1;
|
|
static uint32_t kTargetVersionMinor = 3;
|
|
static uint32_t kTargetVersion = VK_MAKE_API_VERSION(0, kTargetVersionMajor, kTargetVersionMinor, 0); // NOLINT
|
|
|
|
static asl::status_or<VkInstance> create_instance()
|
|
{
|
|
asl::buffer<const char*> instance_extensions;
|
|
asl::buffer<const char*> layers;
|
|
{
|
|
uint32_t count = 0;
|
|
const char* const* extensions = SDL_Vulkan_GetInstanceExtensions(&count);
|
|
for (uint32_t i = 0; i < count; ++i)
|
|
{
|
|
instance_extensions.push(extensions[i]); // NOLINT(*-pointer-arithmetic)
|
|
}
|
|
}
|
|
|
|
layers.push("VK_LAYER_KHRONOS_validation");
|
|
layers.push("VK_LAYER_LUNARG_monitor");
|
|
|
|
instance_extensions.push(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
|
|
|
auto vkGetInstanceProcAddr = asl::bit_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr());
|
|
auto status = vulkan_loader::load_global(vkGetInstanceProcAddr);
|
|
ASL_TRY(status);
|
|
|
|
uint32_t version{};
|
|
vkEnumerateInstanceVersion(&version);
|
|
|
|
uint32_t vk_major = VK_API_VERSION_MAJOR(version); // NOLINT
|
|
uint32_t vk_minor = VK_API_VERSION_MINOR(version); // NOLINT
|
|
|
|
if (vk_major != kTargetVersionMajor || vk_minor < kTargetVersionMinor)
|
|
{
|
|
return asl::runtime_error("Incompatible Vulkan version: {}.{}", vk_major, vk_minor);
|
|
}
|
|
|
|
VkApplicationInfo app_info{
|
|
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
|
|
.pNext = nullptr,
|
|
.pApplicationName = "HK-21",
|
|
.applicationVersion = 0,
|
|
.pEngineName = "Custom",
|
|
.engineVersion = 0,
|
|
.apiVersion = kTargetVersion,
|
|
};
|
|
|
|
VkInstanceCreateInfo instance_create_info{
|
|
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
|
|
.pNext = nullptr,
|
|
.flags = 0,
|
|
.pApplicationInfo = &app_info,
|
|
.enabledLayerCount = static_cast<uint32_t>(layers.size()),
|
|
.ppEnabledLayerNames = layers.data(),
|
|
.enabledExtensionCount = static_cast<uint32_t>(instance_extensions.size()),
|
|
.ppEnabledExtensionNames = instance_extensions.data(),
|
|
};
|
|
|
|
VkInstance instance{};
|
|
VkResult res = vkCreateInstance(&instance_create_info, VK_ALLOCATOR, &instance);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't create Vulkan instance: {}", res);
|
|
}
|
|
|
|
status = vulkan_loader::load_instance(vkGetInstanceProcAddr, instance);
|
|
ASL_TRY(status);
|
|
|
|
ASL_LOG_INFO("Vulkan instance created");
|
|
|
|
return instance;
|
|
}
|
|
|
|
static asl::status_or<VkSurfaceKHR> create_surface(SDL_Window* window, VkInstance instance)
|
|
{
|
|
VkSurfaceKHR surface{};
|
|
if (!SDL_Vulkan_CreateSurface(window, instance, VK_ALLOCATOR, &surface))
|
|
{
|
|
return asl::runtime_error("Couldn't create Vulkan surface");
|
|
}
|
|
|
|
return surface;
|
|
}
|
|
|
|
struct PhysicalDeviceInfo
|
|
{
|
|
VkPhysicalDevice physical_device;
|
|
uint32_t queue_family_index;
|
|
};
|
|
|
|
static asl::status_or<PhysicalDeviceInfo> find_physical_device(VkInstance instance)
|
|
{
|
|
uint32_t count{};
|
|
asl::buffer<VkPhysicalDevice> physical_devices;
|
|
asl::buffer<VkQueueFamilyProperties> queue_family_prps;
|
|
|
|
vkEnumeratePhysicalDevices(instance, &count, nullptr);
|
|
physical_devices.resize_zero(count);
|
|
VkResult res = vkEnumeratePhysicalDevices(instance, &count, physical_devices.data());
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't enumerate Vulkan physical devices: {}", res);
|
|
}
|
|
|
|
for (auto* physical_device: physical_devices)
|
|
{
|
|
VkPhysicalDeviceProperties prps;
|
|
vkGetPhysicalDeviceProperties(physical_device, &prps);
|
|
|
|
auto name = asl::string_view::from_zstr(prps.deviceName);
|
|
|
|
if (prps.apiVersion < kTargetVersion)
|
|
{
|
|
ASL_LOG_INFO("Device {}: invalid supported Vulkan API version", name);
|
|
continue;
|
|
}
|
|
|
|
VkPhysicalDeviceSynchronization2Features synchronization2_features{};
|
|
synchronization2_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES;
|
|
synchronization2_features.pNext = nullptr;
|
|
|
|
VkPhysicalDeviceFeatures2 features{};
|
|
features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
|
features.pNext = &synchronization2_features;
|
|
|
|
vkGetPhysicalDeviceFeatures2(physical_device, &features);
|
|
|
|
if (synchronization2_features.synchronization2 != VK_TRUE)
|
|
{
|
|
ASL_LOG_INFO("Device {}: synchronization2 not supported", name);
|
|
continue;
|
|
}
|
|
|
|
uint32_t queue_family_count{};
|
|
vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, nullptr);
|
|
queue_family_prps.resize_zero(queue_family_count);
|
|
vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, queue_family_prps.data());
|
|
|
|
static constexpr VkQueueFlags kWantedQueueFlags = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT;
|
|
|
|
for (uint32_t queue_family_index = 0; queue_family_index < queue_family_count; ++queue_family_index)
|
|
{
|
|
if ((queue_family_prps[queue_family_index].queueFlags & kWantedQueueFlags) != kWantedQueueFlags) { continue; }
|
|
if (!SDL_Vulkan_GetPresentationSupport(instance, physical_device, queue_family_index)) { continue; }
|
|
|
|
ASL_LOG_INFO("Chosing device \"{}\" with queue family {}", name, queue_family_index);
|
|
return PhysicalDeviceInfo{ physical_device, queue_family_index };
|
|
}
|
|
|
|
ASL_LOG_INFO("Device {}: no valid queue found", name);
|
|
}
|
|
|
|
return asl::runtime_error("Couldn't find a valid Vulkan physical device");
|
|
}
|
|
|
|
static asl::status_or<VkDevice> create_device(VkPhysicalDevice physical_device, uint32_t queue_family_index)
|
|
{
|
|
asl::buffer<const char*> device_extensions;
|
|
device_extensions.push(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
|
|
|
|
const float queue_priority = 1.0F;
|
|
|
|
VkDeviceQueueCreateInfo queue_create_info{
|
|
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
|
|
.pNext = nullptr,
|
|
.flags = 0,
|
|
.queueFamilyIndex = queue_family_index,
|
|
.queueCount = 1,
|
|
.pQueuePriorities = &queue_priority,
|
|
};
|
|
|
|
VkPhysicalDeviceSynchronization2Features synchronization2_features{};
|
|
synchronization2_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES;
|
|
synchronization2_features.pNext = nullptr;
|
|
synchronization2_features.synchronization2 = VK_TRUE;
|
|
|
|
VkDeviceCreateInfo create_info{
|
|
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
|
|
.pNext = &synchronization2_features,
|
|
.flags = 0,
|
|
.queueCreateInfoCount = 1,
|
|
.pQueueCreateInfos = &queue_create_info,
|
|
.enabledLayerCount = 0,
|
|
.ppEnabledLayerNames = nullptr,
|
|
.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size()),
|
|
.ppEnabledExtensionNames = device_extensions.data(),
|
|
.pEnabledFeatures = nullptr,
|
|
};
|
|
|
|
VkDevice device{};
|
|
VkResult res = vkCreateDevice(physical_device, &create_info, VK_ALLOCATOR, &device);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't create Vulkan device: {}", res);
|
|
}
|
|
|
|
ASL_LOG_INFO("Vulkan device created");
|
|
|
|
auto status = vulkan_loader::load_device(device);
|
|
ASL_TRY(status);
|
|
|
|
return device;
|
|
}
|
|
|
|
struct FrameResources : asl::intrusive_list_node<FrameResources>
|
|
{
|
|
VkFence complete_fence = VK_NULL_HANDLE;
|
|
VkCommandPool command_pool = VK_NULL_HANDLE;
|
|
};
|
|
|
|
class DependencyInfoBuilder : public vulkan_sync::DependencyInfoBuilder
|
|
{
|
|
// @Todo Configure allocator
|
|
|
|
asl::buffer<VkImageMemoryBarrier2> m_image_barriers;
|
|
asl::buffer<VkBufferMemoryBarrier2> m_buffer_barriers;
|
|
|
|
public:
|
|
void add_image_barrier(const VkImageMemoryBarrier2& barrier) override
|
|
{
|
|
m_image_barriers.push(barrier);
|
|
}
|
|
|
|
void add_buffer_barrier(const VkBufferMemoryBarrier2& barrier) override
|
|
{
|
|
m_buffer_barriers.push(barrier);
|
|
}
|
|
|
|
void apply(VkCommandBuffer command_buffer)
|
|
{
|
|
if (m_image_barriers.is_empty() &&
|
|
m_buffer_barriers.is_empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
VkDependencyInfo dependency_info{
|
|
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
|
|
.pNext = nullptr,
|
|
.dependencyFlags = 0,
|
|
.memoryBarrierCount = 0,
|
|
.pMemoryBarriers = nullptr,
|
|
.bufferMemoryBarrierCount = static_cast<uint32_t>(m_buffer_barriers.size()),
|
|
.pBufferMemoryBarriers = m_buffer_barriers.data(),
|
|
.imageMemoryBarrierCount = static_cast<uint32_t>(m_image_barriers.size()),
|
|
.pImageMemoryBarriers = m_image_barriers.data(),
|
|
};
|
|
|
|
vkCmdPipelineBarrier2(command_buffer, &dependency_info);
|
|
|
|
m_image_barriers.clear();
|
|
m_buffer_barriers.clear();
|
|
}
|
|
};
|
|
|
|
class GpuImpl : public Gpu
|
|
{
|
|
bool m_destroyed{};
|
|
|
|
VkInstance m_instance;
|
|
VkPhysicalDevice m_physical_device;
|
|
VkSurfaceKHR m_surface;
|
|
uint32_t m_queue_family_index;
|
|
VkDevice m_device;
|
|
VkQueue m_queue;
|
|
|
|
struct Image
|
|
{
|
|
vulkan_sync::ImageState state;
|
|
VkImage image{};
|
|
};
|
|
|
|
asl::option<VkSwapchainKHR> m_swapchain;
|
|
asl::buffer<Image> m_swapchain_images;
|
|
|
|
VkSemaphore m_swapchain_image_acquire_semaphore = VK_NULL_HANDLE;
|
|
VkSemaphore m_queue_complete_semaphore = VK_NULL_HANDLE;
|
|
|
|
asl::GlobalHeap m_allocator; // @Todo Make this configurable
|
|
asl::IntrusiveList<FrameResources> m_in_flight_frames;
|
|
|
|
DependencyInfoBuilder m_dependency_builder;
|
|
|
|
public:
|
|
GpuImpl(
|
|
VkInstance instance,
|
|
VkPhysicalDevice physical_device,
|
|
VkSurfaceKHR surface,
|
|
uint32_t queue_family_index,
|
|
VkDevice device,
|
|
VkQueue queue)
|
|
: m_instance{instance}
|
|
, m_physical_device{physical_device}
|
|
, m_surface{surface}
|
|
, m_queue_family_index{queue_family_index}
|
|
, m_device{device}
|
|
, m_queue{queue}
|
|
, m_swapchain_image_acquire_semaphore{create_semaphore()}
|
|
, m_queue_complete_semaphore{create_semaphore()}
|
|
{
|
|
}
|
|
|
|
ASL_DELETE_COPY_MOVE(GpuImpl);
|
|
|
|
~GpuImpl() override
|
|
{
|
|
ASL_ASSERT(m_destroyed);
|
|
}
|
|
|
|
void destroy() override
|
|
{
|
|
ASL_ASSERT(!m_destroyed);
|
|
m_destroyed = true;
|
|
|
|
vkDeviceWaitIdle(m_device);
|
|
|
|
recycle_resources();
|
|
|
|
destroy_semaphore(m_swapchain_image_acquire_semaphore);
|
|
destroy_semaphore(m_queue_complete_semaphore);
|
|
|
|
if (m_swapchain.has_value())
|
|
{
|
|
vkDestroySwapchainKHR(m_device, m_swapchain.value(), VK_ALLOCATOR);
|
|
}
|
|
|
|
vkDestroyDevice(m_device, VK_ALLOCATOR);
|
|
vkDestroySurfaceKHR(m_instance, m_surface, VK_ALLOCATOR);
|
|
vkDestroyInstance(m_instance, VK_ALLOCATOR);
|
|
}
|
|
|
|
asl::status create_swapchain(uint32_t width, uint32_t height)
|
|
{
|
|
uint32_t count{};
|
|
VkSurfaceCapabilitiesKHR caps{};
|
|
VkResult res = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physical_device, m_surface, &caps);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't retrieve Vulkan surface capabitilies: {}", res);
|
|
}
|
|
|
|
asl::buffer<VkSurfaceFormatKHR> formats;
|
|
vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical_device, m_surface, &count, nullptr);
|
|
formats.resize_zero(count);
|
|
res = vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical_device, m_surface, &count, formats.data());
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't retrieve Vulkan surface formats: {}", res);
|
|
}
|
|
|
|
asl::buffer<VkPresentModeKHR> present_modes;
|
|
vkGetPhysicalDeviceSurfacePresentModesKHR(m_physical_device, m_surface, &count, nullptr);
|
|
present_modes.resize_zero(count);
|
|
res = vkGetPhysicalDeviceSurfacePresentModesKHR(m_physical_device, m_surface, &count, present_modes.data());
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't retrieve Vulkan surface present modes: {}", res);
|
|
}
|
|
|
|
asl::option<VkSurfaceFormatKHR> format;
|
|
for (const auto& f: formats)
|
|
{
|
|
if (f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR &&
|
|
(f.format == VK_FORMAT_B8G8R8_UNORM ||
|
|
f.format == VK_FORMAT_B8G8R8A8_UNORM ||
|
|
f.format == VK_FORMAT_R8G8B8_UNORM ||
|
|
f.format == VK_FORMAT_R8G8B8A8_UNORM))
|
|
{
|
|
format = f;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!format.has_value())
|
|
{
|
|
return asl::runtime_error("Couldn't find suitable Vulkan surface format");
|
|
}
|
|
|
|
VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR;
|
|
for (const auto& p: present_modes)
|
|
{
|
|
if (p == VK_PRESENT_MODE_MAILBOX_KHR)
|
|
{
|
|
present_mode = p;
|
|
break;
|
|
}
|
|
}
|
|
|
|
VkSwapchainCreateInfoKHR create_info{
|
|
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
|
|
.pNext = nullptr,
|
|
.flags = 0,
|
|
.surface = m_surface,
|
|
.minImageCount = asl::min(asl::max(2U, caps.minImageCount), caps.maxImageCount),
|
|
.imageFormat = format.value().format,
|
|
.imageColorSpace = format.value().colorSpace,
|
|
.imageExtent = {
|
|
.width = asl::min(asl::max(width, caps.minImageExtent.width), caps.maxImageExtent.width),
|
|
.height = asl::min(asl::max(height, caps.minImageExtent.height), caps.maxImageExtent.height),
|
|
},
|
|
.imageArrayLayers = 1,
|
|
.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
|
.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
|
.queueFamilyIndexCount = 1,
|
|
.pQueueFamilyIndices = &m_queue_family_index,
|
|
.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
|
|
.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
|
|
.presentMode = present_mode,
|
|
.clipped = VK_TRUE,
|
|
.oldSwapchain = VK_NULL_HANDLE,
|
|
};
|
|
|
|
VkSwapchainKHR swapchain{};
|
|
res = vkCreateSwapchainKHR(m_device, &create_info, VK_ALLOCATOR, &swapchain);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't create Vulkan swapchain: {}", res);
|
|
}
|
|
|
|
m_swapchain = swapchain;
|
|
|
|
vkGetSwapchainImagesKHR(m_device, m_swapchain.value(), &count, nullptr);
|
|
|
|
{
|
|
m_swapchain_images.resize(count);
|
|
|
|
// @Todo Good candidate for temporary allocation
|
|
asl::buffer<VkImage> images;
|
|
images.resize_zero(count);
|
|
|
|
res = vkGetSwapchainImagesKHR(m_device, m_swapchain.value(), &count, images.data());
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't retrieve Vulkan swapchain images: {}", res);
|
|
}
|
|
|
|
for (int64_t i = 0; i < count; ++i)
|
|
{
|
|
m_swapchain_images[i].image = images[i];
|
|
}
|
|
}
|
|
|
|
ASL_LOG_INFO("Vulkan swapchain created ({}x{} with {} images)",
|
|
create_info.imageExtent.width,
|
|
create_info.imageExtent.height,
|
|
m_swapchain_images.size());
|
|
|
|
return asl::ok();
|
|
}
|
|
|
|
VkSemaphore create_semaphore()
|
|
{
|
|
static constexpr VkSemaphoreCreateInfo semaphore_create_info{
|
|
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
|
|
.pNext = nullptr,
|
|
.flags = 0,
|
|
};
|
|
|
|
VkSemaphore semaphore{};
|
|
vkCreateSemaphore(m_device, &semaphore_create_info, VK_ALLOCATOR, &semaphore);
|
|
return semaphore;
|
|
}
|
|
|
|
void destroy_semaphore(VkSemaphore semaphore)
|
|
{
|
|
vkDestroySemaphore(m_device, semaphore, VK_ALLOCATOR);
|
|
}
|
|
|
|
VkFence create_fence()
|
|
{
|
|
static constexpr VkFenceCreateInfo fence_create_info{
|
|
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
|
|
.pNext = nullptr,
|
|
.flags = 0,
|
|
};
|
|
|
|
VkFence fence{};
|
|
vkCreateFence(m_device, &fence_create_info, VK_ALLOCATOR, &fence);
|
|
return fence;
|
|
}
|
|
|
|
void destroy_fence(VkFence fence)
|
|
{
|
|
vkDestroyFence(m_device, fence, VK_ALLOCATOR);
|
|
}
|
|
|
|
asl::status frame_opt()
|
|
{
|
|
uint32_t image_index{};
|
|
|
|
VkResult res = vkAcquireNextImageKHR(
|
|
m_device,
|
|
m_swapchain.value(),
|
|
0xffff'ffff'ffff'ffffLLU,
|
|
m_swapchain_image_acquire_semaphore,
|
|
VK_NULL_HANDLE,
|
|
&image_index);
|
|
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't acquire swapchain image: {}", res);
|
|
}
|
|
|
|
auto& swapchain_image = m_swapchain_images[image_index];
|
|
|
|
VkCommandPoolCreateInfo command_pool_create_info{
|
|
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
|
|
.pNext = nullptr,
|
|
.flags = 0,
|
|
.queueFamilyIndex = m_queue_family_index,
|
|
};
|
|
|
|
VkCommandPool command_pool{};
|
|
res = vkCreateCommandPool(m_device, &command_pool_create_info, VK_ALLOCATOR, &command_pool);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't create command pool: {}", res);
|
|
}
|
|
|
|
VkCommandBufferAllocateInfo alloc_info{
|
|
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
|
|
.pNext = nullptr,
|
|
.commandPool = command_pool,
|
|
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
|
|
.commandBufferCount = 1,
|
|
};
|
|
|
|
VkCommandBuffer command_buffer{};
|
|
res = vkAllocateCommandBuffers(m_device, &alloc_info, &command_buffer);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't allocate command buffer: {}", res);
|
|
}
|
|
|
|
VkCommandBufferBeginInfo begin_info{
|
|
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
|
.pNext = nullptr,
|
|
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
|
|
.pInheritanceInfo = nullptr,
|
|
};
|
|
|
|
res = vkBeginCommandBuffer(command_buffer, &begin_info);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't begin command buffer: {}", res);
|
|
}
|
|
|
|
vulkan_sync::synchronize_resource(
|
|
swapchain_image.image,
|
|
VK_IMAGE_ASPECT_COLOR_BIT,
|
|
&swapchain_image.state,
|
|
vulkan_sync::Usage::kImageClear,
|
|
&m_dependency_builder);
|
|
|
|
VkClearColorValue clear_color{
|
|
.float32 = { 0.0F, 0.137F, 0.4F, 1.0F },
|
|
};
|
|
|
|
VkImageSubresourceRange range{
|
|
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
|
.baseMipLevel = 0,
|
|
.levelCount = VK_REMAINING_MIP_LEVELS,
|
|
.baseArrayLayer = 0,
|
|
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
|
};
|
|
|
|
m_dependency_builder.apply(command_buffer);
|
|
vkCmdClearColorImage(command_buffer, swapchain_image.image, swapchain_image.state.current_layout, &clear_color, 1, &range);
|
|
|
|
vulkan_sync::synchronize_resource(
|
|
swapchain_image.image,
|
|
VK_IMAGE_ASPECT_COLOR_BIT,
|
|
&swapchain_image.state,
|
|
vulkan_sync::Usage::kImagePresent,
|
|
&m_dependency_builder);
|
|
|
|
m_dependency_builder.apply(command_buffer);
|
|
|
|
|
|
res = vkEndCommandBuffer(command_buffer);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't end command buffer: {}", res);
|
|
}
|
|
|
|
VkFence fence = create_fence();
|
|
|
|
VkSemaphoreSubmitInfo semaphore_wait_submit_info{
|
|
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO,
|
|
.pNext = nullptr,
|
|
.semaphore = m_swapchain_image_acquire_semaphore,
|
|
.value = 0,
|
|
.stageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT,
|
|
.deviceIndex = 0,
|
|
};
|
|
|
|
VkSemaphoreSubmitInfo semaphore_signal_submit_info{
|
|
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO,
|
|
.pNext = nullptr,
|
|
.semaphore = m_queue_complete_semaphore,
|
|
.value = 0,
|
|
.stageMask = VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT,
|
|
.deviceIndex = 0,
|
|
};
|
|
|
|
VkCommandBufferSubmitInfo command_buffer_submit_info{
|
|
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO,
|
|
.pNext = nullptr,
|
|
.commandBuffer = command_buffer,
|
|
.deviceMask = 0,
|
|
};
|
|
|
|
VkSubmitInfo2 submit_info{
|
|
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2,
|
|
.pNext = nullptr,
|
|
.flags = 0,
|
|
.waitSemaphoreInfoCount = 1,
|
|
.pWaitSemaphoreInfos = &semaphore_wait_submit_info,
|
|
.commandBufferInfoCount = 1,
|
|
.pCommandBufferInfos = &command_buffer_submit_info,
|
|
.signalSemaphoreInfoCount = 1,
|
|
.pSignalSemaphoreInfos = &semaphore_signal_submit_info,
|
|
};
|
|
|
|
res = vkQueueSubmit2(m_queue, 1, &submit_info, fence);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't submit queue: {}", res);
|
|
}
|
|
|
|
VkPresentInfoKHR present_info{
|
|
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
|
|
.pNext = nullptr,
|
|
.waitSemaphoreCount = 1,
|
|
.pWaitSemaphores = &m_queue_complete_semaphore,
|
|
.swapchainCount = 1,
|
|
.pSwapchains = &m_swapchain.value(),
|
|
.pImageIndices = &image_index,
|
|
.pResults = nullptr,
|
|
};
|
|
|
|
res = vkQueuePresentKHR(m_queue, &present_info);
|
|
if (res != VK_SUCCESS)
|
|
{
|
|
return asl::runtime_error("Couldn't present queue: {}", res);
|
|
}
|
|
|
|
auto* frame_resources = asl::alloc_new<FrameResources>(m_allocator);
|
|
frame_resources->command_pool = command_pool;
|
|
frame_resources->complete_fence = fence;
|
|
|
|
m_in_flight_frames.push_front(frame_resources);
|
|
|
|
return asl::ok();
|
|
}
|
|
|
|
void recycle_resources()
|
|
{
|
|
while (!m_in_flight_frames.is_empty())
|
|
{
|
|
auto* frame = m_in_flight_frames.back();
|
|
auto status = vkGetFenceStatus(m_device, frame->complete_fence);
|
|
|
|
if (status == VK_NOT_READY)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (status != VK_SUCCESS)
|
|
{
|
|
ASL_LOG_ERROR("Error on frame fence query: {}", status);
|
|
return;
|
|
}
|
|
|
|
destroy_fence(frame->complete_fence);
|
|
vkDestroyCommandPool(m_device, frame->command_pool, VK_ALLOCATOR);
|
|
|
|
m_in_flight_frames.pop_back();
|
|
asl::alloc_delete(m_allocator, frame);
|
|
}
|
|
}
|
|
|
|
void frame() override
|
|
{
|
|
recycle_resources();
|
|
|
|
auto s = frame_opt();
|
|
if (!s.ok())
|
|
{
|
|
ASL_LOG_ERROR("Frame error: {}", s);
|
|
}
|
|
}
|
|
};
|
|
|
|
asl::status_or<asl::box<Gpu>> init(SDL_Window* window)
|
|
{
|
|
auto instance = create_instance();
|
|
ASL_TRY(instance);
|
|
|
|
auto surface = create_surface(window, instance.value());
|
|
ASL_TRY(surface);
|
|
|
|
auto physical_device_info = find_physical_device(instance.value());
|
|
ASL_TRY(physical_device_info);
|
|
|
|
auto device = create_device(
|
|
physical_device_info.value().physical_device,
|
|
physical_device_info.value().queue_family_index);
|
|
ASL_TRY(device);
|
|
|
|
VkQueue queue{};
|
|
vkGetDeviceQueue(device.value(), physical_device_info.value().queue_family_index, 0, &queue);
|
|
|
|
auto gpu = asl::make_box<GpuImpl>(
|
|
instance.value(),
|
|
physical_device_info.value().physical_device,
|
|
surface.value(),
|
|
physical_device_info.value().queue_family_index,
|
|
device.value(),
|
|
queue);
|
|
|
|
int window_width{};
|
|
int window_height{};
|
|
SDL_GetWindowSizeInPixels(window, &window_width, &window_height);
|
|
auto s = gpu->create_swapchain(static_cast<uint32_t>(window_width), static_cast<uint32_t>(window_height));
|
|
ASL_TRY(s);
|
|
|
|
return gpu;
|
|
};
|
|
|
|
} // namespace gpu
|
|
|