6 Commits

Author SHA1 Message Date
fabce2805d create imgae views
Some checks failed
Build and Test verification / build (push) Failing after 27s
Build and Test verification / test (push) Has been skipped
2026-05-25 16:01:52 -05:00
4e9d69ba49 create swapchain 2026-05-24 23:00:39 -05:00
8b798fcaa7 fix surface creation on linux 2026-05-23 23:29:57 -05:00
65d21cd3c4 basic create surface
Some checks failed
Build and Test verification / build (push) Failing after 25s
Build and Test verification / test (push) Has been skipped
2026-05-23 18:29:44 -05:00
Preston McGee
bcd19f1e60 Feature: VkDevice Creation (#5)
* update contributing with goals

* add required extensions to vulkan instance

* add validation layers + debug callback

* add device class which enumerates availble gpu devices

* improve device selection logic

* add logical device creation
2026-05-22 21:48:51 -05:00
Preston McGee
a63c271f92 Feature/VkExtensions+VkLayers (#2) 2026-05-16 12:48:39 -07:00
12 changed files with 647 additions and 40 deletions

View File

@@ -33,6 +33,9 @@ add_library(maiden_core STATIC
src/App.cpp
src/Window.cpp
src/Engine.cpp
src/Device.cpp
src/Pipeline.cpp
src/Swapchain.cpp
# include extra source files here
)

View File

@@ -10,10 +10,13 @@ The maiden project is a GPU accelerated 3D rendering engine built with C++ based
### Clone Repository
```bash
$ git clone https://git.vxbard.net/homeburger/maiden.git
# ssh recommended for contribution
$ git clone git@github.com:Blitblank/maiden.git
# http if you don't like ssh:
$ git clone https://github.com/Blitblank/maiden.git
# If there's any necessary submodules then:
$ git clone --recurse-submodules https://git.vxbard.net/homeburger/maiden.git
$ git clone --recurse-submodules git@github.com:Blitblank/maiden.git
# If you have already cloned the repository and you need its submodules:
$ git submodule update --init --recursive
@@ -74,6 +77,24 @@ $ gcovr -r .. --filter "../src"
Basically these are some tricky situations that I encountered when trying to execute this app throughout this development phase. If you are running on WSL Ubuntu 26.04 like me, then you mightr run into these too, hopefully my steps help fix.
note: I am running an x86_64 system with an Nvidia GPU so some things may be slightly different if your system doesn't match.
### [WARN: COPY MODE]
This seems like a WSL specific error and causes real issues with relaying graphics from linux to windows. I fixed this by installing new mesa drivers as reccommended by https://github.com/microsoft/wslg/discussions/312:
```bash
$ sudo add-apt-repository ppa:kisak/kisak-mesa
$ sudo apt-get update && sudo apt upgrade
```
Note: this resulted in the following erre "WARNING: dzn is not a conformant Vulkan implementation, testing use only." Running `$ vkcube` showed that this indeed was just a warning.
(for those curious, dzn is a compaitibility layer between DirectX12 and Vulkan for that WSL conformity)
### Could not locate a Nvidia GPU
```bash
$ sudo add-apt-repository ppa:kisak/turtle
$ sudo apt update
$ sudo apt upgrade
```
verify with `$ vulkaninfo --summary` to ensure your GPU is shown.
## Development Roadmap
### lots of todo here

199
src/Device.cpp Normal file
View File

@@ -0,0 +1,199 @@
#include "Device.hpp"
#include <iostream>
Device::Device(vk::raii::Instance* instance, Window* window): instance_(instance), window_(window) {
selectPhysicalDevice();
createSurface();
createLogicalDevice();
}
Device::~Device() {
}
bool Device::selectPhysicalDevice() {
std::vector<vk::raii::PhysicalDevice> physicalDevices = instance_->enumeratePhysicalDevices();
if(physicalDevices.empty()) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: no physical devices with Vulkan support found." << std::endl;
return false;
}
// validate found devices
uint32_t maxScore = 0;
for(vk::raii::PhysicalDevice& physicalDevice : physicalDevices) {
uint32_t capabilityScore = evaluatePhysicalDevice(physicalDevice);
if(capabilityScore > maxScore) {
maxScore = capabilityScore;
physicalDevice_ = physicalDevice;
}
}
if(maxScore = 0) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: physical devices found, but none capable for this engine." << std::endl;
return false;
} else {
vk::PhysicalDeviceProperties deviceProperties = physicalDevice_.getProperties();
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Physical device selected: " << deviceProperties.deviceName << std::endl;
return true;
}
}
uint32_t Device::evaluatePhysicalDevice(vk::raii::PhysicalDevice& device) {
vk::PhysicalDeviceProperties deviceProperties = device.getProperties();
vk::PhysicalDeviceFeatures deviceFeatures = device.getFeatures();
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Physical device found: " << deviceProperties.deviceName << std::endl;
uint32_t score = 0;
// TODO: this is very basic and can be improved
// prefer discrete graphics to integrated graphics
if(deviceProperties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) {
score += 2;
} else {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Warning: physical device " << deviceProperties.deviceName << " is not a discrete device!" << std::endl;
}
// prefer devices that support vulkan 1.3
if(deviceProperties.apiVersion >= vk::ApiVersion14) {
score++;
} else {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Warning: physical device " << deviceProperties.deviceName << " does not support Vulkan 1.3! (" << std::endl;
}
// prefer devices that support graphics queues
auto queueFamilies = device.getQueueFamilyProperties();
if(std::ranges::any_of( queueFamilies, []( auto const & qfp ) { return !!( qfp.queueFlags & vk::QueueFlagBits::eGraphics ); } )) {
score++;
} else {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Warning: physical device " << deviceProperties.deviceName << " does not support graphics queue families!" << std::endl;
}
// prefer devices that support all required extensions
auto availableDeviceExtensions = device.enumerateDeviceExtensionProperties();
bool found = false;
uint32_t missingExtensions = 0;
for(auto& requiredExtension : requiredDeviceExtensions_) {
for(auto& availableExtension: availableDeviceExtensions) {
if(strcmp(availableExtension.extensionName, requiredExtension) == 0) {
found = true;
continue;
}
}
if(found == false) {
missingExtensions++;
}
}
if(missingExtensions == 0) {
score++;
} else {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Warning: physical device " << deviceProperties.deviceName << " is missing extensions!" << std::endl;
}
// prefer devices that support all required features
auto features = device.template getFeatures2<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features, vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>();
if(features.template get<vk::PhysicalDeviceVulkan13Features>().dynamicRendering && features.template get<vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>().extendedDynamicState) {
score++;
} else {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Warning: physical device " << deviceProperties.deviceName << " is missing features!" << std::endl;
}
return score;
}
bool Device::createLogicalDevice() {
if(surface_ == nullptr) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: cannot create logical device without a valid presentation surface." << std::endl;
return false;
}
if(physicalDevice_ == nullptr) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: cannot create logical device without a valid physical device." << std::endl;
return false;
}
// specify queue family requirements
std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice_.getQueueFamilyProperties();
int32_t queueIndex = -1;
for(uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++) {
if((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) && physicalDevice_.getSurfaceSupportKHR(qfpIndex, *surface_)) {
queueIndex = static_cast<int32_t>(qfpIndex);
break;
}
}
if(queueIndex <= -1) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: could not locate valid graphics queues." << std::endl;
return false;
}
float queuePriority = 0.5f;
vk::DeviceQueueCreateInfo deviceQueueCreateInfo {
.queueFamilyIndex = static_cast<uint32_t>(queueIndex),
.queueCount = 1,
.pQueuePriorities = &queuePriority
};
// specify device feature requirements
vk::PhysicalDeviceVulkan13Features deviceFeatures = { .dynamicRendering = true };
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT deviceStateFeatures = { . extendedDynamicState = true };
vk::StructureChain<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features, vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT> featureChain = {
{}, // empty for now
deviceFeatures,
deviceStateFeatures
};
// create logical device
vk::DeviceCreateInfo deviceCreateInfo {
.pNext = &featureChain.get<vk::PhysicalDeviceFeatures2>(),
.queueCreateInfoCount = 1,
.pQueueCreateInfos = &deviceQueueCreateInfo,
.enabledExtensionCount = static_cast<uint32_t>(requiredDeviceExtensions_.size()),
.ppEnabledExtensionNames = requiredDeviceExtensions_.data()
};
logicalDevice_ = vk::raii::Device(physicalDevice_, deviceCreateInfo);
// initialize the graphics queue
graphicsQueue_ = vk::raii::Queue(logicalDevice_, queueIndex, 0);
if(logicalDevice_ != nullptr) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Info: Created logcal device" << std::endl;
return true;
} else {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: could not create a valid logical device." << std::endl;
return false;
}
}
void Device::createSurface() {
(void)window_->createSurface(instance_, &surface_);
if(surface_ == nullptr) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error creating surface!" << std::endl;
return;
}
if(physicalDevice_ == nullptr) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: cannot create surface without a physical device!" << std::endl;
return;
}
auto surfaceCapabilities = physicalDevice_.getSurfaceCapabilitiesKHR(*surface_);
std::vector<vk::SurfaceFormatKHR> availableFormats = physicalDevice_.getSurfaceFormatsKHR(*surface_);
std::vector<vk::PresentModeKHR> availablePresentModes = physicalDevice_.getSurfacePresentModesKHR(*surface_);
}
bool Device::getExtent(int32_t* width, int32_t* height) {
return window_->getExtent(width, height);
}

49
src/Device.hpp Normal file
View File

@@ -0,0 +1,49 @@
#pragma once
#include <vulkan/vulkan_raii.hpp>
#include "Window.hpp"
class Device {
public:
Device(vk::raii::Instance* instance, Window* window);
~Device();
// helper to get the surface extent from the window
bool getExtent(int32_t* width, int32_t* height);
vk::raii::SurfaceKHR* getSurface() { return &surface_; }
vk::raii::PhysicalDevice physicalDevice() { return physicalDevice_; }
vk::raii::Device* logicalDevice() { return &logicalDevice_; }
private:
// gives a device a score to attempt to select the most capable device
uint32_t evaluatePhysicalDevice(vk::raii::PhysicalDevice& device);
// creates the surface
void createSurface();
// assigns a capable gpu vkdevice to physicalDevice
bool selectPhysicalDevice();
// initializes the logical device
bool createLogicalDevice();
// vulkan objects
vk::raii::Instance* instance_ = nullptr;
vk::raii::PhysicalDevice physicalDevice_ = nullptr;
vk::raii::Device logicalDevice_ = nullptr;
vk::raii::Queue graphicsQueue_ = nullptr;
vk::raii::SurfaceKHR surface_ = nullptr;
// ptrs to other engine objects
Window* window_ = nullptr;
// required extensions for the physical device
std::vector<const char*> requiredDeviceExtensions_ = { vk::KHRSwapchainExtensionName };
};

View File

@@ -3,6 +3,9 @@
#include <iostream>
#include "Device.hpp"
#include "Swapchain.hpp"
Engine::Engine(Window* window): window_(window) {
// cleans up this constructor
@@ -19,12 +22,12 @@ void Engine::init() {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error creating Vulkan instance." << std::endl;
}
// next steps:
// device selection and setup
// queue creation
// vulkan memory allocator
// create vulkan surface
// attach surface to window
Device device(&instance_, window_);
// render pipeline
Swapchain swapchain(&device);
// Pipeline pipeline(&device, &swapchain);
}
@@ -45,24 +48,7 @@ bool Engine::createInstance() {
.apiVersion = VK_API_VERSION_1_4 // this one is most important
};
// get necessary extensions that our windowing library requires
uint32_t instanceExtensionsCount = 0;
char const* const* instanceExtensions{ SDL_Vulkan_GetInstanceExtensions(&instanceExtensionsCount) };
// get required validation layers as specified by our app
std::vector<const char*> requiredValidationLayers;
if(enableValidationLayers) requiredValidationLayers.assign(validationLayers.begin(), validationLayers.end());
auto validationLayerProperties = context_.enumerateInstanceLayerProperties();
auto unsupportedLayer = std::ranges::find_if(requiredValidationLayers, [&validationLayerProperties](const auto& requiredLayer) {
return std::ranges::none_of(validationLayerProperties, [requiredLayer](const auto& layerProperty) {
return strcmp(layerProperty.layerName, requiredLayer) == 0;
});
}); // TODO: what black magic even is this
if(unsupportedLayer != requiredValidationLayers.end()) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Required validation layer not supported: " << *unsupportedLayer << std::endl;
errorCount++;
}
std::vector<const char*> requiredInstanceExtensions = getRequiredInstanceExtensions();
// get all available extensions
auto extensionProperties = context_.enumerateInstanceExtensionProperties();
@@ -71,35 +57,129 @@ bool Engine::createInstance() {
std::cout << "Available Vulkan Extensions: " << std::endl;
for(const auto& extensionProperty : extensionProperties) {
std::cout << "\t" << extensionProperty.extensionName << std::endl;
}
} // this would be a logger.debug(...)
for(uint32_t i = 0; i < instanceExtensionsCount; i++) { // for each extension that we require
// check that all required extensions are available
for(uint32_t i = 0; i < requiredInstanceExtensions.size(); i++) { // for each extension that we require
bool found = false;
for(const auto& extensionProperty : extensionProperties) { // see if it matches any extensions that are provided
if(strcmp(extensionProperty.extensionName, instanceExtensions[i]) == 0) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Required SDL3 extension not supported: " << instanceExtensions[i] << std::endl;
} else {
if(strcmp(extensionProperty.extensionName, requiredInstanceExtensions[i]) == 0) {
found = true;
break;
}
}
if(!found) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Required SDL3 extension not supported: " << instanceExtensions[i] << std::endl;
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Required SDL3 extension not supported: " << requiredInstanceExtensions[i] << std::endl;
errorCount++;
} else {
// in case you're curious
//std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] SDL3 extension located: " << instanceExtensions[i] << std::endl;
//std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] SDL3 extension located: " << requiredInstanceExtensions[i] << std::endl;
}
}
// get required validation layers as specified by our app
std::vector<const char*> requiredValidationLayers;
if(enableValidationLayers) requiredValidationLayers.assign(validationLayers.begin(), validationLayers.end());
// get available validation layers
auto validationLayerProperties = context_.enumerateInstanceLayerProperties();
// again print if we feel like it
std::cout << "Available Vulkan Validation Layers: " << std::endl;
for(const auto& validationLayer : validationLayerProperties) {
std::cout << "\t" << validationLayer.layerName << std::endl;
}
// check that all required validation layers are avilable
for(int i = 0; i < requiredValidationLayers.size(); i++) {
bool found = false;
for(const auto& validationLayer : validationLayerProperties) {
if(strcmp(requiredValidationLayers[i], validationLayer.layerName) == 0) {
found = true;
break;
}
}
if(!found) {
errorCount++;
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Required validation layer not supported: " << requiredValidationLayers[i] << std::endl;
} else { // in case you're curious
//std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] VkValidation layer located: " << requiredValidationLayers[i] << std::endl;
}
}
// if any we had errors then we must exit
if(errorCount != 0) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Unable to create Vulkan instance. Error count: " << errorCount << std::endl;
return false;
}
} // if any weren't then we must exit
if(errorCount != 0) return false;
vk::InstanceCreateInfo instanceCreateInfo {
.pApplicationInfo = &appInfo,
.enabledExtensionCount = instanceExtensionsCount,
.ppEnabledExtensionNames = instanceExtensions,
.enabledLayerCount = static_cast<uint32_t>(requiredValidationLayers.size()),
.ppEnabledLayerNames = requiredValidationLayers.data(),
.enabledExtensionCount = static_cast<uint32_t>(requiredInstanceExtensions.size()),
.ppEnabledExtensionNames = requiredInstanceExtensions.data()
};
instance_ = vk::raii::Instance(context_, instanceCreateInfo);
return (instance_ != nullptr);
}
std::vector<const char*> Engine::getRequiredInstanceExtensions() {
// get extensions that our windowing library requires
uint32_t sdlExtensionsCount = 0;
const char* const* sdlExtensions{ SDL_Vulkan_GetInstanceExtensions(&sdlExtensionsCount) }; // TODO: get this from window so all sdl3 is encapsulated there
// what in the world is this kind of pointer btw
std::vector<const char*> requiredExtensions(sdlExtensions, sdlExtensions + static_cast<size_t>(sdlExtensionsCount));
// manually add an extension for handling validation layers
if(enableValidationLayers) {
requiredExtensions.push_back(vk::EXTDebugUtilsExtensionName);
}
return requiredExtensions;
}
bool Engine::initDebugMessenger() {
if(!enableValidationLayers) return false;
// masks for which debug messages we want to see
vk::DebugUtilsMessageSeverityFlagsEXT severityFlags(vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError);
vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation);
vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{.messageSeverity = severityFlags,
.messageType = messageTypeFlags,
.pfnUserCallback = &debugCallback};
debugMessenger_ = instance_.createDebugUtilsMessengerEXT( debugUtilsMessengerCreateInfoEXT );
// we could get rid of this and just pass all the control to the logger
// like: "treat all messages of severity vk::eVerbose as our own DebugVerbosity::Info"
return (debugMessenger_ != nullptr);
}
VKAPI_ATTR vk::Bool32 VKAPI_CALL Engine::debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity,
vk::DebugUtilsMessageTypeFlagsEXT type,
const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* pUserData) {
// this will eventually go through our logger
std::cout << "[ Validation Layer ] [Type: " << to_string(type) << "] " << pCallbackData->pMessage << std::endl;
/*
vk severity types:
vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose
vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo
vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning
vk::DebugUtilsMessageSeverityFlagBitsEXT::eError
vk message types:
vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral
vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation
vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance
*/
// returns whether or not we should abort, we'll always say no
return vk::False;
}

View File

@@ -13,6 +13,7 @@ public:
Engine(Window* window);
~Engine() = default;
// initializes and sets up the vulkan instance. outside of constructor to allow control of order the order of initialization
void init();
// draw is called every render iteration in that while loop
@@ -20,14 +21,29 @@ public:
private:
Window* window_;
// returns a list of the required extensions needed by our engine
std::vector<const char*> getRequiredInstanceExtensions();
// helper for organizing the creation of the vulkan instance. returns success or failure
bool createInstance();
// Vulkan specific
// helper for attaching callbacks to the instance
bool initDebugMessenger();
// callback function for the vulkan debug extension: routes debug messages from validation layers to our app
static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity,
vk::DebugUtilsMessageTypeFlagsEXT type,
const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* pUserData);
Window* window_;
// Vulkan specific instance members
vk::raii::Context context_;
vk::raii::Instance instance_ = nullptr;
vk::raii::DebugUtilsMessengerEXT debugMessenger_ = nullptr;
// members for control over validation layers
static constexpr bool enableValidationLayers = true; // TODO: only true in debug mode
const std::vector<const char*> validationLayers = { "VK_LAYER_KHRONOS_validation" };

6
src/Pipeline.cpp Normal file
View File

@@ -0,0 +1,6 @@
#include "Pipeline.hpp"
Pipeline::Pipeline(Device* device) : device_(device) {
}

19
src/Pipeline.hpp Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "Device.hpp"
// the Pipeline lays out the rendering steps for the vulkan engine to follow
class Pipeline {
public:
Pipeline(Device* device);
~Pipeline() = default;
private:
Device* device_ = nullptr;
// will include shaders eventually
};

142
src/Swapchain.cpp Normal file
View File

@@ -0,0 +1,142 @@
#include "Swapchain.hpp"
#include <iostream>
Swapchain::Swapchain(Device* device) : device_(device) {
(void)createSwapchain();
(void)createImageViews();
}
bool Swapchain::createSwapchain() {
// get capabilities from the device
vk::raii::PhysicalDevice physicalDevice = device_->physicalDevice();
vk::raii::Device* logicalDevice = device_->logicalDevice();
vk::raii::SurfaceKHR* surface = device_->getSurface();
vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(*surface);
extent_ = chooseExtent(surfaceCapabilities);
uint32_t minImageCount = chooseMinImageCount(surfaceCapabilities);
std::vector<vk::SurfaceFormatKHR> availableFormats = physicalDevice.getSurfaceFormatsKHR(*surface);
surfaceFormat_ = chooseSurfaceFormat(availableFormats);
std::vector<vk::PresentModeKHR> availablePresentModes = physicalDevice.getSurfacePresentModesKHR(*surface);
vk::PresentModeKHR presentMode = choosePresentMode(availablePresentModes);
// create the swapchain object
vk::SwapchainCreateInfoKHR swapchainCreateInfo {
.surface = *surface,
.minImageCount = minImageCount,
.imageFormat = surfaceFormat_.format,
.imageColorSpace = surfaceFormat_.colorSpace,
.imageExtent = extent_,
.imageArrayLayers = 1, // only rendering to a single screen
.imageUsage = vk::ImageUsageFlagBits::eColorAttachment, // draw directly to swapchain image (as opposed to an offscreen image)
.imageSharingMode = vk::SharingMode::eExclusive, // owned only by a single queue
.preTransform = surfaceCapabilities.currentTransform, // dont flip or rotate the image
.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, // use other settings if you want the window to be partly transparent :o
.presentMode = presentMode,
.clipped = true // discard fragments in the window that aren't visible (obscured by another window or minimized)
};
// TODO: [after presentation] need to handle swapchain recreation as a result of window changes
swapchainCreateInfo.oldSwapchain = nullptr;
vkSwapchain_ = vk::raii::SwapchainKHR(*logicalDevice, swapchainCreateInfo);
images_ = vkSwapchain_.getImages();
if(vkSwapchain_ == nullptr) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: could not create the Vulkan Swapchain!" << std::endl;
return false;
} else {
return true;
}
}
vk::SurfaceFormatKHR Swapchain::chooseSurfaceFormat(std::vector<vk::SurfaceFormatKHR> const& availableFormats) {
if(availableFormats.empty()) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: no swap formats available!" << std::endl;
return vk::SurfaceFormatKHR { vk::Format::eUndefined, vk::ColorSpaceKHR::eSrgbNonlinear };
}
// check for preferred format vk::Format::eB8G8R8A8Srgb
const auto formatIt = std::ranges::find_if(availableFormats, [](const auto& format) {
return format.format == vk::Format::eB8G8R8A8Srgb && format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear;
});
// just return whatever if the correct one doesn't exist
return formatIt != availableFormats.end() ? *formatIt : availableFormats[0];
}
vk::PresentModeKHR Swapchain::choosePresentMode(std::vector<vk::PresentModeKHR> const& availablePresentModes) {
// vk::PresentModeKHR::eImmediate: present rendered fragments directly to the surface (results in frame tearing)
// vk::PresentModeKHR::eFifo: first in first out, waits for frame refresh (vsync)
// vk::PresentModeKHR::eFifoRelaxed: same as fifo but does not wait for vsync if the last frame was late
// vk::PresentModeKHR::eMailbox: same as fifo but continues rendering new frames while waiting for vsync
if(availablePresentModes.empty()) {
// fifo is guaranteed to be available so this is unreachable
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: no swap formats available!" << std::endl;
return vk::PresentModeKHR::eFifo;
}
// prefer mailbox if it exists; mailbox ensures least latency between displayed frames and cpu commands
assert(std::ranges::any_of(availablePresentModes, [](auto presentMode) { return presentMode == vk::PresentModeKHR::eFifo; }));
return std::ranges::any_of(availablePresentModes, [](const vk::PresentModeKHR value) {
return vk::PresentModeKHR::eMailbox == value; }) ? vk::PresentModeKHR::eMailbox : vk::PresentModeKHR::eFifo;
}
vk::Extent2D Swapchain::chooseExtent(vk::SurfaceCapabilitiesKHR const& capabilities) {
if(capabilities.currentExtent.width != UINT32_MAX) {
return capabilities.currentExtent;
}
// otherwise we have to get the swap extent via the window
int32_t width = 0;
int32_t height = 0;
(void)device_->getExtent(&width, &height);
vk::Extent2D extent = {
std::clamp<uint32_t>(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width),
std::clamp<uint32_t>(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)
};
return extent;
}
uint32_t Swapchain::chooseMinImageCount(vk::SurfaceCapabilitiesKHR const& capabilities) {
// prefer at least 3 images for the swapchain, otherwise default to the max the driver can support
uint32_t minImageCount = std::max(3u, capabilities.minImageCount); // TODO: magic numbers ?
if((0 < capabilities.maxImageCount) && capabilities.maxImageCount < minImageCount) {
return capabilities.maxImageCount;
} else {
return minImageCount;
}
}
bool Swapchain::createImageViews() {
if(!imageViews_.empty()) {
return false;
}
vk::ImageViewCreateInfo imageViewCreateInfo {
.viewType = vk::ImageViewType::e2D,
.format = surfaceFormat_.format,
.components = { vk::ComponentSwizzle::eIdentity, vk::ComponentSwizzle::eIdentity, vk::ComponentSwizzle::eIdentity, vk::ComponentSwizzle::eIdentity }, // default rgba
.subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, // aspect mask, baseMipLevel, levelCount, baseArrayLayer, layerCount
};
for(vk::Image &image : images_) {
imageViewCreateInfo.image = image;
imageViews_.emplace_back(*(device_->logicalDevice()), imageViewCreateInfo);
}
return true;
}

35
src/Swapchain.hpp Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include "Device.hpp"
// the swapchain functions as the layout for framebuffers that get written to by the gpu and read from to present to the screen
class Swapchain {
public:
Swapchain(Device* device);
~Swapchain() = default;
std::vector<vk::Image> getImages() { return vkSwapchain_.getImages(); }
private:
vk::raii::SwapchainKHR vkSwapchain_ = nullptr;
std::vector<vk::Image> images_;
std::vector<vk::raii::ImageView> imageViews_;
Device* device_ = nullptr;
vk::SurfaceFormatKHR surfaceFormat_;
vk::PresentModeKHR presentFormat_;
vk::Extent2D extent_;
vk::SurfaceFormatKHR chooseSurfaceFormat(std::vector<vk::SurfaceFormatKHR> const& availableFormats);
vk::PresentModeKHR choosePresentMode(std::vector<vk::PresentModeKHR> const& availablePresentModes);
vk::Extent2D chooseExtent(vk::SurfaceCapabilitiesKHR const& capabilities);
uint32_t chooseMinImageCount(vk::SurfaceCapabilitiesKHR const& capabilities);
bool createSwapchain();
bool createImageViews();
};

View File

@@ -2,6 +2,7 @@
#include "Window.hpp"
#include <SDL3/SDL_events.h>
#include <iostream>
Window::Window() {
@@ -43,3 +44,36 @@ void Window::handleEvent(SDL_Event& event) {
rendering_ = true;
}
}
bool Window::createSurface(vk::raii::Instance* instance, vk::raii::SurfaceKHR* surface) {
if(instance == nullptr) {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: cannot create surface with a null Vulkan instance." << std::endl;
return false;
}
#ifdef __WIN32
vk::Win32SurfaceCreateInfoKHR createInfo {
.hinstance = GetModuleHandle(nullptr),
.hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(sdlWindow_), "SDL.window.win32.hwnd", nullptr);
}
surface = instance->createWin32SurfaceKHR(createInfo);
#else // linux (this technically works on windows too but vulkan gives us an explicit method for WIN32)
// its just sdl3 uses the c vulkan api and the app uses the c++ api
VkSurfaceKHR cSurface;
(void)SDL_Vulkan_CreateSurface(sdlWindow_, static_cast<VkInstance>(**instance), nullptr, &cSurface);
*surface = vk::raii::SurfaceKHR(*instance, cSurface);
#endif // __WIN32
if(surface != nullptr) {
return true;
} else {
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Error: unable to create window surface." << std::endl;
return false;
}
}
bool Window::getExtent(int32_t* width, int32_t* height) {
SDL_GetWindowSizeInPixels(sdlWindow_, width, height);
return true;
}

View File

@@ -1,8 +1,9 @@
#pragma once
#include "SDL3/SDL.h"
#include "SDL3/SDL_vulkan.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <vulkan/vulkan_raii.hpp>
// reference: https://wiki.libsdl.org/SDL3/SDL_CreateWindow
class Window {
@@ -17,6 +18,8 @@ public:
bool rendering() { return rendering_; }
bool open() { return open_; }
bool createSurface(vk::raii::Instance* instance, vk::raii::SurfaceKHR* surface);
bool getExtent(int32_t* width, int32_t* height);
private: