13 Commits

Author SHA1 Message Date
e3c4267fc2 add validation layers + debug callback
Some checks failed
Build and Test verification / build (push) Failing after 26s
Build and Test verification / test (push) Has been skipped
2026-05-14 21:10:36 -05:00
cedb80cb03 fix that buffoonery
Some checks failed
Build and Test verification / build (push) Failing after 28m1s
Build and Test verification / test (push) Has been skipped
2026-05-13 19:24:55 -05:00
6835359510 add validation layer checking (c++20 black magic)
Some checks failed
Build and Test verification / build (push) Failing after 38m46s
Build and Test verification / test (push) Has been skipped
2026-05-12 23:19:21 -05:00
e6b98362b6 fixed formatting because nano trolled me 2026-05-12 23:00:43 -05:00
0194699815 add required extensions to vulkan instance (+ use the c++ library over c library)
Some checks failed
Build and Test verification / build (push) Failing after 29s
Build and Test verification / test (push) Has been skipped
2026-05-11 23:32:28 -05:00
302b680a48 update contributing with goals 2026-05-11 22:26:59 -05:00
Preston McGee
836012648b Merge pull request #1 from Blitblank/feature/vulkan-setup
Some checks failed
Build and Test verification / test (push) Has been skipped
Build and Test verification / build (push) Failing after 3m41s
Feature: Vulkan Setup
2026-05-10 21:13:17 -05:00
6407cb7fdb consolidate some cmake
Some checks failed
Build and Test verification / build (push) Failing after 25s
Build and Test verification / test (push) Has been skipped
2026-05-10 17:20:21 -05:00
f54c889b11 me rambling about project plans
Some checks failed
Build and Test verification / build (push) Failing after 24s
Build and Test verification / test (push) Has been skipped
2026-05-10 13:13:57 -05:00
fcdb09a142 hello vulkan
Some checks failed
Build and Test verification / build (push) Failing after 25s
Build and Test verification / test (push) Has been skipped
2026-05-09 20:22:51 -05:00
f72bd8c44f create SDL window
Some checks failed
Build and Test verification / build (push) Failing after 26s
Build and Test verification / test (push) Has been skipped
2026-05-09 15:36:53 -05:00
c3afcadc0f fix pipeline job name
All checks were successful
Build and Test verification / build (push) Successful in 22s
Build and Test verification / test (push) Successful in 4s
2026-05-08 14:43:29 -05:00
434480d156 Merge pull request 'Feature: Repository Infrastructure Scaffolding' (#1) from feature/repo-setup into main
Reviewed-on: #1
2026-05-08 14:41:38 -05:00
12 changed files with 491 additions and 42 deletions

View File

@@ -1,6 +1,6 @@
name: Build and Test verification
run-name: ${{ gitea.actor }}: Build & Test on ${{ gitea.ref_name }}
run-name: "${{ gitea.actor }}: Build & Test on ${{ gitea.ref_name }}"
on: [push]
jobs:

View File

@@ -17,35 +17,66 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(googletest)
# sdl3
FetchContent_Declare(
sdl3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.4.x
)
FetchContent_MakeAvailable(sdl3)
find_package(Vulkan REQUIRED)
# add_subdirectory() to nest CMakeLists
add_executable(maiden
add_library(maiden_core STATIC
src/App.cpp
src/main.cpp
src/Window.cpp
src/Engine.cpp
# include extra source files here
)
add_executable(maiden
src/main.cpp
)
add_executable(maiden_test
src/App.cpp
test/TestApp.cpp
)
target_include_directories(maiden PRIVATE
target_include_directories(maiden_core PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
"${SDL3_SOURCE_DIR}/include" # was having issues with cmake automatically adding these for a lib
"$ENV{VULKAN_SDK}/include" # requires sourcing vulkan sdk setup-env.sh
# add additional include directories here
)
target_compile_options(maiden_core PRIVATE --coverage)
target_link_options(maiden_core PRIVATE --coverage)
target_link_options(maiden PRIVATE --coverage)
target_link_options(maiden_test PRIVATE --coverage)
# vulkan necessity
target_compile_definitions(maiden_core PRIVATE VULKAN_HPP_NO_STRUCT_CONSTRUCTORS)
target_link_libraries(maiden PRIVATE
maiden_core
SDL3::SDL3
Vulkan::Vulkan
)
# test only stuff down here VVV
target_include_directories(maiden_test PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_compile_options(maiden_test PRIVATE --coverage)
target_link_options(maiden_test PRIVATE --coverage)
# TODO: add option for disabling tests (like if target == DEBUG then tests on, otherwise tests off)
target_link_libraries(maiden_test PRIVATE
maiden_core
GTest::gmock
GTest::gmock_main
SDL3::SDL3
Vulkan::Vulkan
)
include(CTest)

46
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,46 @@
# Contributing
### Note: not a real contributing guide.
Because this project is so early in development, this information outlines a design framework and tasks that need to be completed.
## Vulkan Roadmap
From the authors themselves: https://docs.vulkan.org/tutorial/latest/00_Introduction.html
Basic outline where I pretty much copy the guide:
- Device Setup: GPU+Queues initialization, CPU+Queues initialization, include validation layers for testing
- Window Presentation: Surface initialization, Window attachment, Swapchain creation
- Graphics Pipeline Setup: Shaders -> Render Pass
- Command Buffers: Framebuffers, Draw commands, Presenation. <<-- HelloTriangle is here
- Vertex Buffers: Loading geometry data to gpu (mesh loading happens later)
- Descriptor Sets: Dynamic arbitrary data transfer from CPU -> GPU (the guide introduces 3D here)
- Textures: Loading Images, Sampling textures in shader
- Depth Buffering: additional render pass for determining most-front geometry
- Mesh Loading: guide uses TinyObj to load obj files into vertex & index vectors
- ... Boring optimization stuff, like mipmaps, multithreading, multisampling
- it has a section on raytracing :3
Goals:
- loading 3d models
- applying textures
- simple diffuse lighting
- vertex animations
- 3d camera and scene
- transparency
Future ambitions:
- entity component system
- shadows
- raytracing
- rigidbody simulation
## Supporting App Infrastructure
Although not crucial to the core rendering functions of the app, features separate from the rendering engine are convenient to have for testing and usability. Components below are less urgent but should be simpler to develop and implement.
### Performance Profiler
In order to properly test different Vulkan practices and engine implementations, performance should be quickly monitorable by the program. Important compute metrics include: CPU utilization, thread count, memory utilization, VRAM utilization, GPU utilization, and most importantly FPS. Supplemental details like geometry count or hardware configuration could be beneficial by providing context. Frame time (and fps) can be easily recorded inside of the engine, though other metrics must be accessed via the operating system's tools perhaps via the POSIX interface (I haven't done too much research). Recording the performance can initially be done through the logging service (detailed further below) but in-window updates with a GUI (oh boy) would be ideal.
### Configuration Service
A configuration service allows the application to be more flexible by providing the option to tweak parameters without requiring recompilation. Useful configuration parameters could be on window creation, rendering settings (geometry culling, descriptor layout), or vertex loading patterns. There are many options for implementation, like yaml, json, or cfg, with various c++ libraries for support. Classes that require configuration parameters will be injected with a config class from the top-level app and can access whatever it needs through the service.
### Logging Service
Consolidating logging functionality to a single service makes the code much more organized and easier to maintain. The advantage of centralized logging over std::couts everywhere is that much more information can be added to each logged statement: timestamps, function & line traces, log severity level (debug, info, warning, error), etc. We can also give it control to write to log files instead of just the terminal session so all logged messages can persist. Again, implementation involves injecting the service to components that require it and they can interface through the reference.

View File

@@ -18,6 +18,29 @@ $ git clone --recurse-submodules https://git.vxbard.net/homeburger/maiden.git
# If you have already cloned the repository and you need its submodules:
$ git submodule update --init --recursive
```
### Installation Prequisites
LunarG's Vulkan SDK is used for almost every Vulkan project, including this one.
Installation instructions here are for Ubuntu 24, further instructions can be found here: https://vulkan.lunarg.com/doc/sdk/1.4.341.1/linux/getting_started.html
```bash
$ cd ~/Downloads
$ wget "https://sdk.lunarg.com/sdk/download/1.4.341.1/linux/vulkansdk-linux-x86_64-1.4.341.1.tar.xz" # or the latest version on https://vulkan.lunarg.com/sdk/home
$ mkdir /opt/vulkan-sdk
$ cd /opt/vulkan-sdk
$ tar -xvf ~/Downloads/vulkansdk-linux-x86_64-1.4.341.1.tar.xz
$ sudo apt install libxcb-xinput0 libxcb-xinerama0 libxcb-cursor-dev # vulkan prerequeisites
```
Then to setup your current session:
```bash
$ source /opt/vulkan-sdk/1.4.341.1/setup-env.sh # or whichever version you have
```
Note: sourcing only affects your current shell session. Add to your .bashrc script if you wish for it to persist.
Verify your installation by using the the command:
```bash
$ vulkaninfo
```
### Build
This app uses CMake and C++20, so a compatible compiler (gcc8, clang9, msvc16) are necessary.
@@ -47,7 +70,21 @@ $ cd build
$ gcovr -r .. --filter "../src"
```
### app troubleshooting here
## App Troubleshooting
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)
## Development Roadmap
### lots of todo here
See CONTRIBUTING.md for design and more specific development plans.

View File

@@ -1,18 +1,49 @@
#include "App.hpp"
void App::run() {
std::cout << "im an app and im running !!" << std::endl;
(void)foo();
return;
#include <thread>
#include <chrono>
#include <SDL3/SDL_events.h>
App::App(): window_(new Window()), engine_(new Engine(window_)) {
// smart pointers might be a good idea
// since app will be the top level owner we can use unique ptrs for almost everything
}
int32_t App::foo() {
int32_t App::run() {
return 12;
SDL_Event event;
while (window_->open()) {
// app loop for as long as the window is open
// pass events to the window
while(SDL_PollEvent(&event) != 0) {
window_->handleEvent(event);
}
// pass vulkan handling to engine
// call engine.render() or something
// engine will have a pointer to window so can handle pushing to the screen
if(window_->rendering()) {
// engine.draw();
// TODO: performance profiling :3
// static int counter = 0;
// if(counter > 100) {
// std::cout << "out" << std::endl;
// counter = 0;
// }
// counter++;
} else {
// throttle while the window isn't active
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// SDL teardown handled in window destructor
return 0; // maybe want to have an error codes enum for better clarity
}

View File

@@ -4,16 +4,22 @@
#include <iostream>
#include <cstdint>
#include "Window.hpp"
#include "Engine.hpp"
class App {
public:
App() = default;
App();
~App() = default;
void run();
int32_t run();
private:
int32_t foo();
Window* window_;
Engine* engine_;
bool rendering_ = true;
};

182
src/Engine.cpp Normal file
View File

@@ -0,0 +1,182 @@
#include "Engine.hpp"
#include <iostream>
Engine::Engine(Window* window): window_(window) {
// cleans up this constructor
init();
}
void Engine::init() {
if(createInstance()) {
// TODO: need some kind of logger service
std::cout << "[" << __FUNCTION__ << ": " << __LINE__ << "] Vulkan instance successfully created." << std::endl;
} else {
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
}
void Engine::draw() {
}
bool Engine::createInstance() {
uint32_t errorCount = 0;
// create the appInfo filled with information about our app
constexpr vk::ApplicationInfo appInfo { // using the c++ api instead of the c api
.pApplicationName = "maiden",
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
.pEngineName = "null",
.engineVersion = VK_MAKE_VERSION(1, 0, 0),
.apiVersion = VK_API_VERSION_1_4 // this one is most important
};
std::vector<const char*> requiredInstanceExtensions = getRequiredInstanceExtensions();
// get all available extensions
auto extensionProperties = context_.enumerateInstanceExtensionProperties();
// print if we feel like it
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(...)
// 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, requiredInstanceExtensions[i]) == 0) {
found = true;
break;
}
}
if(!found) {
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: " << 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;
}
vk::InstanceCreateInfo instanceCreateInfo {
.pApplicationInfo = &appInfo,
.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) };
// 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;
}

50
src/Engine.hpp Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
// resource allocation is initializion my beloved
#include <vulkan/vulkan_raii.hpp>
#include "Window.hpp"
class Engine {
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
void draw();
private:
// 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();
// 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" };
};

45
src/Window.cpp Normal file
View File

@@ -0,0 +1,45 @@
#include "Window.hpp"
#include <SDL3/SDL_events.h>
Window::Window() {
(void)init();
}
Window::~Window() {
SDL_DestroyWindow(sdlWindow_);
SDL_Quit();
}
int Window::init() {
// TODO: config service for controlling window parameters
sdlWindow_ = SDL_CreateWindow("Maiden", 1280u, 720u, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);
if(sdlWindow_ != nullptr) {
rendering_ = true;
open_ = true;
return 0;
} else {
return -1;
}
}
void Window::handleEvent(SDL_Event& event) {
if(event.type == SDL_EVENT_QUIT ) {
open_ = false;
}
if(event.type == SDL_EVENT_WINDOW_MINIMIZED) {
rendering_ = false;
} else if(event.type == SDL_EVENT_WINDOW_RESTORED) {
rendering_ = true;
}
}

34
src/Window.hpp Normal file
View File

@@ -0,0 +1,34 @@
#pragma once
#include "SDL3/SDL.h"
#include "SDL3/SDL_vulkan.h"
// reference: https://wiki.libsdl.org/SDL3/SDL_CreateWindow
class Window {
public:
Window();
~Window();
// for SDL3 event polling, runs once per app iteration in its while(running) loop
void handleEvent(SDL_Event& event);
bool rendering() { return rendering_; }
bool open() { return open_; }
private:
// launches window, runs at app startup
int init();
// this window class will eventually hold mouse, keyboard, audio, etc. interfaces, like an SDL3 hub
// app will be able to attach callbacks for mouse and keyboard events
SDL_Window* sdlWindow_;
bool rendering_ = false;
bool open_ = false;
};

View File

@@ -1,14 +1,12 @@
#include <iostream>
#include "App.hpp"
int main(int argc, char** argv) {
std::cout << "hi mom !" << std::endl;
// create app and run
App app;
app.run();
// no freaking way
return 0;
}

View File

@@ -2,9 +2,7 @@
#include <gmock/gmock.h>
#include <memory>
#define private public
#include <App.hpp>
#undef private
#include "App.hpp"
class TestApp : public testing::Test {
@@ -21,16 +19,7 @@ class TestApp : public testing::Test {
TEST_F(TestApp, TestApp_run_nominal) {
createUut();
uut_->run();
// no expect here
}
TEST_F(TestApp, TestApp_foo_nominal) {
createUut();
EXPECT_EQ(uut_->foo(), 12);
//EXPECT_EQ(uut_->run(), 0l);
}