diff --git a/.idea/editor.xml b/.idea/editor.xml
index 4596677..5f71ed0 100644
--- a/.idea/editor.xml
+++ b/.idea/editor.xml
@@ -245,17 +245,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3d3e78b..67eafa9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,9 +19,14 @@ project (EtrediaEmpires LANGUAGES CXX)
add_subdirectory(sdk/map)
add_subdirectory(sdk/assets)
+
add_subdirectory(p7lib)
add_subdirectory(utils)
+
add_subdirectory(server/lib)
add_subdirectory(engine-api)
+
add_subdirectory(renderers/magnum-renderer)
+add_subdirectory(renderers/vulkan)
+
add_subdirectory(engine)
\ No newline at end of file
diff --git a/assets/shaders/provinces.frag b/assets/shaders/provinces.frag
index 793a185..972e9f9 100644
Binary files a/assets/shaders/provinces.frag and b/assets/shaders/provinces.frag differ
diff --git a/assets/shaders/provinces.vert b/assets/shaders/provinces.vert
index 0530a90..a14e55d 100644
Binary files a/assets/shaders/provinces.vert and b/assets/shaders/provinces.vert differ
diff --git a/cmake/library.cmake b/cmake/library.cmake
index 43c51c9..9500f50 100644
--- a/cmake/library.cmake
+++ b/cmake/library.cmake
@@ -1,4 +1,12 @@
function(add_library_target TARGET_NAME)
+ cmake_parse_arguments(
+ ADDITIONAL
+ ""
+ ""
+ "MODULES;MODULE_BASEDIRS"
+ ""
+ ${ARGN}
+ )
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -22,23 +30,14 @@ function(add_library_target TARGET_NAME)
modules/*.ixx
)
- target_sources(${TARGET_NAME} PUBLIC FILE_SET modules TYPE CXX_MODULES BASE_DIRS ${PROJECT_SOURCE_DIR} FILES ${MODULES})
+ list(APPEND MODULES ${ADDITIONAL_MODULES})
+ set (MODULE_BASEDIRS ${PROJECT_SOURCE_DIR})
+ list(APPEND MODULE_BASEDIRS ${ADDITIONAL_MODULE_BASEDIRS})
+
+ target_sources(${TARGET_NAME} PUBLIC FILE_SET modules TYPE CXX_MODULES BASE_DIRS ${MODULE_BASEDIRS} FILES ${MODULES})
target_sources(${TARGET_NAME} PUBLIC FILE_SET headers TYPE HEADERS BASE_DIRS ${PROJECT_SOURCE_DIR} FILES ${HEADERS})
target_sources(${TARGET_NAME} PRIVATE ${SOURCES})
- # MSVC STD module linking
- # target_compile_options(${TARGET_NAME} PRIVATE
- # /EHsc
- # "SHELL:/reference \"std=$ENV{STD_MODULE_DIR}/std.ifc\""
- # "SHELL:/reference \"std.compat=$ENV{STD_MODULE_DIR}/std.compat.ifc\""
- #)
- #target_link_libraries(${TARGET_NAME} PRIVATE "$ENV{STD_MODULE_DIR}/std.obj")
- # target_link_libraries(${TARGET_NAME} PRIVATE "$ENV{STD_MODULE_DIR}/std.compat.obj")
- #target_compile_options(${TARGET_NAME} PRIVATE
- # /wd5050
- #)
- #target_compile_features(${TARGET_NAME} PRIVATE cxx_std_23 INTERFACE cxx_std_23)
-
# Clang modules
# target_compile_options(${TARGET_NAME} PRIVATE
# -fmodules
diff --git a/engine-api/modules/Render.cppm b/engine-api/modules/Render.cppm
index 0f0f1b1..329f72e 100644
--- a/engine-api/modules/Render.cppm
+++ b/engine-api/modules/Render.cppm
@@ -62,8 +62,8 @@ export namespace ee_engine {
void SetCamera(const CameraTransform& camera);
constexpr CameraTransform getCamera() const noexcept { return camera; };
- glm::vec2 ScreenToWorld(const float screenx, const float screenY);
- glm::vec2 ScreenToWorld(const glm::vec2& screenPos);
+ glm::vec2 ScreenToWorld(float screenX, float screenY) const;
+ glm::vec2 ScreenToWorld(const glm::vec2& screenPos) const;
protected:
bool _isReady;
@@ -77,12 +77,12 @@ export namespace ee_engine {
MapMesh *mapMesh;
ShaderRefs shaders;
- struct RegionIndecies {
- std::size_t startIndex;
+ struct RegionRanges {
+ std::size_t offset;
std::size_t provincesCount;
};
- std::unordered_map regionsMapping;
+ std::unordered_map regionsMapping;
explicit IRenderer(GLFWwindow *window);
@@ -92,7 +92,7 @@ export namespace ee_engine {
virtual void ApplyCamera() = 0;
- virtual void UpdateProvinces(const std::size_t index, const std::vector& data) = 0;
+ virtual void UpdateProvinces(std::size_t offset, const std::vector& data) = 0;
private:
void UpdateRegionCmd(RendererCommand& command);
diff --git a/engine-api/src/Render.cpp b/engine-api/src/Render.cpp
index 84bda48..aced822 100644
--- a/engine-api/src/Render.cpp
+++ b/engine-api/src/Render.cpp
@@ -31,7 +31,7 @@ void ee_engine::IRenderer::Draw() {
DrawUI();
}
-void ee_engine::IRenderer::ResizeWindow(int width, int height) {
+void ee_engine::IRenderer::ResizeWindow(const int width, const int height) {
screenWidth = width;
screenHeight = height;
screenAspect = static_cast(screenWidth) / static_cast(screenHeight);
@@ -45,15 +45,15 @@ void ee_engine::IRenderer::SetCamera(const CameraTransform& camera) {
this->camera = camera;
- float halfHeight = camera.zoom;
- float halfWidth = camera.zoom * screenAspect;
+ const float halfHeight = camera.zoom;
+ const float halfWidth = camera.zoom * screenAspect;
- glm::mat4 P = glm::ortho(
+ const glm::mat4 P = glm::ortho(
-halfWidth, +halfWidth,
-halfHeight, +halfHeight
);
- glm::mat4 V = glm::translate(
+ const glm::mat4 V = glm::translate(
glm::mat4(1.0f),
glm::vec3(-camera.position, 0.0f)
);
@@ -64,11 +64,11 @@ void ee_engine::IRenderer::SetCamera(const CameraTransform& camera) {
ApplyCamera();
}
-glm::vec2 ee_engine::IRenderer::ScreenToWorld(const float screenX, const float screenY) {
+glm::vec2 ee_engine::IRenderer::ScreenToWorld(const float screenX, const float screenY) const {
return ScreenToWorld(glm::vec2(screenX, screenY));
}
-glm::vec2 ee_engine::IRenderer::ScreenToWorld(const glm::vec2& screenPos) {
+glm::vec2 ee_engine::IRenderer::ScreenToWorld(const glm::vec2& screenPos) const {
float x = 2.0f * screenPos.x / screenWidth - 1.0f;
float y = -2.0f * screenPos.y / screenHeight + 1.0f;
@@ -79,7 +79,7 @@ glm::vec2 ee_engine::IRenderer::ScreenToWorld(const glm::vec2& screenPos) {
return glm::vec2(world) / world.w;
}
-void ee_engine::IRenderer::UpdateRegionCmd(ee_engine::RendererCommand& command) {
+void ee_engine::IRenderer::UpdateRegionCmd(RendererCommand& command) {
if (!regionsMapping.contains(command.region.id)) {
LOG(WARNING, "Updating unregistered region with id: %d", command.region.id);
return;
@@ -90,14 +90,14 @@ void ee_engine::IRenderer::UpdateRegionCmd(ee_engine::RendererCommand& command)
command.region.id, command.region.data.regionId);
}
- RegionIndecies mapping = regionsMapping[command.region.id];
+ RegionRanges mapping = regionsMapping[command.region.id];
std::vector buffer;
buffer.reserve(mapping.provincesCount);
for (std::size_t i = 0; i < mapping.provincesCount; i++) {
buffer.push_back(command.region.data);
}
- UpdateProvinces(mapping.startIndex, buffer);
+ UpdateProvinces(mapping.offset, buffer);
- command.type = ee_engine::RendererCommand::COMPLETED;
+ command.type = RendererCommand::COMPLETED;
}
diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt
index b31ea13..ea341f1 100644
--- a/engine/CMakeLists.txt
+++ b/engine/CMakeLists.txt
@@ -16,6 +16,7 @@ add_executable_target(${EXE})
target_link_libraries(${EXE} PUBLIC utils)
target_link_libraries(${EXE} PUBLIC engine-api)
target_link_libraries(${EXE} PUBLIC magnum-renderer)
+target_link_libraries(${EXE} PUBLIC vulkan-renderer)
target_link_libraries(${EXE} PRIVATE Boost::lockfree)
target_link_libraries(${EXE} PRIVATE xxHash::xxhash)
diff --git a/engine/src/Actions.cpp b/engine/src/Actions.cpp
index a491b0f..87886f2 100644
--- a/engine/src/Actions.cpp
+++ b/engine/src/Actions.cpp
@@ -10,17 +10,17 @@ ee_engine::InputHandlers Actions::mappings{
.mapScroll = MapScroll
};
-void Actions::Zoom(float dx, float dy) {
+void Actions::Zoom(const float dx, const float dy) {
ee_engine::CameraTransform cam = Engine::renderer->getCamera();
cam.zoom *= pow(1.1f, -dy);
Engine::renderer->SetCamera(cam);
}
-void Actions::MapScroll(float x, float y, float lastX, float lastY) {
+void Actions::MapScroll(const float x, const float y, const float lastX, const float lastY) {
ee_engine::CameraTransform cam = Engine::renderer->getCamera();
- glm::vec2 pos = Engine::renderer->ScreenToWorld(x, y);
- glm::vec2 lastPos = Engine::renderer->ScreenToWorld(lastX, lastY);
+ const glm::vec2 pos = Engine::renderer->ScreenToWorld(x, y);
+ const glm::vec2 lastPos = Engine::renderer->ScreenToWorld(lastX, lastY);
cam.position -= pos - lastPos;
Engine::renderer->SetCamera(cam);
diff --git a/engine/src/main.cpp b/engine/src/main.cpp
index 01bc63c..3e3c94a 100644
--- a/engine/src/main.cpp
+++ b/engine/src/main.cpp
@@ -5,7 +5,7 @@ import utils;
import Engine;
import Actions;
-import MagnumRender;
+import VulkanRender;
import GLFWInput;
import std;
@@ -20,7 +20,7 @@ int main(int argc, char **argv) {
glfwInit();
// Vulkan only
- // glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+ glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow *window = glfwCreateWindow(800, 600, "Etredia Empires", nullptr, nullptr);
@@ -38,16 +38,17 @@ int main(int argc, char **argv) {
Engine::assetManager->LoadDir(ee_assets::MATERIAL, "assets/materials");
// Load assets
- Engine::assetManager->LoadAsync(ee_assets::MAP_MESH, "assets/map/ET.map");
+ Engine::assetManager->LoadAsync(ee_assets::MAP_MESH, "assets/map/regions.map");
Engine::assetManager->LoadAsync(ee_assets::SAVE, "assets/scenarios/empires.save");
// Setup input manager TODO: builder
Engine::input = Allocate(Actions::mappings);
// Setup renderer TODO: builder
- Engine::renderer = reinterpret_cast(Allocate(window));
+ //Engine::renderer = reinterpret_cast(Allocate(window));
+ Engine::renderer = reinterpret_cast(Allocate(window));
- ee_assets::AssetMapMesh& regions = Engine::assetManager->FindOnce("assets/map/ET.map");
+ ee_assets::AssetMapMesh& regions = Engine::assetManager->FindOnce("assets/map/regions.map");
Engine::renderer->RegisterMapMesh(®ions.content);
//TODO: ShaderRefs builder from set of AssetMaterial
@@ -64,19 +65,20 @@ int main(int argc, char **argv) {
Engine::renderer->Init();
Engine::renderer->SetCamera({
- //.position = {1000.0f, 1000.0f},
+ .position = {1000.0f, 1000.0f},
.zoom = 2000.0f
+ //.zoom = 1.0f
});
- glfwSetFramebufferSizeCallback(window, [](GLFWwindow *window, int width, int height) {
+ glfwSetFramebufferSizeCallback(window, [](GLFWwindow*, const int width, const int height) {
Engine::renderer->ResizeWindow(width, height);
});
// Setup Client TODO: builder
Engine::client = Allocate();
- // AssetHandle save = Engine::assetManager->Find("assets/scenarios/empires.save");
- // Engine::client->LoadSave(save);
- // Engine::client->Init();
+ const AssetHandle save = Engine::assetManager->Find("assets/scenarios/empires.save");
+ Engine::client->LoadSave(save);
+ Engine::client->Init();
Engine::client->Start();
// Game loop
@@ -99,5 +101,4 @@ int main(int argc, char **argv) {
return 0;
}
-// TODO: Use CRTP where possible in classes with virtual/override
// TODO: Leaks detection
diff --git a/renderers/magnum-renderer/modules/Render.cppm b/renderers/magnum-renderer/modules/MagnumRender.cppm
similarity index 82%
rename from renderers/magnum-renderer/modules/Render.cppm
rename to renderers/magnum-renderer/modules/MagnumRender.cppm
index 4f97adc..2049c28 100644
--- a/renderers/magnum-renderer/modules/Render.cppm
+++ b/renderers/magnum-renderer/modules/MagnumRender.cppm
@@ -18,8 +18,8 @@ using namespace Magnum;
export namespace ee_magnum {
class MagnumRenderer final : public ee_engine::IRenderer {
public:
- MagnumRenderer(GLFWwindow *window);
- ~MagnumRenderer();
+ explicit MagnumRenderer(GLFWwindow *window);
+ ~MagnumRenderer() override;
void Init() override;
void Shutdown() override;
@@ -31,8 +31,7 @@ export namespace ee_magnum {
void ApplyCamera() override;
- virtual void
- UpdateProvinces(const std::size_t index, const std::vector& data) override;
+ void UpdateProvinces(std::size_t offset, const std::vector& data) override;
private:
Platform::GLContext context;
@@ -44,7 +43,5 @@ export namespace ee_magnum {
void ValidateSupport();
void LoadShaders();
void LoadProvinces();
-
- void ApplyMapMVP();
};
}
diff --git a/renderers/magnum-renderer/src/Render.cpp b/renderers/magnum-renderer/src/MagnumRender.cpp
similarity index 96%
rename from renderers/magnum-renderer/src/Render.cpp
rename to renderers/magnum-renderer/src/MagnumRender.cpp
index ddff215..2b57f55 100644
--- a/renderers/magnum-renderer/src/Render.cpp
+++ b/renderers/magnum-renderer/src/MagnumRender.cpp
@@ -95,7 +95,7 @@ void ee_magnum::MagnumRenderer::LoadProvinces() {
provincesData.push_back(data);
}
- regionsMapping[region] = {.startIndex = regionIndex, .provincesCount = provs.size()};
+ regionsMapping[region] = {.offset = regionIndex, .provincesCount = provs.size()};
regionIndex += provs.size() * sizeof(ee_engine::ProvinceSSBO);
}
@@ -107,10 +107,10 @@ void ee_magnum::MagnumRenderer::LoadProvinces() {
provinceSSBO.bind(GL::Buffer::Target::ShaderStorage, ProvinceMaterial::PROVINCES_DATA_BINDING);
}
-void ee_magnum::MagnumRenderer::UpdateProvinces(const std::size_t index,
+void ee_magnum::MagnumRenderer::UpdateProvinces(const std::size_t offset,
const std::vector& data) {
Containers::ArrayView provincesData(data.data(), data.size());
- provinceSSBO.setSubData(index, provincesData);
+ provinceSSBO.setSubData(offset, provincesData);
}
void ee_magnum::MagnumRenderer::Shutdown() {
diff --git a/renderers/vulkan/CMakeLists.txt b/renderers/vulkan/CMakeLists.txt
new file mode 100644
index 0000000..2d868ba
--- /dev/null
+++ b/renderers/vulkan/CMakeLists.txt
@@ -0,0 +1,29 @@
+include(${CMAKE_SOURCE_DIR}/cmake/library.cmake)
+
+set(LIB vulkan-renderer)
+
+# Find dependencies
+
+find_package(Vulkan REQUIRED)
+find_package(glm CONFIG REQUIRED)
+
+# Link library
+
+add_library_target(${LIB}
+ MODULES ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include/vulkan/vulkan.cppm
+ MODULE_BASEDIRS ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include/
+)
+
+target_compile_definitions(${LIB}
+ PUBLIC
+ VULKAN_HPP_NO_CONSTRUCTORS
+ VULKAN_HPP_ENABLE_STD_MODULE
+ VULKAN_HPP_STD_MODULE=std
+)
+
+target_link_libraries(${LIB} PUBLIC engine-api)
+target_link_libraries(${LIB} PUBLIC utils)
+
+target_link_libraries(${LIB} PUBLIC p7)
+target_link_libraries(${LIB} PRIVATE glm::glm)
+target_link_libraries(${LIB} PRIVATE Vulkan::Vulkan)
\ No newline at end of file
diff --git a/renderers/vulkan/modules/VulkanRenderer.cppm b/renderers/vulkan/modules/VulkanRenderer.cppm
new file mode 100644
index 0000000..61d23fa
--- /dev/null
+++ b/renderers/vulkan/modules/VulkanRenderer.cppm
@@ -0,0 +1,89 @@
+module;
+
+#include
+#include
+
+export module VulkanRender;
+import Render;
+import views;
+import vulkan_render;
+import std;
+
+namespace ee_vulkan {
+ template using lazy = std::optional;
+ constexpr std::uint64_t TIMEOUT = std::numeric_limits::max();
+
+ struct DrawConfig {
+ static constexpr vk::ClearValue CLEAR_COLOR = {.color = {std::array{1.0f, 1.0f, 1.0f, 1.0f}}};
+ static constexpr vk::ClearValue CLEAR_DEPTH = {.depthStencil = {1.0f, 0}};
+ };
+
+ struct Mesh {
+ lazy> vertices;
+ lazy> indices;
+ lazy> storage;
+
+ lazy> indirectCommands;
+ };
+}
+
+export namespace ee_vulkan {
+ class VulkanRenderer final : public ee_engine::IRenderer {
+ public:
+ explicit VulkanRenderer(GLFWwindow *window);
+ ~VulkanRenderer() override;
+
+ void Init() override;
+ void Shutdown() override;
+
+ protected:
+ void Clear() override;
+ void DrawMap() override;
+ void DrawUI() override;
+ void ResizeWindow(int width, int height) override;
+
+ void ApplyCamera() override;
+
+ void UpdateProvinces(std::size_t offset, const std::vector& data) override;
+
+ private:
+ const std::uint32_t MAX_FRAMES_IN_FLIGHT = 3;
+
+ bool isDrawing = false;
+
+ lazy context;
+ vulkan_render::Surface surface;
+ lazy swapchain;
+ lazy renderCommands;
+ lazy provincesPipeline;
+ lazy syncManager;
+
+ lazy> depth;
+ lazy> viewProjectionUniform;
+ Mesh provinces;
+
+ std::size_t currentFrame = 0;
+
+ void RecreateSwapchain();
+
+ void LoadProvincesMaterial();
+ void LoadProvinces();
+ void UpdateData(std::uint32_t frame);
+ };
+}
\ No newline at end of file
diff --git a/renderers/vulkan/modules/control.cppm b/renderers/vulkan/modules/control.cppm
new file mode 100644
index 0000000..57c6236
--- /dev/null
+++ b/renderers/vulkan/modules/control.cppm
@@ -0,0 +1,80 @@
+/**
+ * Control partition module is responsible for configuration and usage of Vulkan operation objects
+ * including command buffers, dynamic rendering, synchronization objects and so on.
+ */
+
+export module vulkan_render:control;
+import :hardware;
+import :resources;
+import :data;
+import std;
+import vulkan_hpp;
+
+export namespace vulkan_render {
+
+ class Command {
+ public:
+ const Context &context;
+ vk::raii::CommandBuffer handle;
+
+ Command(const Context &context, vk::raii::CommandBuffer&& buffer);
+ Command(const Context &context, const vk::raii::CommandPool &pool);
+
+ Command(Command&&) noexcept = default;
+ Command(const Command&) = delete;
+ Command& operator=(const Command&) = delete;
+
+ void Begin() const;
+ void Submit(const vk::raii::Queue &queue) const;
+ };
+
+ /**
+ * Set of Vulkan commands buffers for each frame in flight.
+ */
+ class RenderCommands {
+ public:
+ vk::raii::CommandPool graphicsPool;
+ vk::raii::CommandPool transferPool;
+
+ std::vector graphicsBuffers;
+ std::vector transferBuffers;
+
+ /**
+ * Create pools and initialize all command buffers.
+ * @param context Vulkan context with ready device.
+ * @param maxFramesInFlight amount of created buffers for each purpose.
+ */
+ RenderCommands(const Context &context, std::uint32_t maxFramesInFlight);
+
+ void SubmitStagingBuffer(const BaseBuffer &buffer) const;
+
+ private:
+ static vk::raii::CommandPool MakePool(const Context &context, std::uint32_t queueFamily);
+ static std::vector MakeBuffers(
+ const Context &context,
+ const vk::raii::CommandPool &pool,
+ std::uint32_t size
+ );
+ };
+
+ //TODO: sync2 with timeline-semaphore(s)
+ class SyncManager {
+ public:
+ const Context &context;
+ const std::uint32_t TIMEOUT;
+
+ std::vector imageAvailable;
+ std::vector renderFinished;
+ std::vector inFlightFences;
+ std::vector imagesInFlight;
+
+ SyncManager(const Context &context, std::uint32_t maxFramesInFlight, std::uint64_t timeout);
+ void WaitInFlightFence(std::size_t currentFrame);
+ void WaitImageInFlight(std::size_t currentFrame, std::uint32_t imageIndex);
+ vk::Result AcquireNextImage(
+ const Swapchain &swapchain,
+ std::size_t currentFrame,
+ std::uint32_t &imageIndex
+ ) const;
+ };
+}
\ No newline at end of file
diff --git a/renderers/vulkan/modules/data.cppm b/renderers/vulkan/modules/data.cppm
new file mode 100644
index 0000000..71cd310
--- /dev/null
+++ b/renderers/vulkan/modules/data.cppm
@@ -0,0 +1,276 @@
+/**
+ * Data partition module is responsible for configuration user-specific Vulkan objects
+ * like buffers, textures, transforms and so on.
+ */
+
+module;
+
+#if defined(_MSC_VER)
+#define NO_UNIQUE_ADDRESS [[msvc::no_unique_address]]
+#else
+#define NO_UNIQUE_ADDRESS [[no_unique_address]]
+#endif
+
+export module vulkan_render:data;
+import :hardware;
+import :resources;
+import std;
+import vulkan_hpp;
+import utils;
+
+namespace vulkan_render {
+ struct NoMember {
+ constexpr NoMember() = default;
+ constexpr NoMember(std::nullptr_t) {}
+ };
+
+ template
+ using optional = std::conditional_t;
+
+ template
+ using StagingBuffer = optional;
+
+ template
+ using StagingDeviceMemory = optional;
+
+ template
+ using MappedMemory = optional, isMapped>;
+}
+
+export namespace vulkan_render {
+
+ enum class BufferMode {
+ STATIC,
+ CPU_ACCESS,
+ STAGING_ACCESS
+ };
+
+ template
+ class BaseBuffer {
+ public:
+ const Context &context;
+
+ vk::raii::Buffer handle = nullptr;
+ vk::raii::DeviceMemory memory = nullptr;
+
+ NO_UNIQUE_ADDRESS StagingBuffer stagingHandle = nullptr;
+ NO_UNIQUE_ADDRESS StagingDeviceMemory stagingMemory = nullptr;
+
+ std::size_t memorySize;
+
+ explicit BaseBuffer(const Context &context, const std::size_t memorySize) :
+ context(context),
+ memorySize(memorySize) {}
+ };
+
+ template
+ class Buffer : public BaseBuffer {
+ public:
+ NO_UNIQUE_ADDRESS MappedMemory content;
+ static constexpr std::size_t elementSize = sizeof(T);
+
+ Buffer(const Context &context, const std::span data) requires (mode == BufferMode::STATIC)
+ : BaseBuffer(context, data.size() * elementSize) {
+
+ MakeInstance(
+ usage,
+ vk::Flags(vk::MemoryPropertyFlagBits::eHostVisible) | vk::MemoryPropertyFlagBits::eHostCoherent,
+ this->handle,
+ this->memory
+ );
+
+ void *mappedMemory = this->memory.mapMemory(0, this->memorySize);
+ std::ranges::copy(data, static_cast(mappedMemory));
+ this->memory.unmapMemory();
+ }
+
+ Buffer(const Context &context, const std::size_t size) requires (mode == BufferMode::CPU_ACCESS)
+ : BaseBuffer(context, size * elementSize) {
+
+ MakeInstance(
+ usage,
+ vk::Flags(vk::MemoryPropertyFlagBits::eHostVisible) | vk::MemoryPropertyFlagBits::eHostCoherent,
+ this->handle,
+ this->memory
+ );
+
+ void *mappedMemory = this->memory.mapMemory(0, this->memorySize);
+
+ content = std::span(static_cast(mappedMemory), size);
+ std::ranges::fill(content, T{});
+ }
+
+ Buffer(const Context &context, const std::size_t size) requires (mode == BufferMode::STAGING_ACCESS)
+ : BaseBuffer(context, size * elementSize) {
+ MakeInstance(
+ vk::Flags(vk::BufferUsageFlagBits::eTransferDst) | usage,
+ vk::MemoryPropertyFlagBits::eDeviceLocal,
+ this->handle,
+ this->memory
+ );
+
+ MakeInstance(
+ vk::BufferUsageFlagBits::eTransferSrc,
+ vk::Flags(vk::MemoryPropertyFlagBits::eHostVisible) | vk::MemoryPropertyFlagBits::eHostCoherent,
+ this->stagingHandle,
+ this->stagingMemory
+ );
+
+ void *mappedMemory = this->stagingMemory.mapMemory(0, this->memorySize);
+
+ content = std::span(static_cast(mappedMemory), size);
+ std::ranges::fill(content, T{});
+ }
+
+ private:
+ void MakeInstance(
+ const vk::BufferUsageFlags &usageFlags,
+ const vk::MemoryPropertyFlags &memoryFlags,
+ vk::raii::Buffer &buffer,
+ vk::raii::DeviceMemory &memory
+ ) {
+ buffer = this->context.device.handle.createBuffer({
+ .size = this->memorySize,
+ .usage = usageFlags,
+ .sharingMode = vk::SharingMode::eExclusive
+ });
+
+ if (
+ std::optional allocated = this->context.AllocateBufferMemory(
+ buffer,
+ memoryFlags
+ );
+ allocated.has_value()
+ ) {
+ memory = std::move(allocated).value();
+ buffer.bindMemory(memory, 0);
+ }
+ }
+ };
+
+ enum class TextureMode {
+ RENDER,
+ DEPTH
+ };
+
+ template
+ class Texture {
+ public:
+ const Context &context;
+ const std::uint32_t width;
+ const std::uint32_t height;
+
+ NO_UNIQUE_ADDRESS MappedMemory data;
+
+ vk::raii::Image handle = nullptr;
+ vk::raii::ImageView view = nullptr;
+ vk::raii::DeviceMemory memory = nullptr;
+
+ const vk::Format format;
+
+ Texture(
+ const Context &context,
+ const std::uint32_t width, const std::uint32_t height,
+ const vk::Format &format,
+ const std::span textureData
+ ) requires (mode == TextureMode::RENDER)
+ : context(context), width(width), height(height), data(textureData), format(format) {
+ Buffer<
+ std::byte,
+ vk::BufferUsageFlagBits::eTransferSrc,
+ BufferMode::STATIC
+ > stagingBuffer(context, data);
+
+ MakeInstance(
+ vk::Flags(vk::ImageUsageFlagBits::eTransferDst) | vk::ImageUsageFlagBits::eSampled,
+ vk::MemoryPropertyFlagBits::eDeviceLocal,
+ handle,
+ memory
+ );
+
+ /* TODO: Transition image layout
+ * Copy buffer to image
+ * Transition image layout
+ * Create view
+ */
+ }
+
+ Texture(
+ const Context &context,
+ const Surface &surface
+ ) requires (mode == TextureMode::DEPTH)
+ : context(context), width(surface.extent.width), height(surface.extent.height), format(vk::Format::eD32SfloatS8Uint) {
+ MakeInstance(
+ vk::ImageUsageFlagBits::eDepthStencilAttachment,
+ vk::MemoryPropertyFlagBits::eDeviceLocal,
+ handle,
+ memory
+ );
+ MakeView(vk::ImageAspectFlagBits::eDepth);
+ }
+
+ Texture(Texture&&) noexcept = default;
+ Texture(const Texture&) = delete;
+ Texture& operator=(const Texture&) = delete;
+
+ private:
+ void MakeInstance(
+ const vk::ImageUsageFlags &usageFlags,
+ const vk::MemoryPropertyFlags &memoryflags,
+ vk::raii::Image &image,
+ vk::raii::DeviceMemory &memory
+ ) const {
+ image = context.device.handle.createImage({
+ .imageType = vk::ImageType::e2D,
+ .format = format,
+ .extent = {.width = width, .height = height, .depth = 1},
+ .mipLevels = 1,
+ .arrayLayers = 1,
+ .samples = vk::SampleCountFlagBits::e1,
+ .tiling = vk::ImageTiling::eOptimal,
+ .usage = usageFlags,
+ .sharingMode = vk::SharingMode::eExclusive,
+ .initialLayout = vk::ImageLayout::eUndefined,
+ });
+
+ if (
+ std::optional allocated = context.AllocateImageMemory(
+ handle,
+ memoryflags
+ );
+ allocated.has_value()
+ ) {
+ memory = std::move(allocated).value();
+ handle.bindMemory(memory, 0);
+ }
+ }
+
+ void MakeView(const vk::ImageAspectFlags &flags) {
+ view = context.device.handle.createImageView({
+ .image = handle,
+ .viewType = vk::ImageViewType::e2D,
+ .format = format,
+ .subresourceRange = {
+ .aspectMask = flags,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1
+ }
+ });
+ }
+ };
+
+ template
+ class Uniform {
+ public:
+ std::vector> buffers;
+ explicit Uniform(const Context &context, std::uint32_t maxFramesInFlight) {
+ buffers.reserve(maxFramesInFlight);
+ for (std::uint32_t i = 0; i < maxFramesInFlight; i++) {
+ buffers.push_back(Buffer(context, 1));
+ }
+ }
+ };
+
+}
\ No newline at end of file
diff --git a/renderers/vulkan/modules/factory.cppm b/renderers/vulkan/modules/factory.cppm
new file mode 100644
index 0000000..a578a13
--- /dev/null
+++ b/renderers/vulkan/modules/factory.cppm
@@ -0,0 +1,30 @@
+/**
+ * Factory partition module is responsible for construction of render objects
+ */
+
+export module vulkan_render:factory;
+import :hardware;
+import :resources;
+
+export namespace vulkan_render {
+
+ class ResourcesFactory {
+ public:
+ /**
+ * Initialize frames swapchain
+ * @param context Vulkan context with initialized device and surface capabilities.
+ * @param surface surface origin. Extent will be clamped with frames size.
+ */
+ static Swapchain CreateSwapchain(const Context &context,Surface &surface);
+
+ private:
+ /**
+ * Initialize images and views for swapchain frames
+ * @param context Vulkan context with ready device.
+ * @param swapchain frames target with initialized swapchain origin.
+ * @param surface surface origin.
+ */
+ static void SetupFrames(const Context &context, Swapchain &swapchain, const Surface &surface);
+ };
+
+}
\ No newline at end of file
diff --git a/renderers/vulkan/modules/hardware.cppm b/renderers/vulkan/modules/hardware.cppm
new file mode 100644
index 0000000..474c2e0
--- /dev/null
+++ b/renderers/vulkan/modules/hardware.cppm
@@ -0,0 +1,195 @@
+/**
+ * Hardware partition module is responsible for any Vulkan operations dependent on
+ * hardware capabilities including devices and extensions initialization, memory allocations and so on.
+ */
+
+export module vulkan_render:hardware;
+import :resources;
+import std;
+import vulkan_hpp;
+
+export namespace vulkan_render {
+
+ struct PhysicalDeviceCapabilities {
+ vk::PhysicalDeviceProperties properties;
+
+ using Features1 = vk::PhysicalDeviceFeatures2;
+ using Features12 = vk::PhysicalDeviceVulkan12Features;
+ using Features13 = vk::PhysicalDeviceVulkan13Features;
+ using Features = vk::StructureChain;
+
+ Features features;
+ Features enabledFeatures;
+
+ Features1& GetFeatures1() { return features.get(); }
+ Features12& GetFeatures12() { return features.get(); }
+ Features13& GetFeatures13() { return features.get(); }
+ Features1& GetEnabledFeatures1() { return enabledFeatures.get(); }
+ Features12& GetEnabledFeatures12() { return enabledFeatures.get(); }
+ Features13& GetEnabledFeatures13() { return enabledFeatures.get(); }
+ };
+
+ /**
+ * Priority scoring rules to select one physical device among available.
+ */
+ struct PhysicalDevicePriorities {
+ struct GPU {
+ std::uint32_t DISCRETE = 1000;
+ std::uint32_t INTEGRATED = 1;
+ std::uint32_t VIRTUAL = 1;
+ std::uint32_t CPU = 0;
+ std::uint32_t OTHER = 0;
+ } gpu;
+
+ struct QueueFamilies {
+ std::uint32_t PRESENT = 500;
+ } queueFamilies;
+
+ struct SurfaceFormats {
+ std::uint32_t B8G8R8_SRGB = 10;
+ } surfaceFormats;
+
+ struct PresentModes {
+ std::uint32_t FIFO_RELAXED = 10;
+ } presentModes;
+ };
+
+ struct SurfaceCapabilities {
+ vk::SurfaceCapabilitiesKHR capabilities;
+ std::vector availableFormats;
+ std::vector availablePresentModes;
+ };
+
+}
+
+export namespace vulkan_render {
+ /**
+ * Vulkan objects associated with physical device.
+ */
+ struct PhysicalDevice {
+ /**
+ * Ids of queue families by their usage.
+ */
+ struct QueueFamilies {
+ static constexpr std::uint32_t NO_VALUE = std::numeric_limits::max();
+ std::uint32_t transfer = NO_VALUE;
+ std::uint32_t graphics = NO_VALUE;
+ std::uint32_t present = NO_VALUE;
+ } queueFamilies;
+
+ struct Memory {
+ vk::PhysicalDeviceMemoryProperties properties;
+ } memory;
+
+ vk::raii::PhysicalDevice handle = nullptr;
+ };
+
+ /**
+ * Vulkan objects associated with logical device.
+ */
+ struct LogicalDevice {
+ vk::raii::Device handle = nullptr;
+
+ static constexpr float DEFAULT_QUEUE_PRIORITY = 1.0f;
+ vk::raii::Queue transfer = nullptr;
+ vk::raii::Queue graphics = nullptr;
+ vk::raii::Queue present = nullptr;
+ };
+
+}
+
+export namespace vulkan_render {
+
+ /**
+ * Vulkan device and hardware context.
+ * @warning Physical and logical devices need to be defined either directly
+ * or using vulkan_render::Context::SetupPhysicalDevice and vulkan_render::Context::SetupLogicalDevice
+ * before context usage.
+ */
+ class Context {
+ public:
+ vk::raii::Context context;
+ vk::raii::Instance vulkan;
+
+ LogicalDevice device {};
+ PhysicalDevice physicalDevice {};
+
+ /// Loaded on physical device selection in vulkan_render::Context::SetupPhysicalDevice.
+ PhysicalDeviceCapabilities physicalDeviceCapabilities;
+ /// Loaded on physical device selection in vulkan_render::Context::SetupPhysicalDevice.
+ SurfaceCapabilities surfaceCapabilities;
+
+ /**
+ * Initialize VkInstance.
+ * @param appName application name to register in Vulkan.
+ */
+ explicit Context(const char* appName);
+ /**
+ * Initialize VkInstance.
+ * @param instanceInfo custom Vulkan configuration.
+ */
+ explicit Context(const vk::InstanceCreateInfo &instanceInfo);
+ /**
+ * Initialize VkInstance.
+ * @param appName application name to register in Vulkan.
+ * @param extensions list of vulkan enabled extensions labels.
+ * @param layers list of vulkan enabled layers labels.
+ */
+ Context(const char* appName, const std::vector &extensions, const std::vector &layers);
+
+ /**
+ * Select available physical device by priority.
+ *
+ * @note Updates vulkan_render::Context::physicalDevice and vulkan_render::Context::physicalDeviceCapabilities
+ * if at least one device is available, otherwise no effect.
+ *
+ * @param surface instance to be capable with.
+ * @param priorities configuration of physical device selection rules.
+ * @param[out] selectedFormat surface format of selected physical device. Undefined if no device was selected.
+ * @param[out] selectedPresentMode present mode of selected physical device. Undefined if no device was selected.
+ */
+ void SetupPhysicalDevice(
+ const Surface &surface,
+ const PhysicalDevicePriorities &priorities,
+ vk::SurfaceFormatKHR &selectedFormat,
+ vk::PresentModeKHR &selectedPresentMode
+ );
+ /**
+ * Initialize logical device with its available queues.
+ * @param extensions list of enabled device extensions.
+ */
+ void SetupLogicalDevice(const std::vector &extensions);
+
+ /**
+ * Find memory type available on device for specified requirements.
+ * @return id of memory type or std::nullopt if no suitable memory found.
+ */
+ std::optional FindMemoryType(
+ const vk::MemoryRequirements &requirements,
+ const vk::MemoryPropertyFlags &flags
+ );
+
+ std::optional AllocateMemory(
+ const vk::MemoryRequirements2 &memoryRequirements,
+ const vk::MemoryPropertyFlags &memoryFlags
+ ) const;
+
+ std::optional AllocateBufferMemory(
+ const vk::raii::Buffer &buffer,
+ const vk::MemoryPropertyFlags &memoryFlags
+ ) const;
+
+ std::optional AllocateImageMemory(
+ const vk::raii::Image &image,
+ const vk::MemoryPropertyFlags &memoryFlags
+ ) const;
+
+ private:
+ vk::raii::Instance MakeInstance(
+ const char *appName,
+ const std::vector &extensions,
+ const std::vector &layers
+ ) const;
+ };
+
+}
diff --git a/renderers/vulkan/modules/resources.cppm b/renderers/vulkan/modules/resources.cppm
new file mode 100644
index 0000000..ec705a9
--- /dev/null
+++ b/renderers/vulkan/modules/resources.cppm
@@ -0,0 +1,38 @@
+/**
+ * Resources partition module is responsible for definition of
+ * Vulkan resource objects including images, surface and so on.
+ */
+
+export module vulkan_render:resources;
+import std;
+import vulkan_hpp;
+
+export namespace vulkan_render {
+ /**
+ * Vulkan objects associated with surface.
+ * @note handle needs to be set manually according your window context.
+ */
+ struct Surface {
+ vk::raii::SurfaceKHR handle = nullptr;
+ vk::SurfaceFormatKHR format;
+ vk::PresentModeKHR presentMode;
+ vk::Extent2D extent;
+ };
+
+ /**
+ * Vulkan objects associated with frames swapchain.
+ * @note Can be created with vulkan_render::ResourcesFactory::CreateSwapchain.
+ */
+ struct Swapchain {
+ vk::raii::SwapchainKHR handle;
+
+ /**
+ * Images used as targets to render frames.
+ */
+ std::vector frames;
+ /**
+ * Views describing how the swapchain frames are accessed during rendering.
+ */
+ std::vector frameViews;
+ };
+}
\ No newline at end of file
diff --git a/renderers/vulkan/modules/vulkan_render.cppm b/renderers/vulkan/modules/vulkan_render.cppm
new file mode 100644
index 0000000..db32b5b
--- /dev/null
+++ b/renderers/vulkan/modules/vulkan_render.cppm
@@ -0,0 +1,8 @@
+export module vulkan_render;
+export import vulkan_hpp;
+export import :hardware;
+export import :workflow;
+export import :control;
+export import :resources;
+export import :data;
+export import :factory;
diff --git a/renderers/vulkan/modules/workflow.cppm b/renderers/vulkan/modules/workflow.cppm
new file mode 100644
index 0000000..b255913
--- /dev/null
+++ b/renderers/vulkan/modules/workflow.cppm
@@ -0,0 +1,157 @@
+/**
+ * Workflow partition module is responsible for configuration of rendering stages
+ * including pipelines, render passes and so on.
+ */
+
+export module vulkan_render:workflow;
+import :hardware;
+import :resources;
+import :data;
+import std;
+import vulkan_hpp;
+
+export namespace vulkan_render {
+ using DescriptorBindings = std::vector;
+ using DescriptorCounts = std::unordered_map;
+
+ /**
+ * Content of SPIR-V shaders binary data for a single pipeline.
+ */
+ struct ShaderProgram {
+ std::span vertex;
+ std::span fragment;
+
+ /**
+ * Name of vertex shader entry point.
+ */
+ const char* vertexMainFun = "main";
+ /**
+ * Name of fragment shader entry point.
+ */
+ const char* fragmentMainFun = "main";
+ };
+
+ struct ShaderAttributeFormat {
+ static constexpr vk::Format FLOAT = vk::Format::eR32Sfloat;
+ static constexpr vk::Format VEC2 = vk::Format::eR32G32Sfloat;
+ static constexpr vk::Format VEC3 = vk::Format::eR32G32B32Sfloat;
+ static constexpr vk::Format VEC4 = vk::Format::eR32G32B32A32Sfloat;
+
+ ShaderAttributeFormat() = delete;
+ };
+
+ /**
+ * Shaders input attributes binding.
+ */
+ struct ShaderBindings {
+ vk::VertexInputBindingDescription vertexBinding;
+ std::uint32_t vertexAttributesCount;
+ vk::VertexInputAttributeDescription vertexAttributes;
+ };
+
+ /**
+ * Set of descriptions needed for Vulkan pipeline initialization.
+ */
+ struct PipelineConfig {
+ vk::PipelineVertexInputStateCreateInfo vertexInputInfo;
+ vk::PipelineInputAssemblyStateCreateInfo inputAssemblyInfo;
+ vk::PipelineViewportStateCreateInfo viewportStateInfo;
+ vk::PipelineRasterizationStateCreateInfo rasterizerInfo;
+ vk::PipelineMultisampleStateCreateInfo multisamplingInfo;
+ vk::PipelineDepthStencilStateCreateInfo depthStencilInfo;
+ vk::PipelineColorBlendStateCreateInfo colorBlendInfo;
+ vk::PipelineDynamicStateCreateInfo dynamicStateInfo;
+ vk::DescriptorSetLayoutCreateInfo descriptorsInfo;
+
+ // SRC - new color, DST - old color
+ std::vector colorBlendAttachmentStates;
+ std::vector dynamicStates;
+ std::vector descriptors;
+
+ vk::Viewport defaultViewport;
+ vk::Rect2D defaultScissors;
+ vk::PushConstantRange pushConstantRange;
+ };
+
+ struct DescriptorInfo {
+ std::variant<
+ vk::DescriptorBufferInfo,
+ vk::DescriptorImageInfo
+ > info;
+ std::uint32_t bindingPosition;
+ vk::DescriptorType type;
+ };
+}
+
+export namespace vulkan_render {
+ /**
+ * Vulkan pipeline constructed on context, shaders and descriptors.
+ */
+ class Pipeline {
+ public:
+ const Context &context;
+ const Surface &surface;
+ const Swapchain &swapchain;
+ const std::uint32_t maxFramesInFlight;
+ DescriptorCounts descriptorCounts;
+
+ vk::raii::DescriptorSetLayout descriptorsLayout;
+ vk::raii::RenderPass renderPass;
+ vk::raii::PipelineLayout layout;
+ vk::raii::Pipeline handle;
+
+ vk::Viewport viewport;
+ vk::Rect2D scissors;
+
+ vk::raii::DescriptorPool descriptorPool;
+ vk::raii::DescriptorSets descriptorSets;
+
+ std::vector framebuffers;
+
+ /**
+ * Initialize Pipeline on shaders and their input.
+ * @param context Vulkan context with ready device.
+ * @param surface
+ * @param swapchain swapchain frame images used to allocate descriptors.
+ * @param config pipeline configuration.
+ * @param shaderProgram SPIR-V shaders binaries.
+ * @param maxFramesInFlight max number of frames in flight used to allocate descriptors .
+ */
+ Pipeline(
+ const Context &context,
+ const Surface &surface,
+ const Swapchain &swapchain,
+ const Texture &depth,
+ const PipelineConfig &config,
+ const ShaderProgram &shaderProgram,
+ std::uint32_t maxFramesInFlight
+ );
+
+ void UpdateDescriptors(const std::vector &descriptors, std::uint32_t frame);
+
+ /**
+ * Construct default pipeline configuration for given shaders input layout.
+ * @param surface surface origin.
+ * @param bindings shaders input attributes binding definition.
+ * @param descriptors descriptors binding definition (e.g. uniforms).
+ * @param pushConstants definition for small performant data set.
+ * @return default pipeline configuration.
+ */
+ static PipelineConfig MakeConfig(
+ const Surface &surface,
+ const ShaderBindings &bindings,
+ const DescriptorBindings &descriptors,
+ const vk::PushConstantRange& pushConstants
+ );
+
+ private:
+ vk::raii::DescriptorSetLayout MakeDescriptorsLayout(const PipelineConfig &config) const;
+ vk::raii::PipelineLayout MakeLayout(const PipelineConfig &config) const;
+ vk::raii::Pipeline MakeInstance(const PipelineConfig &config, const ShaderProgram &shaderProgram) const;
+ vk::raii::DescriptorPool MakeDescriptorPool(const DescriptorBindings &descriptors);
+ vk::raii::DescriptorSets MakeDescriptorSets() const;
+ vk::raii::RenderPass MakerRenderPass() const;
+ void MakeFramebuffers(const Texture &depth);
+ };
+
+}
\ No newline at end of file
diff --git a/renderers/vulkan/src/VulkanRenderer.cpp b/renderers/vulkan/src/VulkanRenderer.cpp
new file mode 100644
index 0000000..09f9961
--- /dev/null
+++ b/renderers/vulkan/src/VulkanRenderer.cpp
@@ -0,0 +1,356 @@
+module;
+
+#define GLFW_INCLUDE_VULKAN
+#include
+#include
+#include
+
+module VulkanRender;
+import utils;
+
+ee_vulkan::VulkanRenderer::VulkanRenderer(GLFWwindow *window) : IRenderer(window) {
+ std::uint32_t glfwExtensionsCount;
+ const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionsCount);
+
+ context.emplace(
+ "EtrediaEmpires",
+ std::vector(glfwExtensions, glfwExtensions + glfwExtensionsCount),
+ std::vector{"VK_LAYER_KHRONOS_validation"}
+ );
+
+ VkSurfaceKHR vulkanSurface;
+ glfwCreateWindowSurface(*context->vulkan, window, nullptr, &vulkanSurface);
+ surface.handle = vk::raii::SurfaceKHR(context->vulkan, vulkanSurface);
+ surface.extent = {
+ .width = static_cast(screenWidth),
+ .height = static_cast(screenHeight)
+ };
+
+ context->SetupPhysicalDevice(surface, {}, surface.format, surface.presentMode);
+ context->SetupLogicalDevice({vk::KHRSwapchainExtensionName});
+
+ swapchain.emplace(
+ vulkan_render::ResourcesFactory::CreateSwapchain(*context, surface)
+ );
+ renderCommands.emplace(*context, MAX_FRAMES_IN_FLIGHT);
+ syncManager.emplace(*context, MAX_FRAMES_IN_FLIGHT, TIMEOUT);
+
+ LOG(INFO, "Renderer created");
+}
+
+ee_vulkan::VulkanRenderer::~VulkanRenderer() {}
+
+void ee_vulkan::VulkanRenderer::Init() {
+ depth.emplace(*context, surface);
+
+ LoadProvincesMaterial();
+
+ viewProjectionUniform.emplace(*context, MAX_FRAMES_IN_FLIGHT);
+
+ LoadProvinces();
+
+ isDrawing = true;
+}
+
+void ee_vulkan::VulkanRenderer::Shutdown() {
+ context->device.handle.waitIdle();
+}
+
+void ee_vulkan::VulkanRenderer::Clear() {}
+
+void ee_vulkan::VulkanRenderer::DrawMap() {
+ if (!isDrawing) return;
+
+ syncManager->WaitInFlightFence(currentFrame);
+
+ std::uint32_t imageIndex = 0;
+ switch (vk::Result result = syncManager->AcquireNextImage(*swapchain, currentFrame, imageIndex)) {
+ case vk::Result::eSuccess:
+ break;
+
+ case vk::Result::eErrorOutOfDateKHR:
+ case vk::Result::eSuboptimalKHR:
+ LOG(WARNING, "Swapchain needs to be recreated");
+ RecreateSwapchain();
+ return;
+
+ default:
+ LOG(ERROR, "Failed to get next image index. Code: %d", result);
+ break;
+ }
+
+ UpdateData(currentFrame);
+
+ syncManager->WaitImageInFlight(currentFrame, imageIndex);
+
+ const vulkan_render::Command &renderCommand = renderCommands->graphicsBuffers[currentFrame];
+ renderCommand.handle.reset();
+ renderCommand.Begin();
+
+ std::array clears = {DrawConfig::CLEAR_COLOR, DrawConfig::CLEAR_DEPTH};
+ renderCommand.handle.beginRenderPass({
+ .renderPass = provincesPipeline->renderPass,
+ .framebuffer = provincesPipeline->framebuffers[imageIndex],
+ .renderArea = {
+ .offset = {.x = 0, .y = 0},
+ .extent = surface.extent
+ },
+ .clearValueCount = static_cast(clears.size()),
+ .pClearValues = clears.data()
+ }, vk::SubpassContents::eInline);
+
+ renderCommand.handle.bindPipeline(vk::PipelineBindPoint::eGraphics, provincesPipeline->handle);
+ renderCommand.handle.setViewport(0, {provincesPipeline->viewport});
+ renderCommand.handle.setScissor(0, {provincesPipeline->scissors});
+
+ renderCommand.handle.bindVertexBuffers(
+ 0,
+ {provinces.vertices->handle},
+ {0}
+ );
+ renderCommand.handle.bindIndexBuffer(
+ provinces.indices->handle,
+ 0,
+ vk::IndexType::eUint32
+ );
+
+ renderCommand.handle.bindDescriptorSets(
+ vk::PipelineBindPoint::eGraphics,
+ provincesPipeline->layout,
+ 0,
+ {provincesPipeline->descriptorSets[currentFrame]},
+ {}
+ );
+
+ renderCommand.handle.drawIndexedIndirect(
+ provinces.indirectCommands->handle,
+ 0,
+ provinces.indirectCommands->content.size(),
+ provinces.indirectCommands->elementSize
+ );
+
+ renderCommand.handle.endRenderPass();
+ renderCommand.handle.end();
+
+ context->device.handle.resetFences({syncManager->inFlightFences[currentFrame]});
+
+ vk::CommandBufferSubmitInfo cmdSubmitInfo {
+ .commandBuffer = renderCommand.handle
+ };
+ vk::SemaphoreSubmitInfo waitSemaphoreInfo {
+ .semaphore = syncManager->imageAvailable[currentFrame],
+ .stageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput
+ };
+ vk::SemaphoreSubmitInfo signalSemaphoreInfo {
+ .semaphore = syncManager->renderFinished[currentFrame],
+ .stageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput
+ };
+ const vk::SubmitInfo2 submitInfo {
+ .waitSemaphoreInfoCount = 1,
+ .pWaitSemaphoreInfos = &waitSemaphoreInfo,
+ .commandBufferInfoCount = 1,
+ .pCommandBufferInfos = &cmdSubmitInfo,
+ .signalSemaphoreInfoCount = 1,
+ .pSignalSemaphoreInfos = &signalSemaphoreInfo,
+ };
+ context->device.graphics.submit2({submitInfo}, syncManager->inFlightFences[currentFrame]);
+
+ vk::SwapchainKHR swapchains[] = {swapchain->handle};
+ switch (
+ vk::Result result = context->device.present.presentKHR({
+ .waitSemaphoreCount = 1,
+ .pWaitSemaphores = &signalSemaphoreInfo.semaphore,
+ .swapchainCount = 1,
+ .pSwapchains = swapchains,
+ .pImageIndices = &imageIndex
+ })
+ ) {
+ case vk::Result::eSuccess:
+ break;
+
+ case vk::Result::eErrorOutOfDateKHR:
+ case vk::Result::eSuboptimalKHR:
+ LOG(WARNING, "Swapchain needs to be recreated");
+ RecreateSwapchain();
+ return;
+
+ default:
+ LOG(ERROR, "Failed to get next image index. Code: %d", result);
+ break;
+ }
+
+ currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
+}
+
+void ee_vulkan::VulkanRenderer::DrawUI() {}
+
+void ee_vulkan::VulkanRenderer::ResizeWindow(const int width, const int height) {
+ IRenderer::ResizeWindow(width, height);
+ surface.extent = {
+ .width = static_cast(screenWidth),
+ .height = static_cast(screenHeight)
+ };
+}
+
+void ee_vulkan::VulkanRenderer::ApplyCamera() {
+ viewProjection[1][1] *= -1;
+ viewProjectionInversed[1][1] *= -1;
+}
+
+void ee_vulkan::VulkanRenderer::UpdateProvinces(const std::size_t offset, const std::vector &data) {
+ const std::size_t elementOffset = offset / provinces.storage->elementSize;
+ std::ranges::copy(data, provinces.storage->content.data() + elementOffset);
+ renderCommands->SubmitStagingBuffer(*provinces.storage);
+}
+
+void ee_vulkan::VulkanRenderer::RecreateSwapchain() {
+ // We should not render with minimized window
+ if (screenWidth == 0 || screenHeight == 0)
+ {
+ isDrawing = false;
+ return;
+ }
+ isDrawing = true;
+
+ context->device.handle.waitIdle();
+ swapchain = vulkan_render::ResourcesFactory::CreateSwapchain(*context, surface);
+ depth.emplace(*context, surface);
+ LoadProvincesMaterial();
+}
+
+void ee_vulkan::VulkanRenderer::LoadProvincesMaterial() {
+ constexpr vulkan_render::ShaderBindings shaderBindings {
+ .vertexBinding = {
+ .binding = 0,
+ .stride = sizeof(glm::vec2),
+ .inputRate = vk::VertexInputRate::eVertex
+ },
+ .vertexAttributesCount = 1,
+ .vertexAttributes = {
+ .location = 0,
+ .binding = 0,
+ .format = vulkan_render::ShaderAttributeFormat::VEC2,
+ .offset = 0
+ }
+ };
+
+ const vk::DescriptorSetLayoutBinding viewProjection {
+ .binding = 0,
+ .descriptorType = vk::DescriptorType::eUniformBuffer,
+ .descriptorCount = 1,
+ .stageFlags = vk::ShaderStageFlagBits::eVertex,
+ .pImmutableSamplers = nullptr
+ };
+ const vk::DescriptorSetLayoutBinding provincesSSBO {
+ .binding = 1,
+ .descriptorType = vk::DescriptorType::eStorageBuffer,
+ .descriptorCount = 1,
+ .stageFlags = vk::Flags(vk::ShaderStageFlagBits::eVertex) | vk::ShaderStageFlagBits::eFragment,
+ .pImmutableSamplers = nullptr
+ };
+
+ const vk::PushConstantRange model {
+ .stageFlags = vk::ShaderStageFlagBits::eVertex,
+ .offset = 0,
+ .size = sizeof(glm::mat4)
+ };
+
+ vulkan_render::PipelineConfig provincesMaterial = vulkan_render::Pipeline::MakeConfig(
+ surface,
+ shaderBindings,
+ {viewProjection, provincesSSBO},
+ model
+ );
+ vulkan_render::ShaderProgram provincesShader {
+ .vertex = {reinterpret_cast(shaders.vertProvinces->content), shaders.vertProvinces->size},
+ .fragment = {reinterpret_cast(shaders.fragProvinces->content), shaders.fragProvinces->size}
+ };
+
+ provincesPipeline.emplace(
+ *context,
+ surface,
+ *swapchain,
+ *depth,
+ provincesMaterial,
+ provincesShader,
+ MAX_FRAMES_IN_FLIGHT
+ );
+}
+
+void ee_vulkan::VulkanRenderer::LoadProvinces() {
+ std::vector provincesData;
+ std::vector verticesData;
+ std::vector indicesData;
+ std::vector indirectCommandsData;
+
+ std::size_t regionOffset = 0;
+ std::uint32_t indicesOffset = 0;
+ std::int32_t verticesOffset = 0;
+ for (auto& [region, provs] : mapMesh->regions) {
+ for (auto&[vertices, indices] : provs) {
+ indirectCommandsData.push_back({
+ .indexCount = static_cast(indices.size()),
+ .instanceCount = 1,
+ .firstIndex = indicesOffset,
+ .vertexOffset = verticesOffset,
+ .firstInstance = static_cast(provincesData.size())
+ });
+
+ verticesData.append_range(vertices);
+ indicesData.append_range(indices);
+
+ ee_engine::ProvinceSSBO data{
+ .regionId = region,
+ .color = 0xFF0000
+ };
+ provincesData.push_back(data);
+
+ indicesOffset += indices.size();
+ verticesOffset += vertices.size();
+ }
+
+ regionsMapping[region] = {.offset = regionOffset, .provincesCount = provs.size()};
+ regionOffset += provs.size() * sizeof(ee_engine::ProvinceSSBO);
+ }
+
+ provinces.vertices.emplace(*context, verticesData.size());
+ provinces.indices.emplace(*context, indicesData.size());
+ provinces.storage.emplace(*context, provincesData.size());
+ provinces.indirectCommands.emplace(*context, indirectCommandsData.size());
+
+ std::ranges::copy(verticesData, provinces.vertices->content.data());
+ std::ranges::copy(indicesData, provinces.indices->content.data());
+ std::ranges::copy(provincesData, provinces.storage->content.data());
+ std::ranges::copy(indirectCommandsData, provinces.indirectCommands->content.data());
+
+ renderCommands->SubmitStagingBuffer(*provinces.vertices);
+ renderCommands->SubmitStagingBuffer(*provinces.indices);
+ renderCommands->SubmitStagingBuffer(*provinces.storage);
+ renderCommands->SubmitStagingBuffer(*provinces.indirectCommands);
+}
+
+void ee_vulkan::VulkanRenderer::UpdateData(const std::uint32_t frame) {
+ std::span &vpBuffer = viewProjectionUniform->buffers[frame].content;
+ vpBuffer[0] = viewProjection;
+
+ provincesPipeline->UpdateDescriptors({
+ {
+ .info = vk::DescriptorBufferInfo{
+ .buffer = viewProjectionUniform->buffers[frame].handle,
+ .offset = 0,
+ .range = viewProjectionUniform->buffers[frame].memorySize
+ },
+ .bindingPosition = 0,
+ .type = vk::DescriptorType::eUniformBuffer
+ },
+ {
+ .info = vk::DescriptorBufferInfo{
+ .buffer = provinces.storage->handle,
+ .offset = 0,
+ .range = provinces.storage->memorySize
+ },
+ .bindingPosition = 1,
+ .type = vk::DescriptorType::eStorageBuffer
+ }
+ }, frame);
+}
diff --git a/renderers/vulkan/src/control.cpp b/renderers/vulkan/src/control.cpp
new file mode 100644
index 0000000..0772f50
--- /dev/null
+++ b/renderers/vulkan/src/control.cpp
@@ -0,0 +1,155 @@
+module vulkan_render;
+import utils;
+
+vulkan_render::Command::Command(const Context &context, vk::raii::CommandBuffer &&buffer) :
+ context(context),
+ handle(std::move(buffer)) {}
+
+vulkan_render::Command::Command(const Context &context, const vk::raii::CommandPool &pool) :
+ context(context),
+ handle(std::move(
+ context.device.handle.allocateCommandBuffers({
+ .commandPool = pool,
+ .level = vk::CommandBufferLevel::ePrimary, //TODO: reuse some commands using SECONDARY
+ .commandBufferCount = 1
+ }).front()
+ )) {}
+
+void vulkan_render::Command::Begin() const {
+ handle.begin({
+ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit
+ });
+}
+
+void vulkan_render::Command::Submit(const vk::raii::Queue &queue) const {
+ handle.end();
+
+ vk::CommandBufferSubmitInfo cmdSubmitInfo {
+ .commandBuffer = handle
+ };
+
+ const vk::SubmitInfo2 submitInfo {
+ .commandBufferInfoCount = 1,
+ .pCommandBufferInfos = &cmdSubmitInfo,
+
+ };
+
+ const vk::raii::Fence fence = context.device.handle.createFence({});
+ queue.submit2({submitInfo}, fence);
+ if (
+ vk::Result result = context.device.handle.waitForFences({fence}, true, std::numeric_limits::max());
+ result != vk::Result::eSuccess
+ ) {
+ LOG(WARNING, "Command buffer submit timeout. Code: %d", result);
+ }
+}
+
+vulkan_render::RenderCommands::RenderCommands(const Context &context, const std::uint32_t maxFramesInFlight) :
+ graphicsPool(MakePool(context, context.physicalDevice.queueFamilies.graphics)),
+ transferPool(MakePool(context, context.physicalDevice.queueFamilies.transfer)) {
+
+ graphicsBuffers = MakeBuffers(context, graphicsPool, maxFramesInFlight);
+ transferBuffers = MakeBuffers(context, transferPool, maxFramesInFlight);
+
+ LOG(INFO, "Command buffers initialized.");
+}
+
+void vulkan_render::RenderCommands::SubmitStagingBuffer(const BaseBuffer &buffer) const {
+ const Command cmd(buffer.context, transferPool);
+ cmd.Begin();
+
+ vk::BufferCopy2 copyRegion {
+ .srcOffset = 0,
+ .dstOffset = 0,
+ .size = buffer.memorySize
+ };
+
+ cmd.handle.copyBuffer2({
+ .srcBuffer = buffer.stagingHandle,
+ .dstBuffer = buffer.handle,
+ .regionCount = 1,
+ .pRegions = ©Region
+ });
+
+ cmd.Submit(buffer.context.device.transfer);
+}
+
+vk::raii::CommandPool vulkan_render::RenderCommands::MakePool(const Context &context, const std::uint32_t queueFamily) {
+ return context.device.handle.createCommandPool({
+ .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
+ .queueFamilyIndex = queueFamily,
+ });
+}
+
+std::vector vulkan_render::RenderCommands::MakeBuffers(
+ const Context &context,
+ const vk::raii::CommandPool &pool,
+ const std::uint32_t size
+) {
+ std::vector buffers = context.device.handle.allocateCommandBuffers({
+ .commandPool = pool,
+ .level = vk::CommandBufferLevel::ePrimary, //TODO: reuse some commands using SECONDARY
+ .commandBufferCount = size
+ });
+
+ std::vector result;
+ result.reserve(buffers.size());
+ for (vk::raii::CommandBuffer &buffer : buffers) {
+ result.emplace_back(context, std::move(buffer));
+ }
+
+ return result;
+}
+
+vulkan_render::SyncManager::SyncManager(
+ const Context &context,
+ const std::uint32_t maxFramesInFlight,
+ const std::uint64_t timeout
+) :
+ context(context), TIMEOUT(timeout) {
+ imageAvailable.reserve(maxFramesInFlight);
+ renderFinished.reserve(maxFramesInFlight);
+ inFlightFences.reserve(maxFramesInFlight);
+ imagesInFlight.resize(maxFramesInFlight, nullptr);
+
+ for (std::uint32_t i = 0; i < maxFramesInFlight; i++) {
+ imageAvailable.push_back(context.device.handle.createSemaphore({}));
+ renderFinished.push_back(context.device.handle.createSemaphore({}));
+ inFlightFences.push_back(context.device.handle.createFence({.flags = vk::FenceCreateFlagBits::eSignaled}));
+ }
+
+ LOG(INFO, "Sync objects initialized.");
+}
+
+void vulkan_render::SyncManager::WaitInFlightFence(const std::size_t currentFrame) {
+ if (
+ vk::Result result = context.device.handle.waitForFences({inFlightFences[currentFrame]}, vk::True, TIMEOUT);
+ result != vk::Result::eSuccess
+ )
+ LOG(ERROR, "Waiting semaphore timeout. Code: %d", result);
+}
+void vulkan_render::SyncManager::WaitImageInFlight(const std::size_t currentFrame, const std::uint32_t imageIndex) {
+ if (imagesInFlight[imageIndex] == nullptr) return;
+ if (
+ vk::Result result = context.device.handle.waitForFences({imagesInFlight[imageIndex]}, vk::True, TIMEOUT);
+ result != vk::Result::eSuccess
+ )
+ LOG(ERROR, "Waiting semaphore timeout. Code: %d", result);
+ imagesInFlight[imageIndex] = inFlightFences[currentFrame];
+}
+
+vk::Result vulkan_render::SyncManager::AcquireNextImage(
+ const Swapchain &swapchain,
+ const std::size_t currentFrame,
+ std::uint32_t &imageIndex
+) const {
+ auto [result, newImageIndex] = context.device.handle.acquireNextImage2KHR({
+ .swapchain = swapchain.handle,
+ .timeout = TIMEOUT,
+ .semaphore = imageAvailable[currentFrame],
+ .deviceMask = 1
+ });
+
+ imageIndex = newImageIndex;
+ return result;
+}
\ No newline at end of file
diff --git a/renderers/vulkan/src/factory.cpp b/renderers/vulkan/src/factory.cpp
new file mode 100644
index 0000000..96c5077
--- /dev/null
+++ b/renderers/vulkan/src/factory.cpp
@@ -0,0 +1,87 @@
+module vulkan_render;
+import utils;
+
+vulkan_render::Swapchain vulkan_render::ResourcesFactory::CreateSwapchain(const Context &context, Surface &surface) {
+ // Clamp extent
+ surface.extent.width = std::max(
+ context.surfaceCapabilities.capabilities.minImageExtent.width,
+ (std::min)(context.surfaceCapabilities.capabilities.maxImageExtent.width, surface.extent.width)
+ );
+ surface.extent.height = std::max(
+ context.surfaceCapabilities.capabilities.minImageExtent.height,
+ (std::min)(context.surfaceCapabilities.capabilities.maxImageExtent.height, surface.extent.height)
+ );
+
+ std::uint32_t imagesCount = context.surfaceCapabilities.capabilities.minImageCount + 1;
+ // Clamp min images count
+ if (context.surfaceCapabilities.capabilities.maxImageCount > 0 && imagesCount > context.surfaceCapabilities.capabilities.maxImageCount)
+ imagesCount = context.surfaceCapabilities.capabilities.maxImageCount;
+
+ vk::SwapchainCreateInfoKHR swapchainCreateInfo {
+ .surface = surface.handle,
+ .minImageCount = imagesCount,
+ .imageFormat = surface.format.format,
+ .imageColorSpace = surface.format.colorSpace,
+ .imageExtent = surface.extent,
+ .imageArrayLayers = 1,
+ .imageUsage = vk::ImageUsageFlagBits::eColorAttachment,
+ .preTransform = context.surfaceCapabilities.capabilities.currentTransform,
+ .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque,
+ .presentMode = surface.presentMode,
+ .clipped = vk::True,
+ .oldSwapchain = nullptr
+ };
+
+ const std::vector queueFamiliesShare {context.physicalDevice.queueFamilies.graphics, context.physicalDevice.queueFamilies.present};
+ if (context.physicalDevice.queueFamilies.graphics != context.physicalDevice.queueFamilies.present)
+ {
+ swapchainCreateInfo.imageSharingMode = vk::SharingMode::eConcurrent;
+ swapchainCreateInfo.queueFamilyIndexCount = queueFamiliesShare.size();
+ swapchainCreateInfo.pQueueFamilyIndices = queueFamiliesShare.data();
+ }
+ else
+ swapchainCreateInfo.imageSharingMode = vk::SharingMode::eExclusive;
+
+ Swapchain swapchain {
+ .handle = context.device.handle.createSwapchainKHR(swapchainCreateInfo)
+ };
+ LOG(INFO, "Swapchain is created");
+
+ SetupFrames(context, swapchain, surface);
+
+ return swapchain;
+}
+
+void vulkan_render::ResourcesFactory::SetupFrames(const Context &context, Swapchain &swapchain, const Surface &surface) {
+ swapchain.frames.clear();
+ swapchain.frameViews.clear();
+
+ swapchain.frames = swapchain.handle.getImages();
+ LOG(INFO, "Frames are ready");
+
+ swapchain.frameViews.reserve(swapchain.frames.size());
+ for (size_t i = 0; i < swapchain.frames.size(); i++)
+ {
+ vk::ImageViewCreateInfo frameViewInfo {
+ .image = swapchain.frames[i],
+ .viewType = vk::ImageViewType::e2D,
+ .format = surface.format.format,
+ .components {
+ .r = vk::ComponentSwizzle::eIdentity,
+ .g = vk::ComponentSwizzle::eIdentity,
+ .b = vk::ComponentSwizzle::eIdentity,
+ .a = vk::ComponentSwizzle::eIdentity
+ },
+ .subresourceRange {
+ .aspectMask = vk::ImageAspectFlagBits::eColor,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1
+ }
+ };
+
+ swapchain.frameViews.push_back(context.device.handle.createImageView(frameViewInfo));
+ }
+ LOG(INFO, "Frame views are ready.");
+}
diff --git a/renderers/vulkan/src/hardware.cpp b/renderers/vulkan/src/hardware.cpp
new file mode 100644
index 0000000..c9d4ff3
--- /dev/null
+++ b/renderers/vulkan/src/hardware.cpp
@@ -0,0 +1,304 @@
+module vulkan_render;
+import utils;
+
+template using iterator = std::ranges::borrowed_iterator_t&>;
+
+vulkan_render::Context::Context(const char *appName)
+ : vulkan(MakeInstance(appName, {}, {})) {}
+vulkan_render::Context::Context(const vk::InstanceCreateInfo &instanceInfo)
+ : vulkan(vk::raii::Instance(context, instanceInfo)) {}
+
+vulkan_render::Context::Context(
+ const char *appName,
+ const std::vector &extensions,
+ const std::vector &layers
+)
+ : vulkan(MakeInstance(appName, extensions, layers)) {}
+
+void vulkan_render::Context::SetupPhysicalDevice(
+ const Surface &surface,
+ const PhysicalDevicePriorities &priorities,
+ vk::SurfaceFormatKHR& selectedFormat,
+ vk::PresentModeKHR& selectedPresentMode
+) {
+ // Find physical devices
+ const std::vector devices = vulkan.enumeratePhysicalDevices();
+ if (devices.size() == 0) {
+ LOG(CRITICAL, "No devices with Vulkan support found!");
+ return;
+ }
+ LOG(INFO, "Found %zu Vulkan compatible physical devices.", devices.size());
+
+ if (devices.empty())
+ {
+ LOG(CRITICAL, "No compatible physical devices found.");
+ }
+
+ // Filter and select device
+ std::uint32_t bestPriority = 0;
+ for (const vk::raii::PhysicalDevice &physicalDeviceVariant : devices)
+ {
+ std::uint32_t priority = 1;
+
+ // Check device features and properties
+ physicalDeviceCapabilities.features = physicalDeviceVariant.getFeatures2<
+ PhysicalDeviceCapabilities::Features1,
+ PhysicalDeviceCapabilities::Features12,
+ PhysicalDeviceCapabilities::Features13
+ >();
+
+ //TODO: provide features list to check and enable
+ const PhysicalDeviceCapabilities::Features1 &features1 = physicalDeviceCapabilities.GetFeatures1();
+ const PhysicalDeviceCapabilities::Features13 &features13 = physicalDeviceCapabilities.GetFeatures13();
+ if (
+ features1.features.wideLines != vk::True ||
+ features1.features.independentBlend != vk::True ||
+ features1.features.multiDrawIndirect != vk::True ||
+
+ features13.dynamicRendering != vk::True ||
+ features13.synchronization2 != vk::True ||
+ features13.maintenance4 != vk::True
+ ) {
+ continue;
+ }
+
+ PhysicalDeviceCapabilities::Features1 &enabledFeatures1 = physicalDeviceCapabilities.GetEnabledFeatures1();
+ PhysicalDeviceCapabilities::Features13 &enabledFeatures13 = physicalDeviceCapabilities.GetEnabledFeatures13();
+ enabledFeatures1.features.wideLines = vk::True;
+ enabledFeatures1.features.independentBlend = vk::True;
+ enabledFeatures1.features.multiDrawIndirect = vk::True;
+ enabledFeatures13.dynamicRendering = vk::True;
+ enabledFeatures13.synchronization2 = vk::True;
+ enabledFeatures13.maintenance4 = vk::True;
+
+ physicalDeviceCapabilities.properties = physicalDeviceVariant.getProperties();
+ if (physicalDeviceCapabilities.properties.apiVersion < vk::makeApiVersion(0, 1, 3, 0))
+ continue;
+
+ // Check queue families
+ std::vector queueFamiliesProperties = physicalDeviceVariant.getQueueFamilyProperties();
+
+ std::uint32_t i = 0;
+ PhysicalDevice::QueueFamilies queueFamiliesBuffer;
+ for (const vk::QueueFamilyProperties &familyProps : queueFamiliesProperties)
+ {
+ if (familyProps.queueFlags & vk::QueueFlagBits::eGraphics)
+ queueFamiliesBuffer.graphics = i;
+
+ if (
+ queueFamiliesBuffer.graphics != queueFamiliesBuffer.present ||
+ queueFamiliesBuffer.graphics == PhysicalDevice::QueueFamilies::NO_VALUE
+ )
+ {
+ if (physicalDeviceVariant.getSurfaceSupportKHR(i, surface.handle))
+ queueFamiliesBuffer.present = i;
+ }
+
+ i++;
+ }
+
+ if (
+ queueFamiliesBuffer.graphics == PhysicalDevice::QueueFamilies::NO_VALUE ||
+ queueFamiliesBuffer.present == PhysicalDevice::QueueFamilies::NO_VALUE
+ )
+ continue;
+ queueFamiliesBuffer.transfer = queueFamiliesBuffer.graphics;
+
+ // Check surface info
+ SurfaceCapabilities surfaceCapabilitiesBuffer {};
+
+ surfaceCapabilitiesBuffer.availableFormats = physicalDeviceVariant.getSurfaceFormatsKHR(surface.handle);
+ surfaceCapabilitiesBuffer.availablePresentModes = physicalDeviceVariant.getSurfacePresentModesKHR(surface.handle);
+
+ if (
+ surfaceCapabilitiesBuffer.availableFormats.empty() ||
+ surfaceCapabilitiesBuffer.availablePresentModes.empty()
+ )
+ continue;
+
+ // Rate and select physical device
+ if (queueFamiliesBuffer.graphics == queueFamiliesBuffer.present)
+ priority += priorities.queueFamilies.PRESENT;
+
+ switch (physicalDeviceCapabilities.properties.deviceType) {
+ case vk::PhysicalDeviceType::eDiscreteGpu:
+ priority += priorities.gpu.DISCRETE;
+ break;
+ case vk::PhysicalDeviceType::eIntegratedGpu:
+ priority += priorities.gpu.INTEGRATED;
+ break;
+ case vk::PhysicalDeviceType::eVirtualGpu:
+ priority += priorities.gpu.VIRTUAL;
+ break;
+ case vk::PhysicalDeviceType::eCpu:
+ priority += priorities.gpu.CPU;
+ break;
+ case vk::PhysicalDeviceType::eOther:
+ priority += priorities.gpu.OTHER;
+ break;
+ }
+
+ vk::SurfaceFormatKHR formatBuffer {
+ .format = vk::Format::eB8G8R8A8Unorm,
+ .colorSpace = vk::ColorSpaceKHR::eSrgbNonlinear
+ };
+ for (const vk::SurfaceFormatKHR &format : surfaceCapabilitiesBuffer.availableFormats)
+ {
+ if (format.format == vk::Format::eB8G8R8A8Srgb && format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear)
+ {
+ formatBuffer = format;
+ priority += priorities.surfaceFormats.B8G8R8_SRGB;
+ break;
+ }
+ }
+
+ vk::PresentModeKHR presentModeBuffer = vk::PresentModeKHR::eFifo;
+ for (const vk::PresentModeKHR &presentMode : surfaceCapabilitiesBuffer.availablePresentModes)
+ {
+ if (presentMode == vk::PresentModeKHR::eFifoRelaxed)
+ {
+ presentModeBuffer = presentMode;
+ priority += priorities.presentModes.FIFO_RELAXED;
+ break;
+ }
+ }
+
+ if (priority > bestPriority)
+ {
+ bestPriority = priority;
+ physicalDevice.handle = physicalDeviceVariant;
+ physicalDevice.queueFamilies = queueFamiliesBuffer;
+ surfaceCapabilities = surfaceCapabilitiesBuffer;
+ selectedFormat = formatBuffer;
+ selectedPresentMode = presentModeBuffer;
+ }
+ }
+
+ surfaceCapabilities.capabilities = physicalDevice.handle.getSurfaceCapabilitiesKHR(surface.handle);
+
+ LOG(INFO, "%hs is selected for render with %d priority score", physicalDeviceCapabilities.properties.deviceName, bestPriority);
+
+ physicalDevice.memory.properties = physicalDevice.handle.getMemoryProperties();
+}
+
+void vulkan_render::Context::SetupLogicalDevice(const std::vector &extensions) {
+ // Define queues
+ std::vector queuesCreateInfos;
+ const std::set uniqueQueueFamilies {
+ physicalDevice.queueFamilies.transfer,
+ physicalDevice.queueFamilies.graphics,
+ physicalDevice.queueFamilies.present
+ };
+
+ for (const std::uint32_t queueFamily : uniqueQueueFamilies)
+ {
+ vk::DeviceQueueCreateInfo queueCreateInfo {
+ .queueFamilyIndex = queueFamily,
+ .queueCount = 1,
+ .pQueuePriorities = &LogicalDevice::DEFAULT_QUEUE_PRIORITY
+ };
+
+ queuesCreateInfos.push_back(queueCreateInfo);
+ }
+
+ // Create logical device
+ const vk::DeviceCreateInfo deviceInfo {
+ .pNext = &physicalDeviceCapabilities.enabledFeatures.get(),
+ .queueCreateInfoCount = static_cast(queuesCreateInfos.size()),
+ .pQueueCreateInfos = queuesCreateInfos.data(),
+ .enabledExtensionCount = static_cast(extensions.size()),
+ .ppEnabledExtensionNames = extensions.data(),
+ };
+ device.handle = physicalDevice.handle.createDevice(deviceInfo);
+ LOG(INFO, "Logical device is created.");
+
+ // Save queues descriptor
+ device.transfer = device.handle.getQueue(physicalDevice.queueFamilies.transfer, 0);
+ device.graphics = device.handle.getQueue(physicalDevice.queueFamilies.graphics, 0);
+ device.present = device.handle.getQueue(physicalDevice.queueFamilies.present, 0);
+ LOG(INFO, "Queues registered.");
+}
+
+std::optional vulkan_render::Context::FindMemoryType(const vk::MemoryRequirements &requirements, const vk::MemoryPropertyFlags &flags) {
+ std::optional memoryType;
+
+ for (std::uint32_t i = 0; i < physicalDevice.memory.properties.memoryTypeCount; i++)
+ {
+ if (
+ requirements.memoryTypeBits & (1 << i) &&
+ (physicalDevice.memory.properties.memoryTypes[i].propertyFlags & flags) == flags
+ ) {
+ memoryType = i;
+ break;
+ }
+ }
+ if (!memoryType.has_value()) {
+ LOG(CRITICAL, "Failed to find GPU memory with requirements %d and flags %d", requirements.memoryTypeBits, flags);
+ }
+
+ return memoryType;
+}
+
+std::optional vulkan_render::Context::AllocateMemory(
+ const vk::MemoryRequirements2 &memoryRequirements,
+ const vk::MemoryPropertyFlags &memoryFlags
+) const {
+ std::optional memoryTypeId;
+ const vk::PhysicalDeviceMemoryProperties &memoryProps = physicalDevice.memory.properties;
+ for (std::uint32_t i = 0; i < memoryProps.memoryTypeCount; i++) {
+ if (
+ memoryRequirements.memoryRequirements.memoryTypeBits & (1 << i) &&
+ (memoryProps.memoryTypes[i].propertyFlags & memoryFlags) == memoryFlags
+ ) {
+ memoryTypeId = i;
+ break;
+ }
+ }
+ if (!memoryTypeId.has_value()) {
+ LOG(CRITICAL, "Failed to find suitable GPU memory. Flags: %d", memoryFlags);
+ return std::nullopt;
+ }
+
+ return device.handle.allocateMemory({
+ .allocationSize = memoryRequirements.memoryRequirements.size,
+ .memoryTypeIndex = memoryTypeId.value()
+ });
+}
+
+std::optional vulkan_render::Context::AllocateBufferMemory(
+ const vk::raii::Buffer &buffer,
+ const vk::MemoryPropertyFlags &memoryFlags
+) const {
+ return AllocateMemory(device.handle.getBufferMemoryRequirements2({.buffer = buffer}), memoryFlags);
+}
+
+std::optional vulkan_render::Context::AllocateImageMemory(
+ const vk::raii::Image &image,
+ const vk::MemoryPropertyFlags &memoryFlags
+) const {
+ return AllocateMemory(device.handle.getImageMemoryRequirements2({.image = image}), memoryFlags);
+}
+
+vk::raii::Instance vulkan_render::Context::MakeInstance(
+ const char *appName,
+ const std::vector &extensions,
+ const std::vector &layers
+) const {
+ const vk::ApplicationInfo appInfo {
+ .pApplicationName = appName,
+ .applicationVersion = vk::makeApiVersion(0, 1, 0, 0),
+ .pEngineName = "No Engine",
+ .engineVersion = vk::makeApiVersion(0, 1, 0, 0),
+ .apiVersion = vk::makeApiVersion(0, 1, 3, 0)
+ };
+
+ const vk::InstanceCreateInfo createInfo {
+ .pApplicationInfo = &appInfo,
+ .enabledLayerCount = static_cast(layers.size()),
+ .ppEnabledLayerNames = layers.data(),
+ .enabledExtensionCount = static_cast(extensions.size()),
+ .ppEnabledExtensionNames = extensions.data(),
+ };
+
+ return vk::raii::Instance(context, createInfo);
+}
diff --git a/renderers/vulkan/src/workflow.cpp b/renderers/vulkan/src/workflow.cpp
new file mode 100644
index 0000000..a924d1e
--- /dev/null
+++ b/renderers/vulkan/src/workflow.cpp
@@ -0,0 +1,361 @@
+module vulkan_render;
+import utils;
+
+vulkan_render::Pipeline::Pipeline(
+ const Context &context,
+ const Surface &surface,
+ const Swapchain &swapchain,
+ const Texture &depth,
+ const PipelineConfig &config,
+ const ShaderProgram &shaderProgram,
+ const std::uint32_t maxFramesInFlight
+) : context(context),
+ surface(surface),
+ swapchain(swapchain),
+ maxFramesInFlight(maxFramesInFlight),
+ descriptorsLayout(MakeDescriptorsLayout(config)),
+ layout(MakeLayout(config)),
+ renderPass(MakerRenderPass()),
+ handle(MakeInstance(config, shaderProgram)),
+ viewport(config.defaultViewport),
+ scissors(config.defaultScissors),
+ descriptorPool(MakeDescriptorPool(config.descriptors)),
+ descriptorSets(MakeDescriptorSets()) {
+ MakeFramebuffers(depth);
+ LOG(INFO, "Pipeline created");
+}
+
+void vulkan_render::Pipeline::UpdateDescriptors(
+ const std::vector &descriptors,
+ const std::uint32_t frame
+) {
+ std::vector writes;
+ writes.reserve(descriptors.size());
+
+ for (const auto &[info, bindingPosition, type] : descriptors) {
+ vk::WriteDescriptorSet write {
+ .dstSet = descriptorSets[frame],
+ .dstBinding = bindingPosition,
+ .dstArrayElement = 0,
+ .descriptorCount = 1,
+ .descriptorType = type,
+ };
+
+ switch (type) {
+ case vk::DescriptorType::eStorageBuffer:
+ case vk::DescriptorType::eUniformBuffer:
+ write.pBufferInfo = &std::get(info);
+ break;
+ case vk::DescriptorType::eCombinedImageSampler:
+ write.pImageInfo = &std::get(info);
+ break;
+
+ default:
+ break;
+ }
+
+ writes.push_back(write);
+ }
+
+ context.device.handle.updateDescriptorSets(writes, 0);
+}
+
+vulkan_render::PipelineConfig vulkan_render::Pipeline::MakeConfig(
+ const Surface &surface,
+ const ShaderBindings &bindings,
+ const DescriptorBindings &descriptors,
+ const vk::PushConstantRange &pushConstants
+) {
+ PipelineConfig config;
+
+ config.dynamicStates.push_back(vk::DynamicState::eViewport);
+ config.dynamicStates.push_back(vk::DynamicState::eScissor);
+
+ config.colorBlendAttachmentStates.push_back({
+ .blendEnable = vk::True,
+ .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha,
+ .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha,
+ .colorBlendOp = vk::BlendOp::eAdd,
+ .srcAlphaBlendFactor = vk::BlendFactor::eOne,
+ .dstAlphaBlendFactor = vk::BlendFactor::eZero,
+ .alphaBlendOp = vk::BlendOp::eAdd,
+ .colorWriteMask = vk::Flags(vk::ColorComponentFlagBits::eR) | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB
+ });
+
+ config.descriptors = descriptors;
+
+ config.vertexInputInfo = {
+ .vertexBindingDescriptionCount = 1,
+ .pVertexBindingDescriptions = &bindings.vertexBinding,
+ .vertexAttributeDescriptionCount = bindings.vertexAttributesCount,
+ .pVertexAttributeDescriptions = &bindings.vertexAttributes
+ };
+
+ config.inputAssemblyInfo = {
+ .topology = vk::PrimitiveTopology::eTriangleList,
+ .primitiveRestartEnable = vk::False
+ };
+
+ config.rasterizerInfo = {
+ .depthClampEnable = vk::False,
+ .rasterizerDiscardEnable = vk::False,
+ .polygonMode = vk::PolygonMode::eFill,
+ .cullMode = vk::CullModeFlagBits::eNone, //TODO: BACK_BIT
+ .frontFace = vk::FrontFace::eCounterClockwise,
+ .depthBiasEnable = vk::False,
+ .lineWidth = 1.0f,
+ };
+
+ config.multisamplingInfo = {
+ .rasterizationSamples = vk::SampleCountFlagBits::e1,
+ .sampleShadingEnable = vk::False,
+ };
+
+ config.depthStencilInfo = {
+ .depthTestEnable = vk::False,//TODO
+ .depthWriteEnable = vk::False,//TODO
+ .depthCompareOp = vk::CompareOp::eLess,
+ .depthBoundsTestEnable = vk::False,
+ .stencilTestEnable = vk::False,
+ .front = {},
+ .back = {},
+ .minDepthBounds = 0.0f,
+ .maxDepthBounds = 1.0f,
+ };
+
+ config.dynamicStateInfo = {
+ .dynamicStateCount = static_cast(config.dynamicStates.size()),
+ .pDynamicStates = config.dynamicStates.data()
+ };
+
+ config.descriptorsInfo = {
+ .bindingCount = static_cast(config.descriptors.size()),
+ .pBindings = config.descriptors.data()
+ };
+
+ config.pushConstantRange = pushConstants;
+
+ config.defaultViewport = {
+ .x = 0.0f,
+ .y = 0.0f,
+ .width = static_cast(surface.extent.width),
+ .height = static_cast(surface.extent.height),
+ .minDepth = 0.0f,
+ .maxDepth = 1.0f
+ };
+
+ config.defaultScissors = {
+ .offset = {.x = 0, .y = 0},
+ .extent = surface.extent
+ };
+
+ config.viewportStateInfo = {
+ .viewportCount = 1,
+ .pViewports = &config.defaultViewport,
+ .scissorCount = 1,
+ .pScissors = &config.defaultScissors
+ };
+
+ config.colorBlendInfo = {
+ .logicOpEnable = vk::False,
+ .attachmentCount = static_cast(config.colorBlendAttachmentStates.size()),
+ .pAttachments = config.colorBlendAttachmentStates.data(),
+ };
+
+ return config;
+}
+
+vk::raii::DescriptorSetLayout vulkan_render::Pipeline::MakeDescriptorsLayout(const PipelineConfig &config) const {
+ return context.device.handle.createDescriptorSetLayout(config.descriptorsInfo);
+}
+
+vk::raii::PipelineLayout vulkan_render::Pipeline::MakeLayout(const PipelineConfig &config) const {
+ const vk::PipelineLayoutCreateInfo layoutInfo {
+ .setLayoutCount = 1,
+ .pSetLayouts = &*descriptorsLayout,
+ .pushConstantRangeCount = 1,
+ .pPushConstantRanges = &config.pushConstantRange
+ };
+
+ return context.device.handle.createPipelineLayout(layoutInfo);
+}
+
+vk::raii::Pipeline vulkan_render::Pipeline::MakeInstance(
+ const PipelineConfig &config,
+ const ShaderProgram &shaderProgram
+) const {
+ // Configure shaders
+ std::vector stagesInfo;
+ stagesInfo.reserve(3);
+
+ // Vertex
+ vk::ShaderModuleCreateInfo shaderModuleCreateInfo = {
+ .codeSize = shaderProgram.vertex.size(),
+ .pCode = reinterpret_cast(shaderProgram.vertex.data())
+ };
+ const vk::raii::ShaderModule vertexModule = context.device.handle.createShaderModule(shaderModuleCreateInfo);
+ stagesInfo.push_back({
+ .stage = vk::ShaderStageFlagBits::eVertex,
+ .module = vertexModule,
+ .pName = shaderProgram.vertexMainFun,
+ .pSpecializationInfo = nullptr
+ });
+
+ // Fragment
+ shaderModuleCreateInfo = {
+ .codeSize = shaderProgram.fragment.size(),
+ .pCode = reinterpret_cast(shaderProgram.fragment.data())
+ };
+ const vk::raii::ShaderModule fragmentModule = context.device.handle.createShaderModule(shaderModuleCreateInfo);
+ stagesInfo.push_back({
+ .stage = vk::ShaderStageFlagBits::eFragment,
+ .module = fragmentModule,
+ .pName = shaderProgram.fragmentMainFun,
+ .pSpecializationInfo = nullptr
+ });
+
+ //
+
+ vk::StructureChain pipelineInfo(
+ {
+ .stageCount = static_cast(stagesInfo.size()),
+ .pStages = stagesInfo.data(),
+ .pVertexInputState = &config.vertexInputInfo,
+ .pInputAssemblyState = &config.inputAssemblyInfo,
+ .pViewportState = &config.viewportStateInfo,
+ .pRasterizationState = &config.rasterizerInfo,
+ .pMultisampleState = &config.multisamplingInfo,
+ .pDepthStencilState = &config.depthStencilInfo,
+ .pColorBlendState = &config.colorBlendInfo,
+ .pDynamicState = &config.dynamicStateInfo,
+ .layout = layout,
+ .renderPass = renderPass,
+ .basePipelineHandle = nullptr,
+ .basePipelineIndex = -1,
+ },
+ {
+ .colorAttachmentCount = 1,
+ .pColorAttachmentFormats = &surface.format.format,
+ .depthAttachmentFormat = vk::Format::eD32SfloatS8Uint
+ }
+ );
+
+ return context.device.handle.createGraphicsPipeline(nullptr, pipelineInfo.get());
+}
+
+vk::raii::DescriptorPool vulkan_render::Pipeline::MakeDescriptorPool(const DescriptorBindings &descriptors) {
+ descriptorCounts.clear();
+ for (const vk::DescriptorSetLayoutBinding &binding : descriptors) {
+ descriptorCounts[binding.descriptorType] += binding.descriptorCount;
+ }
+
+ std::vector poolSizes;
+ poolSizes.reserve(descriptorCounts.size());
+ for (const auto &[type, count] : descriptorCounts) {
+ poolSizes.push_back({
+ .type = type,
+ .descriptorCount = count * maxFramesInFlight
+ });
+ }
+
+ return context.device.handle.createDescriptorPool({
+ .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet,
+ .maxSets = static_cast(maxFramesInFlight),
+ .poolSizeCount = static_cast(poolSizes.size()),
+ .pPoolSizes = poolSizes.data()
+ });
+}
+
+vk::raii::DescriptorSets vulkan_render::Pipeline::MakeDescriptorSets() const {
+ std::vector layouts(maxFramesInFlight, *descriptorsLayout);
+
+ return vk::raii::DescriptorSets(
+ context.device.handle,
+ {
+ .descriptorPool = descriptorPool,
+ .descriptorSetCount = maxFramesInFlight,
+ .pSetLayouts = layouts.data()
+ }
+ );
+}
+
+vk::raii::RenderPass vulkan_render::Pipeline::MakerRenderPass() const {
+ const vk::AttachmentDescription colorAttachmentDescription {
+ .format = surface.format.format,
+ .samples = vk::SampleCountFlagBits::e1, // No multisampling
+ .loadOp = vk::AttachmentLoadOp::eClear,
+ .storeOp = vk::AttachmentStoreOp::eStore,
+ .stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
+ .stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
+ .initialLayout = vk::ImageLayout::eUndefined,
+ .finalLayout = vk::ImageLayout::ePresentSrcKHR
+ };
+
+ vk::AttachmentReference colorAttachmentRef {
+ .attachment = 0,
+ .layout = vk::ImageLayout::eColorAttachmentOptimal
+ };
+
+ const vk::AttachmentDescription depthAttachmentDescription {
+ .format = vk::Format::eD32SfloatS8Uint,
+ .samples = vk::SampleCountFlagBits::e1,
+ .loadOp = vk::AttachmentLoadOp::eClear,
+ .storeOp = vk::AttachmentStoreOp::eDontCare,
+ .stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
+ .stencilStoreOp = vk::AttachmentStoreOp::eDontCare ,
+ .initialLayout = vk::ImageLayout::eUndefined,
+ .finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal,
+ };
+
+ vk::AttachmentReference depthAttachmentRef {
+ .attachment = 1,
+ .layout = vk::ImageLayout::eDepthStencilAttachmentOptimal
+ };
+
+ vk::SubpassDescription subpass {
+ .pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
+ .colorAttachmentCount = 1,
+ .pColorAttachments = &colorAttachmentRef,
+ .pDepthStencilAttachment = &depthAttachmentRef
+ };
+
+ vk::SubpassDependency dependency {
+ .srcSubpass = vk::SubpassExternal,
+ .dstSubpass = 0,
+ .srcStageMask = vk::Flags(vk::PipelineStageFlagBits::eColorAttachmentOutput) | vk::PipelineStageFlagBits::eLateFragmentTests,
+ .dstStageMask = vk::Flags(vk::PipelineStageFlagBits::eColorAttachmentOutput) | vk::PipelineStageFlagBits::eEarlyFragmentTests,
+ .srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentWrite,
+ .dstAccessMask = vk::Flags(vk::AccessFlagBits::eColorAttachmentWrite) | vk::AccessFlagBits::eDepthStencilAttachmentWrite,
+ };
+
+ std::array attachments = {colorAttachmentDescription, depthAttachmentDescription};
+ const vk::RenderPassCreateInfo baseRenderPassInfo {
+ .attachmentCount = static_cast(attachments.size()),
+ .pAttachments = attachments.data(),
+ .subpassCount = 1,
+ .pSubpasses = &subpass,
+ .dependencyCount = 1,
+ .pDependencies = &dependency
+ };
+
+ return context.device.handle.createRenderPass(baseRenderPassInfo);
+}
+
+void vulkan_render::Pipeline::MakeFramebuffers(const Texture &depth) {
+ framebuffers.reserve(swapchain.frameViews.size());
+ for (const vk::raii::ImageView &frameView : swapchain.frameViews)
+ {
+ std::array attachments = {*frameView, *depth.view};
+
+ vk::FramebufferCreateInfo frameBufferInfo {
+ .renderPass = renderPass,
+ .attachmentCount = static_cast(attachments.size()),
+ .pAttachments = attachments.data(),
+ .width = surface.extent.width,
+ .height = surface.extent.height,
+ .layers = 1
+ };
+
+ framebuffers.emplace_back(context.device.handle.createFramebuffer(frameBufferInfo));
+ }
+}
diff --git a/sdk/assets/shaders/compile.cmd b/sdk/assets/shaders/compile.cmd
new file mode 100644
index 0000000..70b978a
--- /dev/null
+++ b/sdk/assets/shaders/compile.cmd
@@ -0,0 +1,4 @@
+@echo off
+
+glslc provinces/provinces.frag -O -o ../../../assets/shaders/provinces.frag
+glslc provinces/provinces.vert -O -o ../../../assets/shaders/provinces.vert
\ No newline at end of file
diff --git a/sdk/assets/shaders/provinces/provinces.frag b/sdk/assets/shaders/provinces/provinces.frag
new file mode 100644
index 0000000..1730b4f
--- /dev/null
+++ b/sdk/assets/shaders/provinces/provinces.frag
@@ -0,0 +1,10 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 fragColor;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ outColor = vec4(fragColor, 1.0);
+}
\ No newline at end of file
diff --git a/sdk/assets/shaders/provinces/provinces.vert b/sdk/assets/shaders/provinces/provinces.vert
new file mode 100644
index 0000000..4b5cd46
--- /dev/null
+++ b/sdk/assets/shaders/provinces/provinces.vert
@@ -0,0 +1,35 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec2 position;
+
+layout(binding = 0) uniform Camera {
+ mat4 viewProjection;
+} camera;
+
+layout(push_constant) uniform Model {
+ mat4 model;
+} model;
+
+struct Province {
+ uint regionId;
+ uint color;
+};
+layout(std430, binding = 1) readonly buffer Data {
+ Province provinces[];
+};
+
+layout(location = 0) out vec3 fragColor;
+
+vec3 unpackColor(uint c) {
+ return vec3(
+ c & 0xFFu,
+ (c >> 8) & 0xFFu,
+ (c >> 16) & 0xFFu
+ ) / 255.0;
+}
+
+void main() {
+ gl_Position = camera.viewProjection * vec4(position, 0.0, 1.0);
+ fragColor = unpackColor(provinces[gl_InstanceIndex].color);
+}
\ No newline at end of file