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
11 changed files with 514 additions and 10 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
@@ -80,10 +83,18 @@ This seems like a WSL specific error and causes real issues with relaying graphi
$ 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);
}
@@ -126,7 +129,7 @@ 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) };
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));

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: